diff --git a/.clang-format b/.clang-format index a7c337f80e..f2d86c57cd 100644 --- a/.clang-format +++ b/.clang-format @@ -49,7 +49,7 @@ ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true -DerivePointerAlignment: true +DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: true diff --git a/.clang-tidy b/.clang-tidy index 5e486e6a0c..b40e606121 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,17 +2,29 @@ Checks: >- *, -abseil-*, + -altera-*, -android-*, -boost-*, - -bugprone-macro-parentheses, + -bugprone-narrowing-conversions, + -bugprone-signed-char-misuse, -cert-dcl50-cpp, -cert-err58-cpp, - -clang-analyzer-core.CallAndMessage, + -cert-oop57-cpp, + -cert-str34-c, + -clang-analyzer-optin.cplusplus.UninitializedObject, -clang-analyzer-osx.*, - -clang-analyzer-security.*, - -cppcoreguidelines-avoid-goto, - -cppcoreguidelines-c-copy-assignment-signature, - -cppcoreguidelines-owning-memory, + -clang-diagnostic-delete-abstract-non-virtual-dtor, + -clang-diagnostic-delete-non-abstract-non-virtual-dtor, + -clang-diagnostic-shadow-field, + -clang-diagnostic-unused-const-variable, + -clang-diagnostic-unused-parameter, + -concurrency-*, + -cppcoreguidelines-avoid-c-arrays, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-init-variables, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-narrowing-conversions, + -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-pointer-arithmetic, @@ -24,42 +36,55 @@ Checks: >- -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-special-member-functions, - -fuchsia-*, - -fuchsia-default-arguments, -fuchsia-multiple-inheritance, -fuchsia-overloaded-operator, -fuchsia-statically-constructed-objects, + -fuchsia-default-arguments-declarations, + -fuchsia-default-arguments-calls, -google-build-using-namespace, -google-explicit-constructor, -google-readability-braces-around-statements, -google-readability-casting, + -google-readability-namespace-comments, -google-readability-todo, - -google-runtime-int, -google-runtime-references, -hicpp-*, + -llvm-else-after-return, -llvm-header-guard, -llvm-include-order, - -misc-unconventional-assign-operator, + -llvm-qualified-auto, + -llvmlibc-*, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, -misc-unused-parameters, - -modernize-deprecated-headers, - -modernize-pass-by-value, - -modernize-pass-by-value, + -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-*, - -performance-unnecessary-value-param, -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, + -readability-make-member-function-const, -readability-named-parameter, + -readability-qualified-auto, + -readability-redundant-access-specifiers, -readability-redundant-member-init, - -warnings-as-errors, - -zircon-* + -readability-redundant-string-init, + -readability-uppercase-literal-suffix, + -readability-use-anyofallof, WarningsAsErrors: '*' -HeaderFilterRegex: '^.*/src/esphome/.*' AnalyzeTemporaryDtors: false FormatStyle: google CheckOptions: @@ -67,9 +92,11 @@ CheckOptions: value: '1' - key: google-readability-function-size.StatementThreshold value: '800' - - key: google-readability-namespace-comments.ShortNamespaceLines + - key: google-runtime-int.TypeSuffix + value: '_t' + - key: llvm-namespace-comment.ShortNamespaceLines value: '10' - - key: google-readability-namespace-comments.SpacesBeforeComments + - key: llvm-namespace-comment.SpacesBeforeComments value: '2' - key: modernize-loop-convert.MaxCopySize value: '16' @@ -83,6 +110,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 @@ -96,15 +127,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 5ce1768f5f..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,13 +31,26 @@ "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", "!include_dir_named scalar", "!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 f24d70487a..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,3 +25,10 @@ indent_size = 2 [*.{yaml,yml}] indent_style = space indent_size = 2 +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/FUNDING.yml b/.github/FUNDING.yml index 52ac3648b0..864586fe6b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,3 @@ # These are supported funding model platforms -github: -patreon: ottowinter -open_collective: -ko_fi: -tidelift: -custom: https://esphome.io/guides/supporters.html +custom: https://www.nabucasa.com diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..4add58dfbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: false +contact_links: + - name: Issue Tracker + url: https://github.com/esphome/issues + about: Please create bug reports in the dedicated issue tracker. + - name: Feature Request Tracker + url: https://github.com/esphome/feature-requests + about: Please create feature requests in the dedicated feature request tracker. + - name: Frequently Asked Question + url: https://esphome.io/guides/faq.html + about: Please view the FAQ for common questions and what to include in a bug report. + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 94a5b7284e..25411c19f5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,40 @@ -## Description: +# What does this implement/fix? +Quick description and explanation of changes -**Related issue (if applicable):** fixes +## Types of changes + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Other + +**Related issue or feature (if applicable):** fixes **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs# +## Test Environment + +- [ ] ESP32 +- [ ] ESP32 IDF +- [ ] ESP8266 + +## Example entry for `config.yaml`: + + +```yaml +# Example config.yaml + +``` + ## Checklist: - [ ] The code change is tested and works locally. - [ ] Tests have been added to verify that the new code works (under `tests/` folder). - + If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). diff --git a/.github/ci-reporter.yml b/.github/ci-reporter.yml deleted file mode 100644 index 243e671532..0000000000 --- a/.github/ci-reporter.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Set to false to create a new comment instead of updating the app's first one -updateComment: true - -# Use a custom string, or set to false to disable -before: "✨ Good work on this PR so far! ✨ Unfortunately, the [ build]() is failing as of . Here's the output:" - -# Use a custom string, or set to false to disable -after: "Thanks for contributing to this project!" diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index f2b357cc95..0000000000 --- a/.github/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot - -# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic -# Anything higher than this threshold will be marked as toxic and commented on -sentimentBotToxicityThreshold: .8 - -# *Required* Comment to reply with -sentimentBotReplyComment: > - Please be sure to review the code of conduct and be respectful of other users. - -# Note: the bot will only work if your repository has a Code of Conduct diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..d73adbfa30 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + ignore: + # Hypotehsis is only used for testing and is updated quite often + - dependency-name: hypothesis diff --git a/.github/issue-close-app.yml b/.github/issue-close-app.yml deleted file mode 100644 index 5f5fb7572d..0000000000 --- a/.github/issue-close-app.yml +++ /dev/null @@ -1,7 +0,0 @@ -comment: >- - https://github.com/esphome/esphome/issues/430 -issueConfigs: -- content: - - "OTHERWISE THE ISSUE WILL BE CLOSED AUTOMATICALLY" - -caseInsensitive: false diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 0680577b2e..0000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,36 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 7 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. Set to `[]` to disable -exemptLabels: - - keep-open - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: false - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: false - -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings just for `issues` or `pulls` -# issues: -# exemptLabels: -# - help-wanted -# lockLabel: outdated - -# pulls: -# daysUntilLock: 30 - -# Repository to extend settings from -# _extends: repo 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 new file mode 100644 index 0000000000..1d1cc169b2 --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,53 @@ +name: CI for docker images + +# Only run when docker paths change +on: + push: + branches: [dev, beta, release] + paths: + - 'docker/**' + - '.github/workflows/**' + - 'requirements*.txt' + - 'platformio.ini' + + pull_request: + paths: + - 'docker/**' + - '.github/workflows/**' + - 'requirements*.txt' + - 'platformio.ini' + +permissions: + contents: read + packages: read + +jobs: + check-docker: + name: Build docker containers + runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64, armv7, aarch64] + 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: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set TAG + run: | + echo "TAG=check" >> $GITHUB_ENV + + - 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 new file mode 100644 index 0000000000..93a29874f7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,166 @@ +name: CI + +on: + push: + branches: [dev, beta, release] + + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + ci: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + 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: test3 + - 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 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + id: python + with: + python-version: '3.7' + + - name: Cache virtualenv + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} + restore-keys: | + venv-${{ steps.python.outputs.python-version }}- + + - name: Set up virtualenv + run: | + python -m venv .venv + source .venv/bin/activate + pip install -U pip + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . + echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH + echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV + + # 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 + script/build_codeowners.py --check + if: matrix.id == 'ci-custom' + + - name: Lint Python + run: script/lint-python + if: matrix.id == 'lint-python' + + - 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: Run pytest + run: | + 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/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 0000000000..ceb45b2a91 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,27 @@ +name: Lock + +on: + schedule: + - cron: '30 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v3 + with: + pr-inactive-days: "1" + pr-lock-reason: "" + exclude-any-pr-labels: keep-open + + issue-inactive-days: "7" + issue-lock-reason: "" + exclude-any-issue-labels: keep-open diff --git a/.github/workflows/matchers/ci-custom.json b/.github/workflows/matchers/ci-custom.json new file mode 100644 index 0000000000..1d5f2551cd --- /dev/null +++ b/.github/workflows/matchers/ci-custom.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "ci-custom", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+lint:\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/clang-tidy.json b/.github/workflows/matchers/clang-tidy.json new file mode 100644 index 0000000000..03e77341a5 --- /dev/null +++ b/.github/workflows/matchers/clang-tidy.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "clang-tidy", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/gcc.json b/.github/workflows/matchers/gcc.json new file mode 100644 index 0000000000..a00d9c33f4 --- /dev/null +++ b/.github/workflows/matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "gcc", + "severity": "error", + "pattern": [ + { + "regexp": "^src/(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/lint-python.json b/.github/workflows/matchers/lint-python.json new file mode 100644 index 0000000000..6a09f04770 --- /dev/null +++ b/.github/workflows/matchers/lint-python.json @@ -0,0 +1,39 @@ +{ + "problemMatcher": [ + { + "owner": "black", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*): (Please format this file with the black formatter)", + "file": 1, + "message": 2 + } + ] + }, + { + "owner": "flake8", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+): ([EFCDNW]\\d{3}.*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + }, + { + "owner": "pylint", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+): (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ] +} 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/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 0000000000..9c3095c0c9 --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..d6895becc0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,154 @@ +name: Publish Release + +on: + workflow_dispatch: + release: + types: [published] + schedule: + - cron: "0 2 * * *" + +permissions: + contents: read + +jobs: + init: + name: Initialize build + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - uses: actions/checkout@v2 + - name: Get tag + id: tag + run: | + 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' && github.event_name == 'release' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Set up python environment + run: | + script/setup + pip install setuptools wheel twine + - name: Build + run: python setup.py sdist bdist_wheel + - name: Upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload dist/* + + deploy-docker: + name: Build and publish docker containers + if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + needs: [init] + strategy: + matrix: + arch: [amd64, armv7, aarch64] + 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: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - 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 }} + + - 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' + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + 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: 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 }} + + - name: Run manifest + run: | + docker/build.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --build-type "${{ matrix.build_type }}" \ + manifest + + deploy-hassio-repo: + 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/}" + 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\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..c3e450d0cf --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,48 @@ +name: Stale + +on: + schedule: + - cron: '30 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + 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. + + # Use stale to automatically close issues with a reference to the issue tracker + close-issues: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + days-before-pr-stale: -1 + days-before-pr-close: -1 + days-before-issue-stale: 1 + days-before-issue-close: 1 + remove-stale-when-updated: true + stale-issue-label: "stale" + exempt-issue-labels: "not-stale" + stale-issue-message: > + https://github.com/esphome/esphome/issues/430 diff --git a/.gitignore b/.gitignore index fa4670769b..57b8478bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ __pycache__/ # Intellij Idea .idea +# Vim +*.swp + # Hide some OS X stuff .DS_Store .AppleDouble @@ -51,8 +54,10 @@ htmlcov/ .coverage .coverage.* .cache +.esphome nosetests.xml coverage.xml +cov.xml *.cover .hypothesis/ .pytest_cache/ @@ -79,7 +84,8 @@ venv.bak/ .pioenvs .piolibdeps .pio -.vscode +.vscode/ +!.vscode/tasks.json CMakeListsPrivate.txt CMakeLists.txt @@ -96,8 +102,7 @@ CMakeLists.txt .idea/**/dynamic.xml # CMake -cmake-build-debug/ -cmake-build-release/ +cmake-build-*/ CMakeCache.txt CMakeFiles @@ -117,3 +122,8 @@ config/ tests/build/ tests/.esphome/ /.temp-clang-tidy.cpp +/.temp/ +.pio/ + +sdkconfig.* +!sdkconfig.defaults diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 3db0b982ae..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,342 +0,0 @@ ---- -# Based on https://gitlab.com/hassio-addons/addon-node-red/blob/master/.gitlab-ci.yml -variables: - DOCKER_DRIVER: overlay2 - DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '2.0.1' - TZ: UTC - -stages: - - lint - - test - - deploy - -.lint: &lint - image: esphome/esphome-lint:latest - stage: lint - before_script: - - script/setup - tags: - - docker - -.test: &test - image: esphome/esphome-lint:latest - stage: test - before_script: - - script/setup - tags: - - docker - -.docker-base: &docker-base - image: esphome/esphome-base-builder - before_script: - - docker info - - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" - script: - - docker run --rm --privileged multiarch/qemu-user-static:4.1.0-1 --reset -p yes - - TAG="${CI_COMMIT_TAG#v}" - - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - - echo "Tag ${TAG}" - - - | - if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} - BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} - DOCKERFILE=docker/Dockerfile.hassio - else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} - if [[ "${BUILD_ARCH}" == "amd64" ]]; then - BUILD_TO=esphome/esphome - else - BUILD_TO=esphome/esphome-${BUILD_ARCH} - fi - DOCKERFILE=docker/Dockerfile - fi - - - | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --file "${DOCKERFILE}" \ - . - - | - if [[ "${RELEASE}" = "YES" ]]; then - echo "Pushing to ${BUILD_TO}:${TAG}" - docker push "${BUILD_TO}:${TAG}" - fi - - | - if [[ "${LATEST}" = "YES" ]]; then - echo "Pushing to :latest" - docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:latest - docker push ${BUILD_TO}:latest - fi - - | - if [[ "${BETA}" = "YES" ]]; then - echo "Pushing to :beta" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:beta - docker push ${BUILD_TO}:beta - fi - - | - if [[ "${DEV}" = "YES" ]]; then - echo "Pushing to :dev" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:dev - docker push ${BUILD_TO}:dev - fi - services: - - docker:dind - tags: - - docker - stage: deploy - -lint-custom: - <<: *lint - script: - - script/ci-custom.py - -lint-python: - <<: *lint - script: - - script/lint-python - -lint-tidy: - <<: *lint - script: - - pio init --ide atom - - script/clang-tidy --all-headers --fix - - script/ci-suggest-changes - -lint-format: - <<: *lint - script: - - script/clang-format -i - - script/ci-suggest-changes - -test1: - <<: *test - script: - - esphome tests/test1.yaml compile - -test2: - <<: *test - script: - - esphome tests/test2.yaml compile - -test3: - <<: *test - script: - - esphome tests/test3.yaml compile - -.deploy-pypi: &deploy-pypi - <<: *lint - stage: deploy - script: - - pip install twine wheel - - python setup.py sdist bdist_wheel - - twine upload dist/* - -deploy-release:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+$/ - except: - - /^(?!master).+@/ - -deploy-beta:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+b\d+$/ - except: - - /^(?!rc).+@/ - -.latest: &latest - <<: *docker-base - only: - - /^v([0-9\.]+)$/ - except: - - branches - -.beta: &beta - <<: *docker-base - only: - - /^v([0-9\.]+b\d+)$/ - except: - - branches - -.dev: &dev - <<: *docker-base - only: - - dev - -aarch64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - RELEASE: "YES" -aarch64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - RELEASE: "YES" -aarch64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "NO" -aarch64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "YES" -aarch64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -aarch64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -amd64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - RELEASE: "YES" -amd64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - RELEASE: "YES" -amd64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "NO" -amd64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "YES" -amd64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -amd64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -armv7-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - RELEASE: "YES" -armv7-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - RELEASE: "YES" -armv7-dev-docker: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "NO" -armv7-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "YES" -armv7-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -armv7-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -i386-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - RELEASE: "YES" -i386-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - RELEASE: "YES" -i386-dev-docker: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "NO" -i386-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "YES" -i386-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -i386-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d57da791fd..a821c21fa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,27 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + - repo: https://github.com/ambv/black + rev: 20.8b1 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: flake8 + - id: black + args: + - --safe + - --quiet + files: ^((esphome|script|tests)/.+)?[^/]+\.py$ + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.1.1 + files: ^(esphome|tests)/.+\.py$ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: no-commit-to-branch + args: + - --branch=dev + - --branch=release + - --branch=beta diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ca0a3082db..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -sudo: false -language: python -python: '3.6' -install: script/setup -cache: - directories: - - "~/.platformio" - -matrix: - fast_finish: true - include: - - python: "3.7" - env: TARGET=Lint3.7 - script: - - script/ci-custom.py - - flake8 esphome - - pylint esphome - - python: "3.6" - env: TARGET=Test3.6 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - - env: TARGET=Cpp-Lint - dist: trusty - sudo: required - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-7 - packages: - - clang-tidy-7 - - clang-format-7 - before_script: - - pio init --ide atom - - clang-tidy-7 -version - - clang-format-7 -version - - clang-apply-replacements-7 -version - script: - - script/clang-tidy --all-headers -j 2 --fix - - script/clang-format -i -j 2 - - script/ci-suggest-changes diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..b6584bc735 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,32 @@ +{ + "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": [ + { + "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 new file mode 100644 index 0000000000..452c560938 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,194 @@ +# This file is generated by script/build_codeowners.py +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# +# Every time an issue is created with a label corresponding to an integration, +# the integration's code owner is automatically notified. + +# Core Code +setup.py @esphome/core +esphome/*.py @esphome/core +esphome/core/* @esphome/core + +# Integrations +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/button/* @esphome/core +esphome/components/canbus/* @danielschramm @mvturnho +esphome/components/cap1188/* @MrEditor97 +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/cse7761/* @berfenger +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_server/* @jesserockz +esphome/components/esp32_camera_web_server/* @ayufan +esphome/components/esp32_improv/* @jesserockz +esphome/components/esp8266/* @esphome/core +esphome/components/exposure_notifications/* @OttoWinter +esphome/components/ezo/* @ssieb +esphome/components/fastled_base/* @OttoWinter +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/growatt_solar/* @leeuwte +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_serial/* @esphome/core +esphome/components/inkbird_ibsth1_mini/* @fkirill +esphome/components/inkplate6/* @jesserockz +esphome/components/integration/* @OttoWinter +esphome/components/interval/* @esphome/core +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 +esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz +esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz +esphome/components/mcp23x08_base/* @jesserockz +esphome/components/mcp23x17_base/* @jesserockz +esphome/components/mcp23xxx_base/* @jesserockz +esphome/components/mcp2515/* @danielschramm @mvturnho +esphome/components/mcp9808/* @k7hpn +esphome/components/md5/* @esphome/core +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/* @jsuanet @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 @jsuanet +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 +esphome/components/ssd1325_base/* @kbx81 +esphome/components/ssd1325_spi/* @kbx81 +esphome/components/ssd1327_base/* @kbx81 +esphome/components/ssd1327_i2c/* @kbx81 +esphome/components/ssd1327_spi/* @kbx81 +esphome/components/ssd1331_base/* @kbx81 +esphome/components/ssd1331_spi/* @kbx81 +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/number/* @frankiboy1 +esphome/components/tuya/sensor/* @jesserockz +esphome/components/tuya/switch/* @jesserockz +esphome/components/tuya/text_sensor/* @dentra +esphome/components/uart/* @esphome/core +esphome/components/ultrasonic/* @OttoWinter +esphome/components/version/* @esphome/core +esphome/components/web_server_base/* @OttoWinter +esphome/components/whirlpool/* @glmnet +esphome/components/xiaomi_lywsd03mmc/* @ahpohl +esphome/components/xiaomi_mhoc401/* @vevsvevs +esphome/components/xpt2046/* @numo68 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c5be227278..b91a3b4f83 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@otto-winter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esphome@nabucasa.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 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/MANIFEST.in b/MANIFEST.in index cdea2df2a6..0fe80762b3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE include README.md +include requirements.txt include esphome/dashboard/templates/*.html recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE recursive-include esphome *.cpp *.h *.tcc 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 11bbeeda2b..62a64c851d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,156 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.1 -FROM ${BUILD_FROM} +# Build these with the build.py script +# Example: +# python3 docker/build.py --tag dev --arch amd64 --build-type docker build -COPY . . -RUN pip3 install --no-cache-dir -e . +# One of "docker", "hassio" +ARG BASEIMGTYPE=docker -ENV USERNAME="" -ENV PASSWORD="" +FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.1 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.1 AS base-hassio-arm64 +FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.1 AS base-hassio-armv7 +FROM debian:bullseye-20211011-slim AS base-docker-amd64 +FROM debian:bullseye-20211011-slim AS base-docker-arm64 +FROM debian:bullseye-20211011-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.2 \ + # 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 / +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 + +# Settings for dashboard +ENV USERNAME="" PASSWORD="" + +# Expose the dashboard to Docker +EXPOSE 6052 + +COPY docker/docker_entrypoint.sh /entrypoint.sh + +# The directory the user should mount their configuration files to +VOLUME /config WORKDIR /config -ENTRYPOINT ["esphome"] -CMD ["/config", "dashboard"] +# Set entrypoint to esphome (via a script) so that the user doesn't have to type 'esphome' +# in every docker command twice +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 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/Dockerfile.dev b/docker/Dockerfile.dev deleted file mode 100644 index a3871e2513..0000000000 --- a/docker/Dockerfile.dev +++ /dev/null @@ -1,13 +0,0 @@ -FROM esphome/esphome-base-amd64:2.0.1 - -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 e5c9625680..0000000000 --- a/docker/Dockerfile.hassio +++ /dev/null @@ -1,19 +0,0 @@ -ARG BUILD_FROM -FROM ${BUILD_FROM} - -# Copy root filesystem -COPY docker/rootfs/ / -COPY setup.py setup.cfg MANIFEST.in /opt/esphome/ -COPY esphome /opt/esphome/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 2d77502dc2..0000000000 --- a/docker/Dockerfile.lint +++ /dev/null @@ -1,18 +0,0 @@ -FROM esphome/esphome-base-amd64:2.0.1 - -RUN \ - apt-get update \ - && apt-get install -y --no-install-recommends \ - clang-format-7 \ - clang-tidy-7 \ - patch \ - && rm -rf \ - /tmp/* \ - /var/{cache,log}/* \ - /var/lib/apt/lists/* - -COPY requirements_test.txt /requirements_test.txt -RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt - -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 old mode 100755 new mode 100644 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 61% rename from docker/rootfs/etc/services.d/esphome/run rename to docker/hassio-rootfs/etc/services.d/esphome/run index 6257bec6a3..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 /config/esphome dashboard --socket /var/run/esphome.sock --hassio +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 new file mode 100755 index 0000000000..c7b11cf321 --- /dev/null +++ b/docker/platformio_install_deps.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# This script is used in the docker containers to preinstall +# all platformio libraries in the global storage + +import configparser +import subprocess +import sys + +config = configparser.ConfigParser(inline_comment_prefixes=(';', )) +config.read(sys.argv[1]) + +libs = [] +# Extract from every lib_deps key in all sections +for section in config.sections(): + conf = config[section] + if "lib_deps" not in conf: + continue + for lib_dep in conf["lib_deps"].splitlines(): + if not lib_dep: + # Empty line or comment + continue + if lib_dep.startswith("${"): + # Extending from another section + continue + if "@" not in lib_dep: + # No version pinned, this is an internal lib + continue + libs.append(lib_dep) + +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 100644 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 100644 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 73723dfa00..6f57791480 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -8,33 +8,39 @@ from datetime import datetime from esphome import const, writer, yaml_util import esphome.codegen as cg from esphome.config import iter_components, read_config, strip_default_ids -from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ - CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS -from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority -from esphome.helpers import color, indent -from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files +from esphome.const import ( + CONF_BAUD_RATE, + CONF_BROKER, + CONF_DEASSERT_RTS_DTR, + CONF_LOGGER, + CONF_OTA, + CONF_PASSWORD, + CONF_PORT, + CONF_ESPHOME, + CONF_PLATFORMIO_OPTIONS, + SECRETS_FILES, +) +from esphome.core import CORE, EsphomeError, coroutine +from esphome.helpers import indent +from esphome.util import ( + run_external_command, + run_external_process, + safe_print, + list_yaml_files, + get_serial_ports, +) +from esphome.log import color, setup_log, Fore _LOGGER = logging.getLogger(__name__) -def get_serial_ports(): - # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py - from serial.tools.list_ports import comports - result = [] - for port, desc, info in comports(include_links=True): - if not port: - continue - if "VID:PID" in info: - result.append((port, desc)) - result.sort(key=lambda x: x[0]) - return result - - def choose_prompt(options): if not options: - raise EsphomeError("Found no valid options for upload/logging, please make sure relevant " - "sections (ota, mqtt, ...) are in your configuration and/or the device " - "is plugged in.") + raise EsphomeError( + "Found no valid options for upload/logging, please make sure relevant " + "sections (ota, api, mqtt, ...) are in your configuration and/or the " + "device is plugged in." + ) if len(options) == 1: return options[0][1] @@ -44,7 +50,7 @@ def choose_prompt(options): safe_print(f" [{i+1}] {desc}") while True: - opt = input('(number): ') + opt = input("(number): ") if opt in options: opt = options.index(opt) break @@ -54,22 +60,22 @@ def choose_prompt(options): raise ValueError break except ValueError: - safe_print(color('red', f"Invalid option: '{opt}'")) + safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) return options[opt - 1][1] def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): options = [] - for res, desc in get_serial_ports(): - options.append((f"{res} ({desc})", res)) - if (show_ota and 'ota' in CORE.config) or (show_api and 'api' in CORE.config): + for port in get_serial_ports(): + options.append((f"{port.path} ({port.description})", port.path)) + if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): options.append((f"Over The Air ({CORE.address})", CORE.address)) - if default == 'OTA': + if default == "OTA": return CORE.address - if show_mqtt and 'mqtt' in CORE.config: - options.append(("MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) - if default == 'OTA': - return 'MQTT' + if show_mqtt and "mqtt" in CORE.config: + options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) + if default == "OTA": + return "MQTT" if default is not None: return default if check_default is not None and check_default in [opt[1] for opt in options]: @@ -78,11 +84,11 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api def get_port_type(port): - if port.startswith('/') or port.startswith('COM'): - return 'SERIAL' - if port == 'MQTT': - return 'MQTT' - return 'NETWORK' + if port.startswith("/") or port.startswith("COM"): + return "SERIAL" + if port == "MQTT": + return "MQTT" + return "NETWORK" def run_miniterm(config, port): @@ -92,45 +98,69 @@ def run_miniterm(config, port): if CONF_LOGGER not in config: _LOGGER.info("Logger is not enabled. Not starting UART logs.") return - baud_rate = config['logger'][CONF_BAUD_RATE] + 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() except serial.SerialException: _LOGGER.error("Serial port closed!") return - line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', 'backslashreplace') - time = datetime.now().time().strftime('[%H:%M:%S]') + line = ( + raw.replace(b"\r", b"") + .replace(b"\n", b"") + .decode("utf8", "backslashreplace") + ) + time = datetime.now().time().strftime("[%H:%M:%S]") message = time + line safe_print(message) backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state) + config, line, backtrace_state=backtrace_state + ) def wrap_to_code(name, comp): coro = coroutine(comp.to_code) @functools.wraps(comp.to_code) - @coroutine_with_priority(coro.priority) - def wrapped(conf): + async def wrapped(conf): cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: conf_str = yaml_util.dump(conf) - conf_str = conf_str.replace('//', '') + conf_str = conf_str.replace("//", "") + # remove tailing \ to avoid multi-line comment warning + conf_str = conf_str.replace("\\\n", "\n") cg.add(cg.LineComment(indent(conf_str))) - yield coro(conf) + await coro(conf) + if hasattr(coro, "priority"): + wrapped.priority = coro.priority return wrapped def write_cpp(config): + generate_cpp_contents(config) + return write_cpp_file() + + +def generate_cpp_contents(config): _LOGGER.info("Generating C++ source...") for name, component, conf in iter_components(CORE.config): @@ -140,6 +170,8 @@ def write_cpp(config): CORE.flush_tasks() + +def write_cpp_file(): writer.write_platformio_project() code_s = indent(CORE.cpp_main_section) @@ -151,20 +183,60 @@ def compile_program(args, config): from esphome import platformio_api _LOGGER.info("Compiling app...") - return platformio_api.run_compile(config, CORE.verbose) + rc = platformio_api.run_compile(config, CORE.verbose) + if rc != 0: + return rc + idedata = platformio_api.get_idedata(config) + return 0 if idedata is not None else 1 def upload_using_esptool(config, port): - path = CORE.firmware_bin - first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 460800) + from esphome import platformio_api + + first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( + "upload_speed", 460800 + ) def run_esptool(baud_rate): - cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset', - '--baud', str(baud_rate), - '--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path] + idedata = platformio_api.get_idedata(config) - if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: + 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", + "default_reset", + "--after", + "hard_reset", + "--baud", + str(baud_rate), + "--port", + port, + "--chip", + mcu, + "write_flash", + "-z", + "--flash_size", + "detect", + ] + for img in flash_images: + cmd += [img.offset, img.path] + + if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: import esptool + # pylint: disable=protected-access return run_external_command(esptool._main, *cmd) @@ -174,46 +246,48 @@ def upload_using_esptool(config, port): if rc == 0 or first_baudrate == 115200: return rc # Try with 115200 baud rate, with some serial chips the faster baud rates do not work well - _LOGGER.info("Upload with baud rate %s failed. Trying again with baud rate 115200.", - first_baudrate) + _LOGGER.info( + "Upload with baud rate %s failed. Trying again with baud rate 115200.", + first_baudrate, + ) return run_esptool(115200) 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) + if get_port_type(host) == "SERIAL": + return upload_using_esptool(config, host) from esphome import espota2 if CONF_OTA not in config: - raise EsphomeError("Cannot upload Over the Air as the config does not include the ota: " - "component") + raise EsphomeError( + "Cannot upload Over the Air as the config does not include the ota: " + "component" + ) 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) def show_logs(config, args, port): - if 'logger' not in config: + if "logger" not in config: raise EsphomeError("Logger is not configured!") - if get_port_type(port) == 'SERIAL': + if get_port_type(port) == "SERIAL": run_miniterm(config, port) return 0 - if get_port_type(port) == 'NETWORK' and 'api' in config: - from esphome.api.client import run_logs + if get_port_type(port) == "NETWORK" and "api" in config: + from esphome.components.api.client import run_logs return run_logs(config, port) - if get_port_type(port) == 'MQTT' and 'mqtt' in config: + if get_port_type(port) == "MQTT" and "mqtt" in config: from esphome import mqtt - return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id) + return mqtt.show_logs( + config, args.topic, args.username, args.password, args.client_id + ) raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)") @@ -221,46 +295,15 @@ def show_logs(config, args, port): def clean_mqtt(config, args): from esphome import mqtt - return mqtt.clear_topic(config, args.topic, args.username, args.password, args.client_id) - - -def setup_log(debug=False, quiet=False): - if debug: - log_level = logging.DEBUG - CORE.verbose = True - elif quiet: - log_level = logging.CRITICAL - else: - log_level = logging.INFO - logging.basicConfig(level=log_level) - fmt = "%(levelname)s %(message)s" - colorfmt = f"%(log_color)s{fmt}%(reset)s" - datefmt = '%H:%M:%S' - - logging.getLogger('urllib3').setLevel(logging.WARNING) - - try: - from colorlog import ColoredFormatter - logging.getLogger().handlers[0].setFormatter(ColoredFormatter( - colorfmt, - datefmt=datefmt, - reset=True, - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', - } - )) - except ImportError: - pass + return mqtt.clear_topic( + config, args.topic, args.username, args.password, args.client_id + ) def command_wizard(args): from esphome import wizard - return wizard.wizard(args.configuration[0]) + return wizard.wizard(args.configuration) def command_config(args, config): @@ -274,7 +317,8 @@ def command_config(args, config): def command_vscode(args): from esphome import vscode - CORE.config_path = args.configuration[0] + logging.disable(logging.INFO) + logging.disable(logging.WARNING) vscode.read_config(args) @@ -293,8 +337,13 @@ def command_compile(args, config): def command_upload(args, config): - port = choose_upload_log_host(default=args.upload_port, check_default=None, - show_ota=True, show_mqtt=False, show_api=False) + port = choose_upload_log_host( + default=args.device, + check_default=None, + show_ota=True, + show_mqtt=False, + show_api=False, + ) exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code @@ -303,8 +352,13 @@ def command_upload(args, config): def command_logs(args, config): - port = choose_upload_log_host(default=args.serial_port, check_default=None, - show_ota=False, show_mqtt=True, show_api=True) + port = choose_upload_log_host( + default=args.device, + check_default=None, + show_ota=False, + show_mqtt=True, + show_api=True, + ) return show_logs(config, args, port) @@ -316,16 +370,26 @@ def command_run(args, config): if exit_code != 0: return exit_code _LOGGER.info("Successfully compiled program.") - port = choose_upload_log_host(default=args.upload_port, check_default=None, - show_ota=True, show_mqtt=False, show_api=True) + port = choose_upload_log_host( + default=args.device, + check_default=None, + show_ota=True, + show_mqtt=False, + show_api=True, + ) exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code _LOGGER.info("Successfully uploaded program.") if args.no_logs: return 0 - port = choose_upload_log_host(default=args.upload_port, check_default=port, - show_ota=False, show_mqtt=True, show_api=True) + port = choose_upload_log_host( + default=args.device, + check_default=port, + show_ota=False, + show_mqtt=True, + show_api=True, + ) return show_logs(config, args, port) @@ -364,7 +428,7 @@ def command_update_all(args): import click success = {} - files = list_yaml_files(args.configuration[0]) + files = list_yaml_files(args.configuration) twidth = 60 def print_bar(middle_text): @@ -374,167 +438,371 @@ def command_update_all(args): click.echo(f"{half_line}{middle_text}{half_line}") for f in files: - print("Updating {}".format(color('cyan', f))) - print('-' * twidth) + print(f"Updating {color(Fore.CYAN, f)}") + print("-" * twidth) print() - rc = run_external_process('esphome', '--dashboard', f, 'run', '--no-logs', '--upload-port', - 'OTA') + rc = run_external_process( + "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" + ) if rc == 0: - print_bar("[{}] {}".format(color('bold_green', 'SUCCESS'), f)) + print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") success[f] = True else: - print_bar("[{}] {}".format(color('bold_red', 'ERROR'), f)) + print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") success[f] = False print() print() print() - print_bar('[{}]'.format(color('bold_white', 'SUMMARY'))) + print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") failed = 0 for f in files: if success[f]: - print(" - {}: {}".format(f, color('green', 'SUCCESS'))) + print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}") else: - print(" - {}: {}".format(f, color('bold_red', 'FAILED'))) + print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}") failed += 1 return failed +def command_idedata(args, config): + from esphome import platformio_api + import json + + logging.disable(logging.INFO) + logging.disable(logging.WARNING) + + idedata = platformio_api.get_idedata(config) + if idedata is None: + return 1 + + print(json.dumps(idedata.raw, indent=2) + "\n") + return 0 + + PRE_CONFIG_ACTIONS = { - 'wizard': command_wizard, - 'version': command_version, - 'dashboard': command_dashboard, - 'vscode': command_vscode, - 'update-all': command_update_all, + "wizard": command_wizard, + "version": command_version, + "dashboard": command_dashboard, + "vscode": command_vscode, + "update-all": command_update_all, } POST_CONFIG_ACTIONS = { - 'config': command_config, - 'compile': command_compile, - 'upload': command_upload, - 'logs': command_logs, - 'run': command_run, - 'clean-mqtt': command_clean_mqtt, - 'mqtt-fingerprint': command_mqtt_fingerprint, - 'clean': command_clean, + "config": command_config, + "compile": command_compile, + "upload": command_upload, + "logs": command_logs, + "run": command_run, + "clean-mqtt": command_clean_mqtt, + "mqtt-fingerprint": command_mqtt_fingerprint, + "clean": command_clean, + "idedata": command_idedata, } def parse_args(argv): - parser = argparse.ArgumentParser(description=f'ESPHome v{const.__version__}') - parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.", - action='store_true') - parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", - action='store_true') - parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true') - parser.add_argument('configuration', help='Your YAML configuration file.', nargs='*') + options_parser = argparse.ArgumentParser(add_help=False) + options_parser.add_argument( + "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true" + ) + options_parser.add_argument( + "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" + ) + options_parser.add_argument( + "--dashboard", help=argparse.SUPPRESS, action="store_true" + ) + options_parser.add_argument( + "-s", + "--substitution", + nargs=2, + action="append", + help="Add a substitution", + metavar=("key", "value"), + ) - subparsers = parser.add_subparsers(help='Commands', dest='command') + parser = argparse.ArgumentParser( + description=f"ESPHome v{const.__version__}", parents=[options_parser] + ) + + mqtt_options = argparse.ArgumentParser(add_help=False) + mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.") + mqtt_options.add_argument("--username", help="Manually set the MQTT username.") + mqtt_options.add_argument("--password", help="Manually set the MQTT password.") + mqtt_options.add_argument("--client-id", help="Manually set the MQTT client id.") + + subparsers = parser.add_subparsers( + help="Command to run:", dest="command", metavar="command" + ) subparsers.required = True - subparsers.add_parser('config', help='Validate the configuration and spit it out.') - parser_compile = subparsers.add_parser('compile', - help='Read the configuration and compile a program.') - parser_compile.add_argument('--only-generate', - help="Only generate source code, do not compile.", - action='store_true') + parser_config = subparsers.add_parser( + "config", help="Validate the configuration and spit it out." + ) + parser_config.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) - parser_upload = subparsers.add_parser('upload', help='Validate the configuration ' - 'and upload the latest binary.') - parser_upload.add_argument('--upload-port', help="Manually specify the upload port to use. " - "For example /dev/cu.SLAB_USBtoUART.") + parser_compile = subparsers.add_parser( + "compile", help="Read the configuration and compile a program." + ) + parser_compile.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + parser_compile.add_argument( + "--only-generate", + help="Only generate source code, do not compile.", + action="store_true", + ) - parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' - 'and show all MQTT logs.') - parser_logs.add_argument('--topic', help='Manually set the topic to subscribe to.') - parser_logs.add_argument('--username', help='Manually set the username.') - parser_logs.add_argument('--password', help='Manually set the password.') - parser_logs.add_argument('--client-id', help='Manually set the client id.') - parser_logs.add_argument('--serial-port', help="Manually specify a serial port to use" - "For example /dev/cu.SLAB_USBtoUART.") + parser_upload = subparsers.add_parser( + "upload", help="Validate the configuration and upload the latest binary." + ) + parser_upload.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + parser_upload.add_argument( + "--device", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + ) - parser_run = subparsers.add_parser('run', help='Validate the configuration, create a binary, ' - 'upload it, and start MQTT logs.') - parser_run.add_argument('--upload-port', help="Manually specify the upload port/ip to use. " - "For example /dev/cu.SLAB_USBtoUART.") - parser_run.add_argument('--no-logs', help='Disable starting MQTT logs.', - action='store_true') - parser_run.add_argument('--topic', help='Manually set the topic to subscribe to for logs.') - parser_run.add_argument('--username', help='Manually set the MQTT username for logs.') - parser_run.add_argument('--password', help='Manually set the MQTT password for logs.') - parser_run.add_argument('--client-id', help='Manually set the client id for logs.') + parser_logs = subparsers.add_parser( + "logs", + help="Validate the configuration and show all logs.", + parents=[mqtt_options], + ) + parser_logs.add_argument( + "configuration", help="Your YAML configuration file.", nargs=1 + ) + parser_logs.add_argument( + "--device", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + ) - parser_clean = subparsers.add_parser('clean-mqtt', help="Helper to clear an MQTT topic from " - "retain messages.") - parser_clean.add_argument('--topic', help='Manually set the topic to subscribe to.') - parser_clean.add_argument('--username', help='Manually set the username.') - parser_clean.add_argument('--password', help='Manually set the password.') - parser_clean.add_argument('--client-id', help='Manually set the client id.') + parser_run = subparsers.add_parser( + "run", + help="Validate the configuration, create a binary, upload it, and start logs.", + parents=[mqtt_options], + ) + parser_run.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + parser_run.add_argument( + "--device", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + ) + parser_run.add_argument( + "--no-logs", help="Disable starting logs.", action="store_true" + ) - subparsers.add_parser('wizard', help="A helpful setup wizard that will guide " - "you through setting up esphome.") + parser_clean = subparsers.add_parser( + "clean-mqtt", + help="Helper to clear retained messages from an MQTT topic.", + parents=[mqtt_options], + ) + parser_clean.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) - subparsers.add_parser('mqtt-fingerprint', help="Get the SSL fingerprint from a MQTT broker.") + parser_wizard = subparsers.add_parser( + "wizard", + help="A helpful setup wizard that will guide you through setting up ESPHome.", + ) + parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - subparsers.add_parser('version', help="Print the esphome version and exit.") + parser_fingerprint = subparsers.add_parser( + "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." + ) + parser_fingerprint.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) - subparsers.add_parser('clean', help="Delete all temporary build files.") + subparsers.add_parser("version", help="Print the ESPHome version and exit.") - dashboard = subparsers.add_parser('dashboard', - help="Create a simple web server for a dashboard.") - dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.", - type=int, default=6052) - dashboard.add_argument("--username", help="The optional username to require " - "for authentication.", - type=str, default='') - dashboard.add_argument("--password", help="The optional password to require " - "for authentication.", - type=str, default='') - dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.", - action='store_true') - dashboard.add_argument("--hassio", - help=argparse.SUPPRESS, - action="store_true") - dashboard.add_argument("--socket", - help="Make the dashboard serve under a unix socket", type=str) + parser_clean = subparsers.add_parser( + "clean", help="Delete all temporary build files." + ) + parser_clean.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) - vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS) - vscode.add_argument('--ace', action='store_true') + parser_dashboard = subparsers.add_parser( + "dashboard", help="Create a simple web server for a dashboard." + ) + parser_dashboard.add_argument( + "configuration", help="Your YAML configuration file directory." + ) + parser_dashboard.add_argument( + "--port", + help="The HTTP port to open connections on. Defaults to 6052.", + type=int, + default=6052, + ) + parser_dashboard.add_argument( + "--address", + help="The address to bind to.", + type=str, + default="0.0.0.0", + ) + parser_dashboard.add_argument( + "--username", + help="The optional username to require for authentication.", + type=str, + default="", + ) + parser_dashboard.add_argument( + "--password", + help="The optional password to require for authentication.", + type=str, + default="", + ) + parser_dashboard.add_argument( + "--open-ui", help="Open the dashboard UI in a browser.", action="store_true" + ) + parser_dashboard.add_argument( + "--hassio", help=argparse.SUPPRESS, action="store_true" + ) + parser_dashboard.add_argument( + "--socket", help="Make the dashboard serve under a unix socket", type=str + ) - subparsers.add_parser('update-all', help=argparse.SUPPRESS) + parser_vscode = subparsers.add_parser("vscode") + parser_vscode.add_argument("configuration", help="Your YAML configuration file.") + parser_vscode.add_argument("--ace", action="store_true") - return parser.parse_args(argv[1:]) + parser_update = subparsers.add_parser("update-all") + parser_update.add_argument( + "configuration", help="Your YAML configuration file directories.", nargs="+" + ) + + parser_idedata = subparsers.add_parser("idedata") + parser_idedata.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs=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): args = parse_args(argv) CORE.dashboard = args.dashboard - setup_log(args.verbose, args.quiet) - if args.command != 'version' and not args.configuration: - _LOGGER.error("Missing configuration parameter, see esphome --help.") - return 1 + setup_log( + args.verbose, + args.quiet, + # Show timestamp for dashboard access logs + args.command == "dashboard", + ) + 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)) - if sys.version_info < (3, 6, 0): - _LOGGER.error("You're running ESPHome with Python <3.6. ESPHome is no longer compatible " - "with this Python version. Please reinstall ESPHome with Python 3.6+") + if sys.version_info < (3, 7, 0): + _LOGGER.error( + "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.7+" + ) return 1 if args.command in PRE_CONFIG_ACTIONS: try: return PRE_CONFIG_ACTIONS[args.command](args) except EsphomeError as e: - _LOGGER.error(e) + _LOGGER.error(e, exc_info=args.verbose) return 1 for conf_path in args.configuration: + if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): + _LOGGER.warning("Skipping secrets file %s", conf_path) + continue + CORE.config_path = conf_path CORE.dashboard = args.dashboard - config = read_config() + config = read_config(dict(args.substitution) if args.substitution else {}) if config is None: - return 1 + return 2 CORE.config = config if args.command not in POST_CONFIG_ACTIONS: @@ -543,7 +811,7 @@ def run_esphome(argv): try: rc = POST_CONFIG_ACTIONS[args.command](args, config) except EsphomeError as e: - _LOGGER.error(e) + _LOGGER.error(e, exc_info=args.verbose) return 1 if rc != 0: return rc diff --git a/esphome/api/api_pb2.py b/esphome/api/api_pb2.py deleted file mode 100644 index c6c8741f01..0000000000 --- a/esphome/api/api_pb2.py +++ /dev/null @@ -1,2485 +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 fcea90e3b4..0000000000 --- a/esphome/api/client.py +++ /dev/null @@ -1,490 +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, color -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) - - _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: - raise APIConnectionError("Socket was closed") - except socket.timeout: - continue - except OSError as err: - raise APIConnectionError(f"Error while receiving data: {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('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 5df884e7c2..fab998527f 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,8 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_AUTOMATION_ID, CONF_CONDITION, CONF_ELSE, CONF_ID, CONF_THEN, \ - CONF_TRIGGER_ID, CONF_TYPE_ID, CONF_TIME -from esphome.core import coroutine +from esphome.const import ( + CONF_AUTOMATION_ID, + CONF_CONDITION, + CONF_COUNT, + CONF_ELSE, + CONF_ID, + CONF_THEN, + CONF_TIMEOUT, + CONF_TRIGGER_ID, + CONF_TYPE_ID, + CONF_TIME, +) +from esphome.jsonschema import jschema_extractor from esphome.util import Registry @@ -13,7 +23,12 @@ def maybe_simple_id(*validators): def maybe_conf(conf, *validators): validator = cv.All(*validators) + @jschema_extractor("maybe") def validate(value): + # pylint: disable=comparison-with-callable + if value == jschema_extractor: + return validator + if isinstance(value, dict): return validator(value) with cv.remove_prepend_path([conf]): @@ -30,36 +45,35 @@ def register_condition(name, condition_type, schema): return CONDITION_REGISTRY.register(name, condition_type, schema) -Action = cg.esphome_ns.class_('Action') -Trigger = cg.esphome_ns.class_('Trigger') +Action = cg.esphome_ns.class_("Action") +Trigger = cg.esphome_ns.class_("Trigger") ACTION_REGISTRY = Registry() -Condition = cg.esphome_ns.class_('Condition') +Condition = cg.esphome_ns.class_("Condition") CONDITION_REGISTRY = Registry() -validate_action = cv.validate_registry_entry('action', ACTION_REGISTRY) -validate_action_list = cv.validate_registry('action', ACTION_REGISTRY) -validate_condition = cv.validate_registry_entry('condition', CONDITION_REGISTRY) -validate_condition_list = cv.validate_registry('condition', CONDITION_REGISTRY) +validate_action = cv.validate_registry_entry("action", ACTION_REGISTRY) +validate_action_list = cv.validate_registry("action", ACTION_REGISTRY) +validate_condition = cv.validate_registry_entry("condition", CONDITION_REGISTRY) +validate_condition_list = cv.validate_registry("condition", CONDITION_REGISTRY) def validate_potentially_and_condition(value): if isinstance(value, list): - with cv.remove_prepend_path(['and']): - return validate_condition({ - 'and': value - }) + with cv.remove_prepend_path(["and"]): + return validate_condition({"and": value}) return validate_condition(value) -DelayAction = cg.esphome_ns.class_('DelayAction', Action, cg.Component) -LambdaAction = cg.esphome_ns.class_('LambdaAction', Action) -IfAction = cg.esphome_ns.class_('IfAction', Action) -WhileAction = cg.esphome_ns.class_('WhileAction', Action) -WaitUntilAction = cg.esphome_ns.class_('WaitUntilAction', Action, cg.Component) -UpdateComponentAction = cg.esphome_ns.class_('UpdateComponentAction', Action) -Automation = cg.esphome_ns.class_('Automation') +DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) +LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) +IfAction = cg.esphome_ns.class_("IfAction", Action) +WhileAction = cg.esphome_ns.class_("WhileAction", Action) +RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) +WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component) +UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) +Automation = cg.esphome_ns.class_("Automation") -LambdaCondition = cg.esphome_ns.class_('LambdaCondition', Condition) -ForCondition = cg.esphome_ns.class_('ForCondition', Condition, cg.Component) +LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) +ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) def validate_automation(extra_schema=None, extra_validators=None, single=False): @@ -83,9 +97,10 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): try: return cv.Schema([schema])(value) except cv.Invalid as err2: - if 'extra keys not allowed' in str(err2) and len(err2.path) == 2: + if "extra keys not allowed" in str(err2) and len(err2.path) == 2: + # pylint: disable=raise-missing-from raise err - if 'Unable to find action' in str(err): + if "Unable to find action" in str(err): raise err2 raise cv.MultipleInvalid([err, err2]) elif isinstance(value, dict): @@ -96,7 +111,13 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): # This should only happen with invalid configs, but let's have a nice error message. return [schema(value)] + @jschema_extractor("automation") def validator(value): + # hack to get the schema + # pylint: disable=comparison-with-callable + if value == jschema_extractor: + return schema + value = validator_(value) if extra_validators is not None: value = cv.Schema([extra_validators])(value) @@ -109,162 +130,223 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): return validator -AUTOMATION_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger), - cv.GenerateID(CONF_AUTOMATION_ID): cv.declare_id(Automation), - cv.Required(CONF_THEN): validate_action_list, -}) +AUTOMATION_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger), + cv.GenerateID(CONF_AUTOMATION_ID): cv.declare_id(Automation), + cv.Required(CONF_THEN): validate_action_list, + } +) -AndCondition = cg.esphome_ns.class_('AndCondition', Condition) -OrCondition = cg.esphome_ns.class_('OrCondition', Condition) -NotCondition = cg.esphome_ns.class_('NotCondition', Condition) +AndCondition = cg.esphome_ns.class_("AndCondition", Condition) +OrCondition = cg.esphome_ns.class_("OrCondition", Condition) +NotCondition = cg.esphome_ns.class_("NotCondition", Condition) -@register_condition('and', AndCondition, validate_condition_list) -def and_condition_to_code(config, condition_id, template_arg, args): - conditions = yield build_condition_list(config, template_arg, args) - yield cg.new_Pvariable(condition_id, template_arg, conditions) +@register_condition("and", AndCondition, validate_condition_list) +async def and_condition_to_code(config, condition_id, template_arg, args): + conditions = await build_condition_list(config, template_arg, args) + return cg.new_Pvariable(condition_id, template_arg, conditions) -@register_condition('or', OrCondition, validate_condition_list) -def or_condition_to_code(config, condition_id, template_arg, args): - conditions = yield build_condition_list(config, template_arg, args) - yield cg.new_Pvariable(condition_id, template_arg, conditions) +@register_condition("or", OrCondition, validate_condition_list) +async def or_condition_to_code(config, condition_id, template_arg, args): + conditions = await build_condition_list(config, template_arg, args) + return cg.new_Pvariable(condition_id, template_arg, conditions) -@register_condition('not', NotCondition, validate_potentially_and_condition) -def not_condition_to_code(config, condition_id, template_arg, args): - condition = yield build_condition(config, template_arg, args) - yield cg.new_Pvariable(condition_id, template_arg, condition) +@register_condition("not", NotCondition, validate_potentially_and_condition) +async def not_condition_to_code(config, condition_id, template_arg, args): + condition = await build_condition(config, template_arg, args) + return cg.new_Pvariable(condition_id, template_arg, condition) -@register_condition('lambda', LambdaCondition, cv.lambda_) -def lambda_condition_to_code(config, condition_id, template_arg, args): - lambda_ = yield cg.process_lambda(config, args, return_type=bool) - yield cg.new_Pvariable(condition_id, template_arg, lambda_) +@register_condition("lambda", LambdaCondition, cv.returning_lambda) +async def lambda_condition_to_code(config, condition_id, template_arg, args): + lambda_ = await cg.process_lambda(config, args, return_type=bool) + return cg.new_Pvariable(condition_id, template_arg, lambda_) -@register_condition('for', ForCondition, cv.Schema({ - cv.Required(CONF_TIME): cv.templatable(cv.positive_time_period_milliseconds), - cv.Required(CONF_CONDITION): validate_potentially_and_condition, -}).extend(cv.COMPONENT_SCHEMA)) -def for_condition_to_code(config, condition_id, template_arg, args): - condition = yield build_condition(config[CONF_CONDITION], cg.TemplateArguments(), []) +@register_condition( + "for", + ForCondition, + cv.Schema( + { + cv.Required(CONF_TIME): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Required(CONF_CONDITION): validate_potentially_and_condition, + } + ).extend(cv.COMPONENT_SCHEMA), +) +async def for_condition_to_code(config, condition_id, template_arg, args): + condition = await build_condition( + config[CONF_CONDITION], cg.TemplateArguments(), [] + ) var = cg.new_Pvariable(condition_id, template_arg, condition) - yield cg.register_component(var, config) - templ = yield cg.templatable(config[CONF_TIME], args, cg.uint32) + await cg.register_component(var, config) + templ = await cg.templatable(config[CONF_TIME], args, cg.uint32) cg.add(var.set_time(templ)) - yield var + return var -@register_action('delay', DelayAction, cv.templatable(cv.positive_time_period_milliseconds)) -def delay_action_to_code(config, action_id, template_arg, args): +@register_action( + "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) +) +async def delay_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_component(var, {}) - template_ = yield cg.templatable(config, args, cg.uint32) + await cg.register_component(var, {}) + template_ = await cg.templatable(config, args, cg.uint32) cg.add(var.set_delay(template_)) - yield var + return var -@register_action('if', IfAction, cv.All({ - cv.Required(CONF_CONDITION): validate_potentially_and_condition, - cv.Optional(CONF_THEN): validate_action_list, - cv.Optional(CONF_ELSE): validate_action_list, -}, cv.has_at_least_one_key(CONF_THEN, CONF_ELSE))) -def if_action_to_code(config, action_id, template_arg, args): - conditions = yield build_condition(config[CONF_CONDITION], template_arg, args) +@register_action( + "if", + IfAction, + cv.All( + { + cv.Required(CONF_CONDITION): validate_potentially_and_condition, + cv.Optional(CONF_THEN): validate_action_list, + cv.Optional(CONF_ELSE): validate_action_list, + }, + cv.has_at_least_one_key(CONF_THEN, CONF_ELSE), + ), +) +async def if_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_THEN in config: - actions = yield build_action_list(config[CONF_THEN], template_arg, args) + actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) if CONF_ELSE in config: - actions = yield build_action_list(config[CONF_ELSE], template_arg, args) + actions = await build_action_list(config[CONF_ELSE], template_arg, args) cg.add(var.add_else(actions)) - yield var + return var -@register_action('while', WhileAction, cv.Schema({ - cv.Required(CONF_CONDITION): validate_potentially_and_condition, - cv.Required(CONF_THEN): validate_action_list, -})) -def while_action_to_code(config, action_id, template_arg, args): - conditions = yield build_condition(config[CONF_CONDITION], template_arg, args) +@register_action( + "while", + WhileAction, + cv.Schema( + { + cv.Required(CONF_CONDITION): validate_potentially_and_condition, + cv.Required(CONF_THEN): validate_action_list, + } + ), +) +async def while_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) - actions = yield build_action_list(config[CONF_THEN], template_arg, args) + actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) - yield var + return var + + +@register_action( + "repeat", + RepeatAction, + cv.Schema( + { + cv.Required(CONF_COUNT): cv.templatable(cv.positive_not_null_int), + cv.Required(CONF_THEN): validate_action_list, + } + ), +) +async def repeat_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) + cg.add(var.set_count(count_template)) + actions = await build_action_list(config[CONF_THEN], template_arg, args) + cg.add(var.add_then(actions)) + return var def validate_wait_until(value): - schema = cv.Schema({ - cv.Required(CONF_CONDITION): validate_potentially_and_condition, - }) + 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: return schema(value) return validate_wait_until({CONF_CONDITION: value}) -@register_action('wait_until', WaitUntilAction, validate_wait_until) -def wait_until_action_to_code(config, action_id, template_arg, args): - conditions = yield build_condition(config[CONF_CONDITION], template_arg, args) +@register_action("wait_until", WaitUntilAction, validate_wait_until) +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) - yield cg.register_component(var, {}) - yield var + 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 -@register_action('lambda', LambdaAction, cv.lambda_) -def lambda_action_to_code(config, action_id, template_arg, args): - lambda_ = yield cg.process_lambda(config, args, return_type=cg.void) - yield cg.new_Pvariable(action_id, template_arg, lambda_) +@register_action("lambda", LambdaAction, cv.lambda_) +async def lambda_action_to_code(config, action_id, template_arg, args): + lambda_ = await cg.process_lambda(config, args, return_type=cg.void) + return cg.new_Pvariable(action_id, template_arg, lambda_) -@register_action('component.update', UpdateComponentAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), -})) -def component_update_action_to_code(config, action_id, template_arg, args): - comp = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, comp) +@register_action( + "component.update", + UpdateComponentAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), + } + ), +) +async def component_update_action_to_code(config, action_id, template_arg, args): + comp = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, comp) -@coroutine -def build_action(full_config, template_arg, args): - registry_entry, config = cg.extract_registry_entry_config(ACTION_REGISTRY, full_config) +async def build_action(full_config, template_arg, args): + registry_entry, config = cg.extract_registry_entry_config( + ACTION_REGISTRY, full_config + ) action_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun - yield builder(config, action_id, template_arg, args) + ret = await builder(config, action_id, template_arg, args) + return ret -@coroutine -def build_action_list(config, templ, arg_type): +async def build_action_list(config, templ, arg_type): actions = [] for conf in config: - action = yield build_action(conf, templ, arg_type) + action = await build_action(conf, templ, arg_type) actions.append(action) - yield actions + return actions -@coroutine -def build_condition(full_config, template_arg, args): - registry_entry, config = cg.extract_registry_entry_config(CONDITION_REGISTRY, full_config) +async def build_condition(full_config, template_arg, args): + registry_entry, config = cg.extract_registry_entry_config( + CONDITION_REGISTRY, full_config + ) action_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun - yield builder(config, action_id, template_arg, args) + ret = await builder(config, action_id, template_arg, args) + return ret -@coroutine -def build_condition_list(config, templ, args): +async def build_condition_list(config, templ, args): conditions = [] for conf in config: - condition = yield build_condition(conf, templ, args) + condition = await build_condition(conf, templ, args) conditions.append(condition) - yield conditions + return conditions -@coroutine -def build_automation(trigger, args, config): +async def build_automation(trigger, args, config): arg_types = [arg[0] for arg in args] templ = cg.TemplateArguments(*arg_types) obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger) - actions = yield build_action_list(config[CONF_THEN], templ, args) + actions = await build_action_list(config[CONF_THEN], templ, args) cg.add(obj.add_actions(actions)) - yield obj + return obj diff --git a/esphome/codegen.py b/esphome/codegen.py index c30b43f952..5e1e934e58 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -9,18 +9,77 @@ # pylint: disable=unused-import from esphome.cpp_generator import ( # noqa - Expression, RawExpression, RawStatement, TemplateArguments, - StructInitializer, ArrayInitializer, safe_exp, Statement, LineComment, - progmem_array, statement, variable, Pvariable, new_Pvariable, - add, add_global, add_library, add_build_flag, add_define, - get_variable, get_variable_with_full_id, process_lambda, is_template, templatable, MockObj, - MockObjClass) + Expression, + RawExpression, + RawStatement, + TemplateArguments, + StructInitializer, + ArrayInitializer, + safe_exp, + Statement, + LineComment, + progmem_array, + static_const_array, + statement, + variable, + new_variable, + Pvariable, + new_Pvariable, + add, + add_global, + add_library, + add_build_flag, + add_define, + add_platformio_option, + get_variable, + get_variable_with_full_id, + process_lambda, + is_template, + templatable, + MockObj, + MockObjClass, +) from esphome.cpp_helpers import ( # noqa - gpio_pin_expression, register_component, build_registry_entry, - build_registry_list, extract_registry_entry_config, register_parented) + gpio_pin_expression, + register_component, + build_registry_entry, + build_registry_list, + extract_registry_entry_config, + register_parented, +) from esphome.cpp_types import ( # noqa - global_ns, void, nullptr, float_, double, bool_, int_, std_ns, std_string, - std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN, - esphome_ns, App, Nameable, Component, ComponentPtr, - PollingComponent, Application, optional, arduino_json_ns, JsonObject, - JsonObjectRef, JsonObjectConstRef, Controller, GPIOPin) + global_ns, + void, + nullptr, + float_, + double, + bool_, + int_, + std_ns, + std_string, + std_vector, + uint8, + uint16, + uint32, + uint64, + int32, + const_char_ptr, + NAN, + esphome_ns, + App, + EntityBase, + Component, + ComponentPtr, + PollingComponent, + Application, + optional, + arduino_json_ns, + JsonObject, + JsonObjectRef, + JsonObjectConstRef, + Controller, + GPIOPin, + InternalGPIOPin, + gpio_Flags, + EntityCategory, +) diff --git a/esphome/components/a4988/a4988.cpp b/esphome/components/a4988/a4988.cpp index 99b677a9ab..429fa25648 100644 --- a/esphome/components/a4988/a4988.cpp +++ b/esphome/components/a4988/a4988.cpp @@ -4,13 +4,14 @@ namespace esphome { namespace a4988 { -static const char *TAG = "a4988.stepper"; +static const char *const TAG = "a4988.stepper"; void A4988::setup() { ESP_LOGCONFIG(TAG, "Setting up A4988..."); if (this->sleep_pin_ != nullptr) { this->sleep_pin_->setup(); this->sleep_pin_->digital_write(false); + this->sleep_pin_state_ = false; } this->step_pin_->setup(); this->step_pin_->digital_write(false); @@ -27,7 +28,12 @@ void A4988::dump_config() { void A4988::loop() { bool at_target = this->has_reached_target(); if (this->sleep_pin_ != nullptr) { + bool sleep_rising_edge = !sleep_pin_state_ & !at_target; this->sleep_pin_->digital_write(!at_target); + this->sleep_pin_state_ = !at_target; + if (sleep_rising_edge) { + delayMicroseconds(1000); + } } if (at_target) { this->high_freq_.stop(); diff --git a/esphome/components/a4988/a4988.h b/esphome/components/a4988/a4988.h index 10fb5e0015..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 { @@ -21,6 +21,7 @@ class A4988 : public stepper::Stepper, public Component { GPIOPin *step_pin_; GPIOPin *dir_pin_; GPIOPin *sleep_pin_{nullptr}; + bool sleep_pin_state_; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/a4988/stepper.py b/esphome/components/a4988/stepper.py index 29696dbd5e..7f53856c7b 100644 --- a/esphome/components/a4988/stepper.py +++ b/esphome/components/a4988/stepper.py @@ -5,27 +5,29 @@ import esphome.codegen as cg from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN -a4988_ns = cg.esphome_ns.namespace('a4988') -A4988 = a4988_ns.class_('A4988', stepper.Stepper, cg.Component) +a4988_ns = cg.esphome_ns.namespace("a4988") +A4988 = a4988_ns.class_("A4988", stepper.Stepper, cg.Component) -CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(A4988), - cv.Required(CONF_STEP_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_DIR_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_SLEEP_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(A4988), + cv.Required(CONF_STEP_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DIR_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SLEEP_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield stepper.register_stepper(var, config) + await cg.register_component(var, config) + await stepper.register_stepper(var, config) - step_pin = yield cg.gpio_pin_expression(config[CONF_STEP_PIN]) + step_pin = await cg.gpio_pin_expression(config[CONF_STEP_PIN]) cg.add(var.set_step_pin(step_pin)) - dir_pin = yield cg.gpio_pin_expression(config[CONF_DIR_PIN]) + dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN]) cg.add(var.set_dir_pin(dir_pin)) if CONF_SLEEP_PIN in config: - sleep_pin = yield cg.gpio_pin_expression(config[CONF_SLEEP_PIN]) + sleep_pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN]) cg.add(var.set_sleep_pin(sleep_pin)) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index a60cc9e29a..a7f1e6f3a9 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -1,28 +1,37 @@ +#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 { -static const char *TAG = "ac_dimmer"; +static const char *const TAG = "ac_dimmer"; // Global array to store dimmer objects -static AcDimmerDataStore *all_dimmers[32]; +static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) /// 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 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 16f04ac984..c39fc382b6 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -4,40 +4,49 @@ from esphome import pins from esphome.components import output from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD -ac_dimmer_ns = cg.esphome_ns.namespace('ac_dimmer') -AcDimmer = ac_dimmer_ns.class_('AcDimmer', output.FloatOutput, cg.Component) +CODEOWNERS = ["@glmnet"] -DimMethod = ac_dimmer_ns.enum('DimMethod') +ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer") +AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component) + +DimMethod = ac_dimmer_ns.enum("DimMethod") DIM_METHODS = { - 'LEADING_PULSE': DimMethod.DIM_METHOD_LEADING_PULSE, - 'LEADING': DimMethod.DIM_METHOD_LEADING, - 'TRAILING': DimMethod.DIM_METHOD_TRAILING, + "LEADING_PULSE": DimMethod.DIM_METHOD_LEADING_PULSE, + "LEADING": DimMethod.DIM_METHOD_LEADING, + "TRAILING": DimMethod.DIM_METHOD_TRAILING, } -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) +CONF_GATE_PIN = "gate_pin" +CONF_ZERO_CROSS_PIN = "zero_cross_pin" +CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle" +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, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) # override default min power to 10% if CONF_MIN_POWER not in config: config[CONF_MIN_POWER] = 0.1 - yield output.register_output(var, config) + await output.register_output(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_GATE_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_GATE_PIN]) cg.add(var.set_gate_pin(pin)) - pin = yield cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN]) cg.add(var.set_zero_cross_pin(pin)) cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE])) cg.add(var.set_method(config[CONF_METHOD])) diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py new file mode 100644 index 0000000000..919ffecbea --- /dev/null +++ b/esphome/components/adalight/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_UART_ID + +DEPENDENCIES = ["uart"] + +adalight_ns = cg.esphome_ns.namespace("adalight") +AdalightLightEffect = adalight_ns.class_( + "AdalightLightEffect", uart.UARTDevice, AddressableLightEffect +) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect( + "adalight", + AdalightLightEffect, + "Adalight", + {cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)}, +) +async def adalight_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + await uart.register_uart_device(effect, config) + return effect diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp new file mode 100644 index 0000000000..35e98d7360 --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -0,0 +1,142 @@ +#include "adalight_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adalight { + +static const char *const TAG = "adalight_light_effect"; + +static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; +static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; + +AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void AdalightLightEffect::start() { + AddressableLightEffect::start(); + + last_ack_ = 0; + last_byte_ = 0; + last_reset_ = 0; +} + +void AdalightLightEffect::stop() { + frame_.resize(0); + + AddressableLightEffect::stop(); +} + +unsigned int AdalightLightEffect::get_frame_size_(int led_count) const { + // 3 bytes: Ada + // 2 bytes: LED count + // 1 byte: checksum + // 3 bytes per LED + return 3 + 2 + 1 + led_count * 3; +} + +void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { + int buffer_capacity = get_frame_size_(it.size()); + + frame_.clear(); + frame_.reserve(buffer_capacity); +} + +void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(Color::BLACK); + } + it.schedule_show(); +} + +void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { + const uint32_t now = millis(); + + if (now - this->last_ack_ >= ADALIGHT_ACK_INTERVAL) { + ESP_LOGV(TAG, "Sending ACK"); + this->write_str("Ada\n"); + this->last_ack_ = now; + } + + if (!this->last_reset_) { + ESP_LOGW(TAG, "Frame: Reset."); + reset_frame_(it); + blank_all_leds_(it); + this->last_reset_ = now; + } + + if (!this->frame_.empty() && now - this->last_byte_ >= ADALIGHT_RECEIVE_TIMEOUT) { + ESP_LOGW(TAG, "Frame: Receive timeout (size=%zu).", this->frame_.size()); + reset_frame_(it); + blank_all_leds_(it); + } + + if (this->available() > 0) { + ESP_LOGV(TAG, "Frame: Available (size=%d).", this->available()); + } + + while (this->available() != 0) { + uint8_t data; + if (!this->read_byte(&data)) + break; + this->frame_.push_back(data); + this->last_byte_ = now; + + switch (this->parse_frame_(it)) { + case INVALID: + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]); + reset_frame_(it); + break; + + case PARTIAL: + break; + + case CONSUMED: + ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size()); + reset_frame_(it); + break; + } + } +} + +AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableLight &it) { + if (frame_.empty()) + return INVALID; + + // Check header: `Ada` + if (frame_[0] != 'A') + return INVALID; + if (frame_.size() > 1 && frame_[1] != 'd') + return INVALID; + if (frame_.size() > 2 && frame_[2] != 'a') + return INVALID; + + // 3 bytes: Count Hi, Count Lo, Checksum + if (frame_.size() < 6) + return PARTIAL; + + // Check checksum + uint16_t checksum = frame_[3] ^ frame_[4] ^ 0x55; + if (checksum != frame_[5]) + return INVALID; + + // Check if we received the full frame + uint16_t led_count = (frame_[3] << 8) + frame_[4] + 1; + auto buffer_size = get_frame_size_(led_count); + if (frame_.size() < buffer_size) + return PARTIAL; + + // Apply lights + auto accepted_led_count = std::min(led_count, it.size()); + uint8_t *led_data = &frame_[6]; + + for (int led = 0; led < accepted_led_count; led++, led_data += 3) { + auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]); + + it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); + } + + it.schedule_show(); + return CONSUMED; +} + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h new file mode 100644 index 0000000000..b757191864 --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace adalight { + +class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { + public: + AdalightLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const Color ¤t_color) override; + + protected: + enum Frame { + INVALID, + PARTIAL, + CONSUMED, + }; + + unsigned int get_frame_size_(int led_count) const; + void reset_frame_(light::AddressableLight &it); + void blank_all_leds_(light::AddressableLight &it); + Frame parse_frame_(light::AddressableLight &it); + + protected: + uint32_t last_ack_{0}; + uint32_t last_byte_{0}; + uint32_t last_reset_{0}; + std::vector frame_; +}; + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index e69de29bb2..f70ffa9520 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 2c448d0392..0a439f8b8d 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -1,90 +1,168 @@ #include "adc_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC +#include ADC_MODE(ADC_VCC) +#else +#include +#endif #endif namespace esphome { namespace adc { -static const char *TAG = "adc"; - -#ifdef ARDUINO_ARCH_ESP32 -void ADCSensor::set_attenuation(adc_attenuation_t attenuation) { this->attenuation_ = attenuation; } -#endif +static const char *const TAG = "adc"; void ADCSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); - GPIOPin(this->pin_, INPUT).setup(); - -#ifdef ARDUINO_ARCH_ESP32 - analogSetPinAttenuation(this->pin_, this->attenuation_); +#ifndef USE_ADC_SENSOR_VCC + pin_->setup(); #endif + +#ifdef USE_ESP32 + adc1_config_width(ADC_WIDTH_BIT_12); + if (!autorange_) { + adc1_config_channel_atten(channel_, attenuation_); + } + + // load characteristics for each attenuation + for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { + auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_BIT_12, + 1100, // default vref + &cal_characteristics_[i]); + switch (cal_value) { + case ESP_ADC_CAL_VAL_EFUSE_VREF: + ESP_LOGV(TAG, "Using eFuse Vref for calibration"); + break; + case ESP_ADC_CAL_VAL_EFUSE_TP: + ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); + break; + case ESP_ADC_CAL_VAL_DEFAULT_VREF: + default: + break; + } + } + + // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) + adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); +#endif +#endif // USE_ESP32 } + 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_); -#endif -#endif -#ifdef ARDUINO_ARCH_ESP32 - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); - switch (this->attenuation_) { - case ADC_0db: - ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); - break; - case ADC_2_5db: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); - break; - case ADC_6db: - ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); - break; - case ADC_11db: - ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); - break; - } + LOG_PIN(" Pin: ", pin_); #endif +#endif // USE_ESP8266 + +#ifdef USE_ESP32 + LOG_PIN(" Pin: ", pin_); + if (autorange_) + ESP_LOGCONFIG(TAG, " Attenuation: auto"); + else + switch (this->attenuation_) { + case ADC_ATTEN_DB_0: + ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); + break; + case ADC_ATTEN_DB_2_5: + ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); + break; + case ADC_ATTEN_DB_6: + ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); + break; + 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 // USE_ESP32 LOG_UPDATE_INTERVAL(this); } + float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGD(TAG, "'%s': Got voltage=%.2fV", this->get_name().c_str(), value_v); + ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } + +#ifdef USE_ESP8266 float ADCSensor::sample() { -#ifdef ARDUINO_ARCH_ESP32 - float value_v = analogRead(this->pin_) / 4095.0f; - switch (this->attenuation_) { - case ADC_0db: - value_v *= 1.1; - break; - case ADC_2_5db: - value_v *= 1.5; - break; - case ADC_6db: - value_v *= 2.2; - break; - case ADC_11db: - value_v *= 3.9; - break; +#ifdef USE_ADC_SENSOR_VCC + int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) +#else + int raw = analogRead(this->pin_->get_pin()); // NOLINT +#endif + if (output_raw_) { + return raw; } - return value_v; + return raw / 1024.0f; +} #endif -#ifdef ARDUINO_ARCH_ESP8266 -#ifdef USE_ADC_SENSOR_VCC - return ESP.getVcc() / 1024.0f; -#else - return analogRead(this->pin_) / 1024.0f; -#endif -#endif +#ifdef USE_ESP32 +float ADCSensor::sample() { + if (!autorange_) { + int raw = adc1_get_raw(channel_); + if (raw == -1) { + return NAN; + } + if (output_raw_) { + return raw; + } + uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); + return mv / 1000.0f; + } + + int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095; + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); + raw11 = adc1_get_raw(channel_); + if (raw11 < 4095) { + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); + raw6 = adc1_get_raw(channel_); + if (raw6 < 4095) { + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); + raw2 = adc1_get_raw(channel_); + if (raw2 < 4095) { + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); + raw0 = adc1_get_raw(channel_); + } + } + } + + if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw11 == -1) { + return NAN; + } + + uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]); + uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]); + uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); + uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); + + // Contribution of each value, in range 0-2048 + uint32_t c11 = std::min(raw11, 2048); + uint32_t c6 = 2048 - std::abs(raw6 - 2048); + uint32_t c2 = 2048 - std::abs(raw2 - 2048); + uint32_t c0 = std::min(4095 - raw0, 2048); + // max theoretical csum value is 2048*4 = 8192 + uint32_t csum = c11 + c6 + c2 + c0; + + // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned + uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); + return mv_scaled / (float) (csum * 1000U); } -#ifdef ARDUINO_ARCH_ESP8266 +#endif // USE_ESP32 + +#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..12272a1577 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -1,19 +1,26 @@ #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" +#include +#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) { attenuation_ = attenuation; } + void set_channel(adc1_channel_t channel) { channel_ = channel; } + void set_autorange(bool autorange) { autorange_ = autorange; } #endif /// Update adc values. @@ -23,18 +30,23 @@ 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; } + void set_output_raw(bool output_raw) { output_raw_ = output_raw; } float sample() override; -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 std::string unique_id() override; #endif protected: - uint8_t pin_; + InternalGPIOPin *pin_; + bool output_raw_{false}; -#ifdef ARDUINO_ARCH_ESP32 - adc_attenuation_t attenuation_{ADC_0db}; +#ifdef USE_ESP32 + adc_atten_t attenuation_{ADC_ATTEN_DB_0}; + adc1_channel_t channel_{}; + bool autorange_{false}; + esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 6a274f04af..c812e67a68 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,47 +2,179 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor, voltage_sampler -from esphome.const import CONF_ATTENUATION, CONF_ID, CONF_PIN, ICON_FLASH, UNIT_VOLT +from esphome.const import ( + CONF_ATTENUATION, + CONF_RAW, + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_PIN, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) +from esphome.core import CORE +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) -AUTO_LOAD = ['voltage_sampler'] +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, + "auto": "auto", +} + +adc1_channel_t = cg.global_ns.enum("adc1_channel_t") + +# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h +# pin to adc1 channel mapping +ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { + VARIANT_ESP32: { + 36: adc1_channel_t.ADC1_CHANNEL_0, + 37: adc1_channel_t.ADC1_CHANNEL_1, + 38: adc1_channel_t.ADC1_CHANNEL_2, + 39: adc1_channel_t.ADC1_CHANNEL_3, + 32: adc1_channel_t.ADC1_CHANNEL_4, + 33: adc1_channel_t.ADC1_CHANNEL_5, + 34: adc1_channel_t.ADC1_CHANNEL_6, + 35: adc1_channel_t.ADC1_CHANNEL_7, + }, + VARIANT_ESP32S2: { + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, + 6: adc1_channel_t.ADC1_CHANNEL_5, + 7: adc1_channel_t.ADC1_CHANNEL_6, + 8: adc1_channel_t.ADC1_CHANNEL_7, + 9: adc1_channel_t.ADC1_CHANNEL_8, + 10: adc1_channel_t.ADC1_CHANNEL_9, + }, + VARIANT_ESP32S3: { + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, + 6: adc1_channel_t.ADC1_CHANNEL_5, + 7: adc1_channel_t.ADC1_CHANNEL_6, + 8: adc1_channel_t.ADC1_CHANNEL_7, + 9: adc1_channel_t.ADC1_CHANNEL_8, + 10: adc1_channel_t.ADC1_CHANNEL_9, + }, + VARIANT_ESP32C3: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, + VARIANT_ESP32H2: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, } 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: + value = pins.internal_gpio_input_pin_number(value) + variant = get_esp32_variant() + if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: + raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") + + if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: + raise cv.Invalid(f"{variant} doesn't support ADC on this pin") + return pins.internal_gpio_input_pin_schema(value) + + if 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) + + raise NotImplementedError -adc_ns = cg.esphome_ns.namespace('adc') -ADCSensor = adc_ns.class_('ADCSensor', sensor.Sensor, cg.PollingComponent, - voltage_sampler.VoltageSampler) - -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2).extend({ - cv.GenerateID(): cv.declare_id(ADCSensor), - cv.Required(CONF_PIN): validate_adc_pin, - cv.SplitDefault(CONF_ATTENUATION, esp32='0db'): - cv.All(cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True)), -}).extend(cv.polling_component_schema('60s')) +def validate_config(config): + if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": + raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.") + return config -def to_code(config): +adc_ns = cg.esphome_ns.namespace("adc") +ADCSensor = adc_ns.class_( + "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(ADCSensor), + cv.Required(CONF_PIN): validate_adc_pin, + cv.Optional(CONF_RAW, default=False): cv.boolean, + cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( + cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True) + ), + } + ) + .extend(cv.polling_component_schema("60s")), + validate_config, +) + + +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - if config[CONF_PIN] == 'VCC': - cg.add_define('USE_ADC_SENSOR_VCC') + 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_RAW in config: + cg.add(var.set_output_raw(config[CONF_RAW])) if CONF_ATTENUATION in config: - cg.add(var.set_attenuation(config[CONF_ATTENUATION])) + if config[CONF_ATTENUATION] == "auto": + cg.add(var.set_autorange(cg.global_ns.true)) + else: + cg.add(var.set_attenuation(config[CONF_ATTENUATION])) + + if CORE.is_esp32: + variant = get_esp32_variant() + pin_num = config[CONF_PIN][CONF_NUMBER] + chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel(chan)) diff --git a/esphome/api/__init__.py b/esphome/components/addressable_light/__init__.py similarity index 100% rename from esphome/api/__init__.py rename to esphome/components/addressable_light/__init__.py diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp new file mode 100644 index 0000000000..16fab15b17 --- /dev/null +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -0,0 +1,67 @@ +#include "addressable_light_display.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace addressable_light { + +static const char *const TAG = "addressable_light.display"; + +int AddressableLightDisplay::get_width_internal() { return this->width_; } +int AddressableLightDisplay::get_height_internal() { return this->height_; } + +void AddressableLightDisplay::setup() { + this->addressable_light_buffer_.resize(this->width_ * this->height_, {0, 0, 0, 0}); +} + +void AddressableLightDisplay::update() { + if (!this->enabled_) + return; + + this->do_update_(); + this->display(); +} + +void AddressableLightDisplay::display() { + bool dirty = false; + uint8_t old_r, old_g, old_b, old_w; + Color *c; + + for (uint32_t offset = 0; offset < this->addressable_light_buffer_.size(); offset++) { + c = &(this->addressable_light_buffer_[offset]); + + light::ESPColorView pixel = (*this->light_)[offset]; + + // Track the original values for the pixel view. If it has changed updating, then + // we trigger a redraw. Avoiding redraws == avoiding flicker! + old_r = pixel.get_red(); + old_g = pixel.get_green(); + old_b = pixel.get_blue(); + old_w = pixel.get_white(); + + pixel.set_rgbw(c->r, c->g, c->b, c->w); + + // If the actual value of the pixel changed, then schedule a redraw. + if (pixel.get_red() != old_r || pixel.get_green() != old_g || pixel.get_blue() != old_b || + pixel.get_white() != old_w) { + dirty = true; + } + } + + if (dirty) { + this->light_->schedule_show(); + } +} + +void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + if (this->pixel_mapper_f_.has_value()) { + // Params are passed by reference, so they may be modified in call. + this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; + } else { + this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; + } +} +} // namespace addressable_light +} // namespace esphome diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h new file mode 100644 index 0000000000..163faf27b0 --- /dev/null +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -0,0 +1,59 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/light/addressable_light.h" + +namespace esphome { +namespace addressable_light { + +class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { + public: + light::AddressableLight *get_light() const { return this->light_; } + + void set_width(int32_t width) { width_ = width; } + void set_height(int32_t height) { height_ = height; } + void set_light(light::LightState *state) { + light_state_ = state; + light_ = static_cast(state->get_output()); + } + void set_enabled(bool enabled) { + if (light_state_) { + if (enabled_ && !enabled) { // enabled -> disabled + // - Tell the parent light to refresh, effectively wiping the display. Also + // restores the previous effect (if any). + light_state_->make_call().set_effect(this->last_effect_).perform(); + + } else if (!enabled_ && enabled) { // disabled -> enabled + // - Save the current effect. + this->last_effect_ = light_state_->get_effect_name(); + // - Disable any current effect. + light_state_->make_call().set_effect(0).perform(); + } + } + enabled_ = enabled; + } + bool get_enabled() { return enabled_; } + + void set_pixel_mapper(std::function &&pixel_mapper_f) { this->pixel_mapper_f_ = pixel_mapper_f; } + void setup() override; + void display(); + + protected: + int get_width_internal() override; + int get_height_internal() override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void update() override; + + light::LightState *light_state_; + light::AddressableLight *light_; + bool enabled_{true}; + int32_t width_; + int32_t height_; + std::vector addressable_light_buffer_; + optional last_effect_; + optional> pixel_mapper_f_; +}; +} // namespace addressable_light +} // namespace esphome diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py new file mode 100644 index 0000000000..0684bf8dfc --- /dev/null +++ b/esphome/components/addressable_light/display.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, light +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_PAGES, + CONF_ADDRESSABLE_LIGHT_ID, + CONF_HEIGHT, + CONF_WIDTH, + CONF_UPDATE_INTERVAL, + CONF_PIXEL_MAPPER, +) + +CODEOWNERS = ["@justfalter"] + +addressable_light_ns = cg.esphome_ns.namespace("addressable_light") +AddressableLightDisplay = addressable_light_ns.class_( + "AddressableLightDisplay", display.DisplayBuffer, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AddressableLightDisplay), + cv.Required(CONF_ADDRESSABLE_LIGHT_ID): cv.use_id( + light.AddressableLightState + ), + cv.Required(CONF_WIDTH): cv.positive_int, + cv.Required(CONF_HEIGHT): cv.positive_int, + cv.Optional( + CONF_UPDATE_INTERVAL, default="16ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_PIXEL_MAPPER): cv.returning_lambda, + } + ), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID]) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + cg.add(var.set_light(wrapped_light)) + + await cg.register_component(var, config) + await display.register_display(var, config) + + if CONF_PIXEL_MAPPER in config: + pixel_mapper_template_ = await cg.process_lambda( + config[CONF_PIXEL_MAPPER], + [(int, "x"), (int, "y")], + return_type=cg.int_, + ) + cg.add(var.set_pixel_mapper(pixel_mapper_template_)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp index 9316d9cad0..2c61fc6a44 100644 --- a/esphome/components/ade7953/ade7953.cpp +++ b/esphome/components/ade7953/ade7953.cpp @@ -4,10 +4,11 @@ namespace esphome { namespace ade7953 { -static const char *TAG = "ade7953"; +static const char *const TAG = "ade7953"; void ADE7953::dump_config() { ESP_LOGCONFIG(TAG, "ADE7953:"); + LOG_PIN(" IRQ Pin: ", irq_pin_); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); @@ -17,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) { \ - 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 7591bc1684..bb160cd8eb 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,6 +10,7 @@ namespace ade7953 { class ADE7953 : public i2c::I2CDevice, public PollingComponent { public: + void set_irq_pin(InternalGPIOPin *irq_pin) { irq_pin_ = irq_pin; } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } @@ -20,10 +22,13 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { } void setup() override { + 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; }); } @@ -33,28 +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]) << 16; + *value |= ((uint32_t) recv[2]) << 8; + *value |= ((uint32_t) recv[3]); + return i2c::ERROR_OK; } + InternalGPIOPin *irq_pin_ = nullptr; bool is_setup_{false}; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_a_sensor_{nullptr}; diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index b048b1ed71..d02f466091 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -1,39 +1,90 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, i2c -from esphome.const import CONF_ID, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT +from esphome import pins +from esphome.const import ( + CONF_ID, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -ace7953_ns = cg.esphome_ns.namespace('ade7953') -ADE7953 = ace7953_ns.class_('ADE7953', cg.PollingComponent, i2c.I2CDevice) +ade7953_ns = cg.esphome_ns.namespace("ade7953") +ADE7953 = ade7953_ns.class_("ADE7953", cg.PollingComponent, i2c.I2CDevice) -CONF_CURRENT_A = 'current_a' -CONF_CURRENT_B = 'current_b' -CONF_ACTIVE_POWER_A = 'active_power_a' -CONF_ACTIVE_POWER_B = 'active_power_b' +CONF_IRQ_PIN = "irq_pin" +CONF_CURRENT_A = "current_a" +CONF_CURRENT_B = "current_b" +CONF_ACTIVE_POWER_A = "active_power_a" +CONF_ACTIVE_POWER_B = "active_power_b" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(ADE7953), - - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), - cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), - cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), - cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADE7953), + cv.Optional(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_A): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_B): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) - for key in [CONF_VOLTAGE, CONF_CURRENT_A, CONF_CURRENT_B, CONF_ACTIVE_POWER_A, - CONF_ACTIVE_POWER_B]: + if CONF_IRQ_PIN in config: + irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(irq_pin)) + + for key in [ + CONF_VOLTAGE, + CONF_CURRENT_A, + CONF_CURRENT_B, + CONF_ACTIVE_POWER_A, + CONF_ACTIVE_POWER_B, + ]: if key not in config: continue conf = config[key] - sens = yield sensor.new_sensor(conf) - cg.add(getattr(var, f'set_{key}_sensor')(sens)) + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/ads1115/__init__.py b/esphome/components/ads1115/__init__.py index a41a521ba7..e8861a2f67 100644 --- a/esphome/components/ads1115/__init__.py +++ b/esphome/components/ads1115/__init__.py @@ -3,23 +3,29 @@ import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID -DEPENDENCIES = ['i2c'] -AUTO_LOAD = ['sensor', 'voltage_sampler'] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensor", "voltage_sampler"] MULTI_CONF = True -ads1115_ns = cg.esphome_ns.namespace('ads1115') -ADS1115Component = ads1115_ns.class_('ADS1115Component', cg.Component, i2c.I2CDevice) +ads1115_ns = cg.esphome_ns.namespace("ads1115") +ADS1115Component = ads1115_ns.class_("ADS1115Component", cg.Component, i2c.I2CDevice) -CONF_CONTINUOUS_MODE = 'continuous_mode' -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(ADS1115Component), - cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(None)) +CONF_CONTINUOUS_MODE = "continuous_mode" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1115Component), + cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(None)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE])) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index 0899571a47..beb379db93 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -1,10 +1,11 @@ #include "ads1115.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ads1115 { -static const char *TAG = "ads1115"; +static const char *const TAG = "ads1115"; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; @@ -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 55619b22e9..da33a39041 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -1,60 +1,79 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler -from esphome.const import CONF_GAIN, CONF_MULTIPLEXER, ICON_FLASH, UNIT_VOLT, CONF_ID +from esphome.const import ( + CONF_GAIN, + CONF_MULTIPLEXER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + CONF_ID, +) from . import ads1115_ns, ADS1115Component -DEPENDENCIES = ['ads1115'] +DEPENDENCIES = ["ads1115"] -ADS1115Multiplexer = ads1115_ns.enum('ADS1115Multiplexer') +ADS1115Multiplexer = ads1115_ns.enum("ADS1115Multiplexer") MUX = { - 'A0_A1': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1, - 'A0_A3': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3, - 'A1_A3': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3, - 'A2_A3': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3, - 'A0_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG, - 'A1_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG, - 'A2_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG, - 'A3_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG, + "A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1, + "A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3, + "A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3, + "A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3, + "A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG, + "A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG, + "A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG, + "A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG, } -ADS1115Gain = ads1115_ns.enum('ADS1115Gain') +ADS1115Gain = ads1115_ns.enum("ADS1115Gain") GAIN = { - '6.144': ADS1115Gain.ADS1115_GAIN_6P144, - '4.096': ADS1115Gain.ADS1115_GAIN_4P096, - '2.048': ADS1115Gain.ADS1115_GAIN_2P048, - '1.024': ADS1115Gain.ADS1115_GAIN_1P024, - '0.512': ADS1115Gain.ADS1115_GAIN_0P512, - '0.256': ADS1115Gain.ADS1115_GAIN_0P256, + "6.144": ADS1115Gain.ADS1115_GAIN_6P144, + "4.096": ADS1115Gain.ADS1115_GAIN_4P096, + "2.048": ADS1115Gain.ADS1115_GAIN_2P048, + "1.024": ADS1115Gain.ADS1115_GAIN_1P024, + "0.512": ADS1115Gain.ADS1115_GAIN_0P512, + "0.256": ADS1115Gain.ADS1115_GAIN_0P256, } def validate_gain(value): if isinstance(value, float): - value = f'{value:0.03f}' + value = f"{value:0.03f}" elif not isinstance(value, str): raise cv.Invalid(f'invalid gain "{value}"') return cv.enum(GAIN)(value) -ADS1115Sensor = ads1115_ns.class_('ADS1115Sensor', sensor.Sensor, cg.PollingComponent, - voltage_sampler.VoltageSampler) +ADS1115Sensor = ads1115_ns.class_( + "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) -CONF_ADS1115_ID = 'ads1115_id' -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 3).extend({ - cv.GenerateID(): cv.declare_id(ADS1115Sensor), - cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), - cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space='_'), - cv.Required(CONF_GAIN): validate_gain, -}).extend(cv.polling_component_schema('60s')) +CONF_ADS1115_ID = "ads1115_id" +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(ADS1115Sensor), + cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), + cv.Required(CONF_GAIN): validate_gain, + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): - paren = yield cg.get_variable(config[CONF_ADS1115_ID]) +async def to_code(config): + paren = await cg.get_variable(config[CONF_ADS1115_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) - yield sensor.register_sensor(var, config) - yield cg.register_component(var, config) + await sensor.register_sensor(var, config) + await cg.register_component(var, config) cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) cg.add(var.set_gain(config[CONF_GAIN])) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 6951254e0d..3c690c39b5 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -10,20 +10,21 @@ // // 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 { -static const char *TAG = "aht10"; +static const char *const TAG = "aht10"; 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,14 +67,19 @@ 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()); - if (!this->read_bytes(0, data, 6, delay)) { + delay_ms = AHT10_HUMIDITY_DELAY; + bool success = false; + for (int i = 0; i < AHT10_ATTEMPTS; ++i) { + ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis()); + delay(delay_ms); + if (this->read(data, 6) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); - } else if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy + 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) @@ -79,11 +96,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; @@ -92,19 +110,19 @@ void AHT10Component::update() { uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4; - float temperature = ((200.0 * (float) raw_temperature) / 1048576.0) - 50.0; + float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f; float humidity; if (raw_humidity == 0) { // unrealistic value humidity = NAN; } else { - humidity = (float) raw_humidity * 100.0 / 1048576.0; + humidity = (float) raw_humidity * 100.0f / 1048576.0f; } if (this->temperature_sensor_ != nullptr) { 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 71b0adce79..654d645966 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -1,30 +1,54 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -aht10_ns = cg.esphome_ns.namespace('aht10') -AHT10Component = aht10_ns.class_('AHT10Component', cg.PollingComponent, i2c.I2CDevice) +aht10_ns = cg.esphome_ns.namespace("aht10") +AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(AHT10Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 2), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AHT10Component), + 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, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) 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 59cb977fe8..c06a2a34d7 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -5,11 +5,12 @@ #include "am2320.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace am2320 { -static const char *TAG = "am2320"; +static const char *const TAG = "am2320"; // ---=== Calc CRC16 ===--- uint16_t crc_16(uint8_t *ptr, uint8_t length) { @@ -37,9 +38,9 @@ void AM2320Component::update() { return; } - float temperature = (((data[4] & 0x7F) << 8) + data[5]) / 10.0; + float temperature = (((data[4] & 0x7F) << 8) + data[5]) / 10.0f; temperature = (data[4] & 0x80) ? -temperature : temperature; - float humidity = ((data[2] << 8) + data[3]) / 10.0; + float humidity = ((data[2] << 8) + data[3]) / 10.0f; ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temperature, humidity); if (this->temperature_sensor_ != nullptr) @@ -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 d62899663c..088978a8f1 100644 --- a/esphome/components/am2320/sensor.py +++ b/esphome/components/am2320/sensor.py @@ -1,30 +1,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -am2320_ns = cg.esphome_ns.namespace('am2320') -AM2320Component = am2320_ns.class_('AM2320Component', cg.PollingComponent, i2c.I2CDevice) +am2320_ns = cg.esphome_ns.namespace("am2320") +AM2320Component = am2320_ns.class_( + "AM2320Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(AM2320Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x5C)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AM2320Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5C)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/xiaomi_miflora/__init__.py b/esphome/components/am43/__init__.py similarity index 100% rename from esphome/components/xiaomi_miflora/__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..68c85d0e9c --- /dev/null +++ b/esphome/components/am43/sensor.py @@ -0,0 +1,52 @@ +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, + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, + 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_of_measurement=UNIT_PERCENT, + device_class=DEVICE_CLASS_BATTERY, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=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 new file mode 100644 index 0000000000..7c9ff07f97 --- /dev/null +++ b/esphome/components/animation/__init__.py @@ -0,0 +1,119 @@ +import logging + +from esphome import core +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_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE +from esphome.core import CORE, HexInt + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ["display"] +MULTI_CONF = True + +Animation_ = display.display_ns.class_("Animation") + +ANIMATION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( + espImage.IMAGE_TYPE, upper=True + ), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + +CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) + +CODEOWNERS = ["@syndlex"] + + +async def to_code(config): + from PIL import Image + + path = CORE.relative_config_path(config[CONF_FILE]) + try: + image = Image.open(path) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") + + width, height = image.size + frames = image.n_frames + if CONF_RESIZE in config: + new_width_max, new_height_max = config[CONF_RESIZE] + ratio = min(new_width_max / width, new_height_max / height) + width, height = int(width * ratio), int(height * ratio) + else: + if width > 500 or height > 500: + _LOGGER.warning( + "The image you requested is very big. Please consider using" + " the resize parameter." + ) + + if config[CONF_TYPE] == "GRAYSCALE": + data = [0 for _ in range(height * width * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("L", dither=Image.NONE) + if CONF_RESIZE in config: + frame = frame.resize([width, height]) + pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + ) + for pix in pixels: + data[pos] = pix + pos += 1 + + elif config[CONF_TYPE] == "RGB24": + data = [0 for _ in range(height * width * 3 * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("RGB") + if CONF_RESIZE in config: + frame = frame.resize([width, height]) + pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + ) + for pix in pixels: + data[pos] = pix[0] + pos += 1 + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] + pos += 1 + + elif config[CONF_TYPE] == "BINARY": + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range((height * width8 // 8) * frames)] + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("1", dither=Image.NONE) + if CONF_RESIZE in config: + frame = frame.resize([width, height]) + for y in range(height): + for x in range(width): + if frame.getpixel((x, y)): + continue + pos = x + y * width8 + (height * width8 * frameIndex) + data[pos // 8] |= 0x80 >> (pos % 8) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.new_Pvariable( + config[CONF_ID], + prog_arr, + width, + height, + frames, + espImage.IMAGE_TYPE[config[CONF_TYPE]], + ) diff --git a/esphome/components/xiaomi_mijia/__init__.py b/esphome/components/anova/__init__.py similarity index 100% rename from esphome/components/xiaomi_mijia/__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..4f8f0d0ee2 --- /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_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT}); + 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..ce4febbe37 --- /dev/null +++ b/esphome/components/anova/anova_base.cpp @@ -0,0 +1,134 @@ +#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) { + char buf[32]; + memset(buf, 0, sizeof(buf)); + strncpy(buf, (char *) data, std::min(length, sizeof(buf) - 1)); + 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(buf, "stopped", 7)) { + this->has_running_ = true; + this->running_ = false; + } + if (!strncmp(buf, "running", 7)) { + this->has_running_ = true; + this->running_ = true; + } + break; + } + case START: { + if (!strncmp(buf, "start", 5)) { + this->has_running_ = true; + this->running_ = true; + } + break; + } + case STOP: { + if (!strncmp(buf, "stop", 4)) { + this->has_running_ = true; + this->running_ = false; + } + break; + } + case READ_TARGET_TEMPERATURE: + case SET_TARGET_TEMPERATURE: { + this->target_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); + if (this->fahrenheit_) + this->target_temp_ = ftoc(this->target_temp_); + this->has_target_temp_ = true; + break; + } + case READ_CURRENT_TEMPERATURE: { + this->current_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); + if (this->fahrenheit_) + this->current_temp_ = ftoc(this->current_temp_); + this->has_current_temp_ = true; + break; + } + case SET_UNIT: + case READ_UNIT: { + this->unit_ = buf[0]; + this->fahrenheit_ = 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..b831157849 --- /dev/null +++ b/esphome/components/anova/anova_base.h @@ -0,0 +1,79 @@ +#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_; + 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/__init__.py b/esphome/components/apds9960/__init__.py index 4725c16032..8de83251b7 100644 --- a/esphome/components/apds9960/__init__.py +++ b/esphome/components/apds9960/__init__.py @@ -3,21 +3,27 @@ import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID -DEPENDENCIES = ['i2c'] -AUTO_LOAD = ['sensor', 'binary_sensor'] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensor", "binary_sensor"] MULTI_CONF = True -CONF_APDS9960_ID = 'apds9960_id' +CONF_APDS9960_ID = "apds9960_id" -apds9960_nds = cg.esphome_ns.namespace('apds9960') -APDS9960 = apds9960_nds.class_('APDS9960', cg.PollingComponent, i2c.I2CDevice) +apds9960_nds = cg.esphome_ns.namespace("apds9960") +APDS9960 = apds9960_nds.class_("APDS9960", cg.PollingComponent, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(APDS9960), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x39)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(APDS9960), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x39)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 2e09d11182..9ee873ac64 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -1,13 +1,14 @@ #include "apds9960.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace apds9960 { -static const char *TAG = "apds9960"; +static const char *const TAG = "apds9960"; #define APDS9960_ERROR_CHECK(func) \ - if (!func) { \ + if (!(func)) { \ this->mark_failed(); \ return; \ } diff --git a/esphome/components/apds9960/binary_sensor.py b/esphome/components/apds9960/binary_sensor.py index 4404510909..4a5c69f6a9 100644 --- a/esphome/components/apds9960/binary_sensor.py +++ b/esphome/components/apds9960/binary_sensor.py @@ -4,24 +4,28 @@ from esphome.components import binary_sensor from esphome.const import CONF_DIRECTION, CONF_DEVICE_CLASS, DEVICE_CLASS_MOVING from . import APDS9960, CONF_APDS9960_ID -DEPENDENCIES = ['apds9960'] +DEPENDENCIES = ["apds9960"] DIRECTIONS = { - 'UP': 'set_up_direction', - 'DOWN': 'set_down_direction', - 'LEFT': 'set_left_direction', - 'RIGHT': 'set_right_direction', + "UP": "set_up_direction", + "DOWN": "set_down_direction", + "LEFT": "set_left_direction", + "RIGHT": "set_right_direction", } -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), - cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), - cv.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_MOVING): binary_sensor.device_class, -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), + cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), + cv.Optional( + CONF_DEVICE_CLASS, default=DEVICE_CLASS_MOVING + ): binary_sensor.device_class, + } +) -def to_code(config): - hub = yield cg.get_variable(config[CONF_APDS9960_ID]) - var = yield binary_sensor.new_binary_sensor(config) +async def to_code(config): + hub = await cg.get_variable(config[CONF_APDS9960_ID]) + var = await binary_sensor.new_binary_sensor(config) func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]]) cg.add(func(var)) diff --git a/esphome/components/apds9960/sensor.py b/esphome/components/apds9960/sensor.py index 58087cbe86..e1990ec26e 100644 --- a/esphome/components/apds9960/sensor.py +++ b/esphome/components/apds9960/sensor.py @@ -1,27 +1,39 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_TYPE, UNIT_PERCENT, ICON_LIGHTBULB +from esphome.const import ( + CONF_TYPE, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + ICON_LIGHTBULB, +) from . import APDS9960, CONF_APDS9960_ID -DEPENDENCIES = ['apds9960'] +DEPENDENCIES = ["apds9960"] TYPES = { - 'CLEAR': 'set_clear_channel', - 'RED': 'set_red_channel', - 'GREEN': 'set_green_channel', - 'BLUE': 'set_blue_channel', - 'PROXIMITY': 'set_proximity', + "CLEAR": "set_clear_channel", + "RED": "set_red_channel", + "GREEN": "set_green_channel", + "BLUE": "set_blue_channel", + "PROXIMITY": "set_proximity", } -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PERCENT, ICON_LIGHTBULB, 1).extend({ - cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), - cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), -}) +CONFIG_SCHEMA = sensor.sensor_schema( + 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), + cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), + } +) -def to_code(config): - hub = yield cg.get_variable(config[CONF_APDS9960_ID]) - var = yield sensor.new_sensor(config) +async def to_code(config): + hub = await cg.get_variable(config[CONF_APDS9960_ID]) + var = await sensor.new_sensor(config) func = getattr(hub, TYPES[config[CONF_TYPE]]) cg.add(func(var)) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index eef60602ba..6b2e7fd06b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,51 +1,100 @@ +import base64 + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition -from esphome.const import CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, CONF_PASSWORD, CONF_PORT, \ - CONF_REBOOT_TIMEOUT, CONF_SERVICE, CONF_VARIABLES, CONF_SERVICES, CONF_TRIGGER_ID, CONF_EVENT +from esphome.const import ( + CONF_DATA, + CONF_DATA_TEMPLATE, + CONF_ID, + CONF_KEY, + CONF_PASSWORD, + CONF_PORT, + CONF_REBOOT_TIMEOUT, + CONF_SERVICE, + CONF_VARIABLES, + CONF_SERVICES, + CONF_TRIGGER_ID, + CONF_EVENT, + CONF_TAG, +) from esphome.core import coroutine_with_priority -DEPENDENCIES = ['network'] -AUTO_LOAD = ['async_tcp'] +DEPENDENCIES = ["network"] +AUTO_LOAD = ["socket"] +CODEOWNERS = ["@OttoWinter"] -api_ns = cg.esphome_ns.namespace('api') -APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller) -HomeAssistantServiceCallAction = api_ns.class_('HomeAssistantServiceCallAction', automation.Action) -APIConnectedCondition = api_ns.class_('APIConnectedCondition', Condition) +api_ns = cg.esphome_ns.namespace("api") +APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) +HomeAssistantServiceCallAction = api_ns.class_( + "HomeAssistantServiceCallAction", automation.Action +) +APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) -UserServiceTrigger = api_ns.class_('UserServiceTrigger', automation.Trigger) -ListEntitiesServicesArgument = api_ns.class_('ListEntitiesServicesArgument') +UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) +ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument") SERVICE_ARG_NATIVE_TYPES = { - 'bool': bool, - 'int': cg.int32, - 'float': float, - 'string': cg.std_string, - 'bool[]': cg.std_vector.template(bool), - 'int[]': cg.std_vector.template(cg.int32), - 'float[]': cg.std_vector.template(float), - 'string[]': cg.std_vector.template(cg.std_string), + "bool": bool, + "int": cg.int32, + "float": float, + "string": cg.std_string, + "bool[]": cg.std_vector.template(bool), + "int[]": cg.std_vector.template(cg.int32), + "float[]": cg.std_vector.template(float), + "string[]": cg.std_vector.template(cg.std_string), } +CONF_ENCRYPTION = "encryption" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(APIServer), - cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=''): cv.string_strict, - cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SERVICES): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), - cv.Required(CONF_SERVICE): cv.valid_name, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema({ - cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), - }), - }), -}).extend(cv.COMPONENT_SCHEMA) + +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( + { + cv.GenerateID(): cv.declare_id(APIServer), + cv.Optional(CONF_PORT, default=6053): cv.port, + cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SERVICES): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), + cv.Required(CONF_SERVICE): cv.valid_name, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( + { + cv.validate_id_name: cv.one_of( + *SERVICE_ARG_NATIVE_TYPES, lower=True + ), + } + ), + } + ), + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), + } +).extend(cv.COMPONENT_SCHEMA) @coroutine_with_priority(40.0) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_password(config[CONF_PASSWORD])) @@ -61,81 +110,128 @@ def to_code(config): func_args.append((native, name)) service_arg_names.append(name) templ = cg.TemplateArguments(*template_args) - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], templ, - conf[CONF_SERVICE], service_arg_names) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names + ) cg.add(var.register_user_service(trigger)) - yield automation.build_automation(trigger, func_args, conf) + await automation.build_automation(trigger, func_args, conf) - cg.add_define('USE_API') + 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.4") + else: + cg.add_define("USE_API_PLAINTEXT") + + cg.add_define("USE_API") cg.add_global(api_ns.using) KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) -HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_SERVICE): cv.templatable(cv.string), - cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema({cv.string: cv.returning_lambda}), -}) +HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Required(CONF_SERVICE): cv.templatable(cv.string), + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( + {cv.string: cv.returning_lambda} + ), + } +) -@automation.register_action('homeassistant.service', HomeAssistantServiceCallAction, - HOMEASSISTANT_SERVICE_ACTION_SCHEMA) -def homeassistant_service_to_code(config, action_id, template_arg, args): - serv = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "homeassistant.service", + HomeAssistantServiceCallAction, + HOMEASSISTANT_SERVICE_ACTION_SCHEMA, +) +async def homeassistant_service_to_code(config, action_id, template_arg, args): + serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = yield cg.templatable(config[CONF_SERVICE], args, None) + templ = await cg.templatable(config[CONF_SERVICE], args, None) cg.add(var.set_service(templ)) for key, value in config[CONF_DATA].items(): - templ = yield cg.templatable(value, args, None) + templ = await cg.templatable(value, args, None) cg.add(var.add_data(key, templ)) for key, value in config[CONF_DATA_TEMPLATE].items(): - templ = yield cg.templatable(value, args, None) + templ = await cg.templatable(value, args, None) cg.add(var.add_data_template(key, templ)) for key, value in config[CONF_VARIABLES].items(): - templ = yield cg.templatable(value, args, None) + templ = await cg.templatable(value, args, None) cg.add(var.add_variable(key, templ)) - yield var + return var def validate_homeassistant_event(value): value = cv.string(value) - if not value.startswith('esphome.'): - raise cv.Invalid("ESPHome can only generate Home Assistant events that begin with " - "esphome. For example 'esphome.xyz'") + if not value.startswith("esphome."): + raise cv.Invalid( + "ESPHome can only generate Home Assistant events that begin with " + "esphome. For example 'esphome.xyz'" + ) return value -HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_EVENT): validate_homeassistant_event, - cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA, -}) +HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Required(CONF_EVENT): validate_homeassistant_event, + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA, + } +) -@automation.register_action('homeassistant.event', HomeAssistantServiceCallAction, - HOMEASSISTANT_EVENT_ACTION_SCHEMA) -def homeassistant_event_to_code(config, action_id, template_arg, args): - serv = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "homeassistant.event", + HomeAssistantServiceCallAction, + HOMEASSISTANT_EVENT_ACTION_SCHEMA, +) +async def homeassistant_event_to_code(config, action_id, template_arg, args): + serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - templ = yield cg.templatable(config[CONF_EVENT], args, None) + templ = await cg.templatable(config[CONF_EVENT], args, None) cg.add(var.set_service(templ)) for key, value in config[CONF_DATA].items(): - templ = yield cg.templatable(value, args, None) + templ = await cg.templatable(value, args, None) cg.add(var.add_data(key, templ)) for key, value in config[CONF_DATA_TEMPLATE].items(): - templ = yield cg.templatable(value, args, None) + templ = await cg.templatable(value, args, None) cg.add(var.add_data_template(key, templ)) for key, value in config[CONF_VARIABLES].items(): - templ = yield cg.templatable(value, args, None) + templ = await cg.templatable(value, args, None) cg.add(var.add_variable(key, templ)) - yield var + return var -@automation.register_condition('api.connected', APIConnectedCondition, {}) -def api_connected_to_code(config, condition_id, template_arg, args): - yield cg.new_Pvariable(condition_id, template_arg) +HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Required(CONF_TAG): cv.templatable(cv.string_strict), + }, + key=CONF_TAG, +) + + +@automation.register_action( + "homeassistant.tag_scanned", + HomeAssistantServiceCallAction, + HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, +) +async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): + serv = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, serv, True) + cg.add(var.set_service("esphome.tag_scanned")) + templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) + cg.add(var.add_data("tag_id", templ)) + return var + + +@automation.register_condition("api.connected", APIConnectedCondition, {}) +async def api_connected_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4bb7d1b555..dca722dca5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -38,6 +38,9 @@ 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) {} + rpc button_command (ButtonCommandRequest) returns (void) {} } @@ -46,6 +49,7 @@ service APIConnection { // The Home Assistant protocol is structured as a simple // TCP socket with short binary messages encoded in the protocol buffers format // First, a message in this protocol has a specific format: +// * A zero byte. // * VarInt denoting the size of the message object. (type is not part of this) // * VarInt denoting the type of message. // * The message object encoded as a ProtoBuf message @@ -175,6 +179,12 @@ message DeviceInfoResponse { string model = 6; bool has_deep_sleep = 7; + + // The esphome project details if set + string project_name = 8; + string project_version = 9; + + uint32 webserver_port = 10; } message ListEntitiesRequest { @@ -194,6 +204,14 @@ message SubscribeStatesRequest { // Empty } +// ==================== COMMON ===================== + +enum EntityCategory { + ENTITY_CATEGORY_NONE = 0; + ENTITY_CATEGORY_CONFIG = 1; + ENTITY_CATEGORY_DIAGNOSTIC = 2; +} + // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; @@ -207,6 +225,9 @@ message ListEntitiesBinarySensorResponse { string device_class = 5; bool is_status_binary_sensor = 6; + bool disabled_by_default = 7; + string icon = 8; + EntityCategory entity_category = 9; } message BinarySensorStateResponse { option (id) = 21; @@ -236,6 +257,9 @@ message ListEntitiesCoverResponse { bool supports_position = 6; bool supports_tilt = 7; string device_class = 8; + bool disabled_by_default = 9; + string icon = 10; + EntityCategory entity_category = 11; } enum LegacyCoverState { @@ -301,12 +325,21 @@ message ListEntitiesFanResponse { bool supports_oscillation = 5; bool supports_speed = 6; + bool supports_direction = 7; + int32 supported_speed_count = 8; + bool disabled_by_default = 9; + string icon = 10; + EntityCategory entity_category = 11; } enum FanSpeed { FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; } +enum FanDirection { + FAN_DIRECTION_FORWARD = 0; + FAN_DIRECTION_REVERSE = 1; +} message FanStateResponse { option (id) = 23; option (source) = SOURCE_SERVER; @@ -316,7 +349,9 @@ message FanStateResponse { fixed32 key = 1; bool state = 2; bool oscillating = 3; - FanSpeed speed = 4; + FanSpeed speed = 4 [deprecated = true]; + FanDirection direction = 5; + int32 speed_level = 6; } message FanCommandRequest { option (id) = 31; @@ -327,13 +362,29 @@ message FanCommandRequest { fixed32 key = 1; bool has_state = 2; bool state = 3; - bool has_speed = 4; - FanSpeed speed = 5; + bool has_speed = 4 [deprecated = true]; + FanSpeed speed = 5 [deprecated = true]; bool has_oscillating = 6; bool oscillating = 7; + bool has_direction = 8; + FanDirection direction = 9; + bool has_speed_level = 10; + int32 speed_level = 11; } // ==================== 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; @@ -344,13 +395,18 @@ 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; + EntityCategory entity_category = 15; } message LightStateResponse { option (id) = 24; @@ -361,11 +417,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 { @@ -379,6 +439,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; @@ -387,6 +451,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; @@ -396,6 +464,18 @@ message LightCommandRequest { } // ==================== SENSOR ==================== +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 { option (id) = 16; option (source) = SOURCE_SERVER; @@ -410,6 +490,12 @@ message ListEntitiesSensorResponse { string unit_of_measurement = 6; int32 accuracy_decimals = 7; 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; + EntityCategory entity_category = 13; } message SensorStateResponse { option (id) = 25; @@ -437,6 +523,8 @@ message ListEntitiesSwitchResponse { string icon = 5; bool assumed_state = 6; + bool disabled_by_default = 7; + EntityCategory entity_category = 8; } message SwitchStateResponse { option (id) = 26; @@ -469,6 +557,8 @@ message ListEntitiesTextSensorResponse { string unique_id = 4; string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; } message TextSensorStateResponse { option (id) = 27; @@ -489,9 +579,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; @@ -506,7 +597,6 @@ message SubscribeLogsResponse { option (no_delay) = false; LogLevel level = 1; - string tag = 2; string message = 3; bool send_failed = 4; } @@ -547,6 +637,7 @@ message SubscribeHomeAssistantStateResponse { option (id) = 39; option (source) = SOURCE_SERVER; string entity_id = 1; + string attribute = 2; } message HomeAssistantStateResponse { @@ -556,6 +647,7 @@ message HomeAssistantStateResponse { string entity_id = 1; string state = 2; + string attribute = 3; } // ==================== IMPORT TIME ==================== @@ -626,6 +718,9 @@ message ListEntitiesCameraResponse { fixed32 key = 2; string name = 3; string unique_id = 4; + bool disabled_by_default = 5; + string icon = 6; + EntityCategory entity_category = 7; } message CameraImageResponse { @@ -650,11 +745,12 @@ message CameraImageRequest { // ==================== CLIMATE ==================== enum ClimateMode { CLIMATE_MODE_OFF = 0; - CLIMATE_MODE_AUTO = 1; + CLIMATE_MODE_HEAT_COOL = 1; CLIMATE_MODE_COOL = 2; CLIMATE_MODE_HEAT = 3; CLIMATE_MODE_FAN_ONLY = 4; CLIMATE_MODE_DRY = 5; + CLIMATE_MODE_AUTO = 6; } enum ClimateFanMode { CLIMATE_FAN_ON = 0; @@ -671,7 +767,7 @@ enum ClimateSwingMode { CLIMATE_SWING_OFF = 0; CLIMATE_SWING_BOTH = 1; CLIMATE_SWING_VERTICAL = 2; - CLIMATE_SWINT_HORIZONTAL = 3; + CLIMATE_SWING_HORIZONTAL = 3; } enum ClimateAction { CLIMATE_ACTION_OFF = 0; @@ -682,6 +778,16 @@ enum ClimateAction { CLIMATE_ACTION_DRYING = 5; CLIMATE_ACTION_FAN = 6; } +enum ClimatePreset { + 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; option (source) = SOURCE_SERVER; @@ -698,10 +804,18 @@ 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; + EntityCategory entity_category = 20; } message ClimateStateResponse { option (id) = 47; @@ -715,10 +829,14 @@ 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; + string custom_fan_mode = 11; + ClimatePreset preset = 12; + string custom_preset = 13; } message ClimateCommandRequest { option (id) = 48; @@ -735,10 +853,127 @@ 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; ClimateSwingMode swing_mode = 15; + bool has_custom_fan_mode = 16; + string custom_fan_mode = 17; + bool has_preset = 18; + ClimatePreset preset = 19; + bool has_custom_preset = 20; + string custom_preset = 21; +} + +// ==================== NUMBER ==================== +enum NumberMode { + NUMBER_MODE_AUTO = 0; + NUMBER_MODE_BOX = 1; + NUMBER_MODE_SLIDER = 2; +} +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; + EntityCategory entity_category = 10; + string unit_of_measurement = 11; + NumberMode mode = 12; +} +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; + EntityCategory entity_category = 8; +} +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; +} + +// ==================== BUTTON ==================== +message ListEntitiesButtonResponse { + option (id) = 61; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BUTTON"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; +} +message ButtonCommandRequest { + option (id) = 62; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BUTTON"; + option (no_delay) = true; + + fixed32 key = 1; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index beccf91d27..f615815023 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" @@ -9,149 +12,155 @@ #ifdef USE_HOMEASSISTANT_TIME #include "esphome/components/homeassistant/time/homeassistant_time.h" #endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_helpers.h" +#endif namespace esphome { namespace api { -static const char *TAG = "api.connection"; +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 if (err == APIError::CONNECTION_CLOSED) { + ESP_LOGW(TAG, "%s: Connection closed", 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_ >= (int) 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 @@ -173,6 +182,9 @@ 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(); + msg.entity_category = static_cast(binary_sensor->get_entity_category()); return this->send_list_entities_binary_sensor_response(msg); } #endif @@ -204,6 +216,9 @@ 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(); + msg.entity_category = static_cast(cover->get_entity_category()); return this->send_list_entities_cover_response(msg); } void APIConnection::cover_command(const CoverCommandRequest &msg) { @@ -236,6 +251,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; @@ -246,8 +264,12 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { resp.state = fan->state; if (traits.supports_oscillation()) resp.oscillating = fan->oscillating; - if (traits.supports_speed()) - resp.speed = static_cast(fan->speed); + if (traits.supports_speed()) { + resp.speed_level = fan->speed; + resp.speed = static_cast(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count())); + } + if (traits.supports_direction()) + resp.direction = static_cast(fan->direction); return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::FanState *fan) { @@ -259,6 +281,11 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.unique_id = get_default_unique_id("fan", fan); msg.supports_oscillation = traits.supports_oscillation(); 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(); + msg.entity_category = static_cast(fan->get_entity_category()); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -266,15 +293,24 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (fan == nullptr) return; + auto traits = fan->get_traits(); + auto call = fan->make_call(); if (msg.has_state) call.set_state(msg.state); if (msg.has_oscillating) call.set_oscillating(msg.oscillating); - if (msg.has_speed) - call.set_speed(static_cast(msg.speed)); + if (msg.has_speed_level) { + // Prefer level + call.set_speed(msg.speed_level); + } else if (msg.has_speed) { + call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); + } + if (msg.has_direction) + call.set_direction(static_cast(msg.direction)); call.perform(); } +#pragma GCC diagnostic pop #endif #ifdef USE_LIGHT @@ -284,21 +320,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); @@ -310,11 +346,23 @@ 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(); + msg.entity_category = static_cast(light->get_entity_category()); + + 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(); } @@ -335,6 +383,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); @@ -344,6 +396,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) @@ -377,6 +433,10 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.unit_of_measurement = sensor->get_unit_of_measurement(); 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->get_state_class()); + msg.disabled_by_default = sensor->is_disabled_by_default(); + msg.entity_category = static_cast(sensor->get_entity_category()); return this->send_list_entities_sensor_response(msg); } #endif @@ -399,6 +459,8 @@ 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(); + msg.entity_category = static_cast(a_switch->get_entity_category()); return this->send_list_entities_switch_response(msg); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { @@ -433,6 +495,8 @@ 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(); + msg.entity_category = static_cast(text_sensor->get_entity_category()); return this->send_list_entities_text_sensor_response(msg); } #endif @@ -455,10 +519,16 @@ 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()) - resp.fan_mode = static_cast(climate->fan_mode); + 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()) { + 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()) resp.swing_mode = static_cast(climate->swing_mode); return this->send_climate_state_response(resp); @@ -470,29 +540,33 @@ 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.entity_category = static_cast(climate->get_entity_category()); + 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}) { - 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 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 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 : 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 : 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) { @@ -509,23 +583,128 @@ 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) + call.set_fan_mode(msg.custom_fan_mode); + if (msg.has_preset) + call.set_preset(static_cast(msg.preset)); + if (msg.has_custom_preset) + call.set_preset(msg.custom_preset); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); } #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.entity_category = static_cast(number->get_entity_category()); + msg.unit_of_measurement = number->traits.get_unit_of_measurement(); + msg.mode = static_cast(number->traits.get_mode()); + + 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(); + msg.entity_category = static_cast(select->get_entity_category()); + + 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_BUTTON +bool APIConnection::send_button_info(button::Button *button) { + ListEntitiesButtonResponse msg; + msg.key = button->get_object_id_hash(); + msg.object_id = button->get_object_id(); + msg.name = button->get_name(); + msg.unique_id = get_default_unique_id("button", button); + msg.icon = button->get_icon(); + msg.disabled_by_default = button->is_disabled_by_default(); + msg.entity_category = static_cast(button->get_entity_category()); + msg.device_class = button->get_device_class(); + return this->send_list_entities_button_response(msg); +} +void APIConnection::button_command(const ButtonCommandRequest &msg) { + button::Button *button = App.get_button_by_key(msg.key); + if (button == nullptr) + return; + + button->press(); +} +#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; @@ -533,6 +712,9 @@ 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(); + msg.icon = camera->get_icon(); + msg.entity_category = static_cast(camera->get_entity_category()); return this->send_list_entities_camera_response(msg); } void APIConnection::camera_image(const CameraImageRequest &msg) { @@ -561,30 +743,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 = 3; + resp.api_version_minor = 6; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; @@ -596,7 +768,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 @@ -614,18 +786,24 @@ 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 +#ifdef ESPHOME_PROJECT_NAME + resp.project_name = ESPHOME_PROJECT_NAME; + resp.project_version = ESPHOME_PROJECT_VERSION; +#endif +#ifdef USE_WEBSERVER + resp.webserver_port = WEBSERVER_PORT; #endif return resp; } void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { for (auto &it : this->parent_->get_state_subs()) - if (it.entity_id == msg.entity_id) + if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { it.callback(msg.state); + } } void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; @@ -639,29 +817,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; - 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"); @@ -671,22 +840,31 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } } - this->client_->add(reinterpret_cast(header.data()), header.size()); - this->client_->add(reinterpret_cast(buffer.get_buffer()->data()), buffer.get_buffer()->size()); - 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..72697b5911 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,20 @@ 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 +#ifdef USE_BUTTON + bool send_button_info(button::Button *button); + void button_command(const ButtonCommandRequest &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 +91,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 +102,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 +132,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 +151,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 +166,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..151c2512de --- /dev/null +++ b/esphome/components/api/api_frame_helper.cpp @@ -0,0 +1,1010 @@ +#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 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"; + } else if (err == APIError::CONNECTION_CLOSED) { + return "CONNECTION_CLOSED"; + } + 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) { + 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 (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; + } + rx_header_buf_len_ += received; + if ((size_t) 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 (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; + } + rx_buf_len_ += received; + if ((size_t) 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", format_hex_pretty(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; + 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", + format_hex_pretty(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 ((size_t) 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) { + 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 (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; + } + 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 (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; + } + rx_buf_len_ += received; + if ((size_t) 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", format_hex_pretty(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) { + 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) { + 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; + 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", + format_hex_pretty(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 ((size_t) 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..57e3c961d5 --- /dev/null +++ b/esphome/components/api/api_frame_helper.h @@ -0,0 +1,185 @@ +#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, + CONNECTION_CLOSED = 1022, +}; + +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 6b98f95f53..5b6853c276 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6,6 +6,18 @@ namespace esphome { namespace api { +template<> const char *proto_enum_to_string(enums::EntityCategory value) { + switch (value) { + case enums::ENTITY_CATEGORY_NONE: + return "ENTITY_CATEGORY_NONE"; + case enums::ENTITY_CATEGORY_CONFIG: + return "ENTITY_CATEGORY_CONFIG"; + case enums::ENTITY_CATEGORY_DIAGNOSTIC: + return "ENTITY_CATEGORY_DIAGNOSTIC"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { switch (value) { case enums::LEGACY_COVER_STATE_OPEN: @@ -52,6 +64,66 @@ template<> const char *proto_enum_to_string(enums::FanSpeed val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::FanDirection value) { + switch (value) { + case enums::FAN_DIRECTION_FORWARD: + return "FAN_DIRECTION_FORWARD"; + case enums::FAN_DIRECTION_REVERSE: + return "FAN_DIRECTION_REVERSE"; + default: + 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"; + } +} template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: @@ -62,6 +134,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: @@ -98,8 +172,8 @@ template<> const char *proto_enum_to_string(enums::ClimateMo switch (value) { case enums::CLIMATE_MODE_OFF: return "CLIMATE_MODE_OFF"; - case enums::CLIMATE_MODE_AUTO: - return "CLIMATE_MODE_AUTO"; + case enums::CLIMATE_MODE_HEAT_COOL: + return "CLIMATE_MODE_HEAT_COOL"; case enums::CLIMATE_MODE_COOL: return "CLIMATE_MODE_COOL"; case enums::CLIMATE_MODE_HEAT: @@ -108,6 +182,8 @@ template<> const char *proto_enum_to_string(enums::ClimateMo return "CLIMATE_MODE_FAN_ONLY"; case enums::CLIMATE_MODE_DRY: return "CLIMATE_MODE_DRY"; + case enums::CLIMATE_MODE_AUTO: + return "CLIMATE_MODE_AUTO"; default: return "UNKNOWN"; } @@ -144,8 +220,8 @@ template<> const char *proto_enum_to_string(enums::Clim return "CLIMATE_SWING_BOTH"; case enums::CLIMATE_SWING_VERTICAL: return "CLIMATE_SWING_VERTICAL"; - case enums::CLIMATE_SWINT_HORIZONTAL: - return "CLIMATE_SWINT_HORIZONTAL"; + case enums::CLIMATE_SWING_HORIZONTAL: + return "CLIMATE_SWING_HORIZONTAL"; default: return "UNKNOWN"; } @@ -168,6 +244,40 @@ template<> const char *proto_enum_to_string(enums::Climate return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::ClimatePreset value) { + switch (value) { + 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_ECO: + return "CLIMATE_PRESET_ECO"; + case enums::CLIMATE_PRESET_SLEEP: + return "CLIMATE_PRESET_SLEEP"; + case enums::CLIMATE_PRESET_ACTIVITY: + return "CLIMATE_PRESET_ACTIVITY"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::NumberMode value) { + switch (value) { + case enums::NUMBER_MODE_AUTO: + return "NUMBER_MODE_AUTO"; + case enums::NUMBER_MODE_BOX: + return "NUMBER_MODE_BOX"; + case enums::NUMBER_MODE_SLIDER: + return "NUMBER_MODE_SLIDER"; + default: + return "UNKNOWN"; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -179,14 +289,16 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("HelloRequest {\n"); out.append(" client_info: "); out.append("'").append(this->client_info).append("'"); out.append("\n"); out.append("}"); } +#endif bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -216,8 +328,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("HelloResponse {\n"); out.append(" api_version_major: "); sprintf(buffer, "%u", this->api_version_major); @@ -234,6 +347,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: { @@ -245,14 +359,16 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("ConnectRequest {\n"); out.append(" password: "); out.append("'").append(this->password).append("'"); out.append("\n"); out.append("}"); } +#endif bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -264,24 +380,36 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("ConnectResponse {\n"); out.append(" invalid_password: "); out.append(YESNO(this->invalid_password)); 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: { @@ -292,6 +420,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_deep_sleep = value.as_bool(); return true; } + case 10: { + this->webserver_port = value.as_uint32(); + return true; + } default: return false; } @@ -318,6 +450,14 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->model = value.as_string(); return true; } + case 8: { + this->project_name = value.as_string(); + return true; + } + case 9: { + this->project_version = value.as_string(); + return true; + } default: return false; } @@ -330,9 +470,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->compilation_time); buffer.encode_string(6, this->model); buffer.encode_bool(7, this->has_deep_sleep); + buffer.encode_string(8, this->project_name); + buffer.encode_string(9, this->project_version); + buffer.encode_uint32(10, this->webserver_port); } +#ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("DeviceInfoResponse {\n"); out.append(" uses_password: "); out.append(YESNO(this->uses_password)); @@ -361,20 +505,48 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" has_deep_sleep: "); out.append(YESNO(this->has_deep_sleep)); out.append("\n"); + + out.append(" project_name: "); + out.append("'").append(this->project_name).append("'"); + out.append("\n"); + + out.append(" project_version: "); + out.append("'").append(this->project_version).append("'"); + out.append("\n"); + + out.append(" webserver_port: "); + sprintf(buffer, "%u", this->webserver_port); + out.append(buffer); + 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; + } + case 9: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -397,6 +569,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; } @@ -418,9 +594,13 @@ 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); + buffer.encode_enum(9, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesBinarySensorResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -446,8 +626,21 @@ 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -477,8 +670,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("BinarySensorStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -494,6 +688,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: { @@ -508,6 +703,14 @@ 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; + } + case 11: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -530,6 +733,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; } @@ -553,9 +760,13 @@ 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); + buffer.encode_enum(11, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesCoverResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -589,8 +800,21 @@ 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -630,8 +854,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("CoverStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -657,6 +882,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: { @@ -711,8 +937,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("CoverCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -750,6 +977,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: { @@ -760,6 +988,22 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supports_speed = value.as_bool(); return true; } + case 7: { + this->supports_direction = value.as_bool(); + return true; + } + case 8: { + this->supported_speed_count = value.as_int32(); + return true; + } + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 11: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -778,6 +1022,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; } @@ -799,9 +1047,15 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); + buffer.encode_bool(7, this->supports_direction); + buffer.encode_int32(8, this->supported_speed_count); + buffer.encode_bool(9, this->disabled_by_default); + buffer.encode_string(10, this->icon); + buffer.encode_enum(11, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesFanResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -827,8 +1081,30 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" supports_speed: "); out.append(YESNO(this->supports_speed)); out.append("\n"); + + out.append(" supports_direction: "); + out.append(YESNO(this->supports_direction)); + out.append("\n"); + + out.append(" supported_speed_count: "); + 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -843,6 +1119,14 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed = value.as_enum(); return true; } + case 5: { + this->direction = value.as_enum(); + return true; + } + case 6: { + this->speed_level = value.as_int32(); + return true; + } default: return false; } @@ -862,9 +1146,12 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_enum(4, this->speed); + 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]; + __attribute__((unused)) char buffer[64]; out.append("FanStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -882,8 +1169,18 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(" speed: "); out.append(proto_enum_to_string(this->speed)); out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); + + out.append(" speed_level: "); + sprintf(buffer, "%d", this->speed_level); + out.append(buffer); + out.append("\n"); out.append("}"); } +#endif bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -910,6 +1207,22 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->oscillating = value.as_bool(); return true; } + case 8: { + this->has_direction = value.as_bool(); + return true; + } + case 9: { + this->direction = value.as_enum(); + return true; + } + case 10: { + this->has_speed_level = value.as_bool(); + return true; + } + case 11: { + this->speed_level = value.as_int32(); + return true; + } default: return false; } @@ -932,9 +1245,14 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->speed); buffer.encode_bool(6, this->has_oscillating); buffer.encode_bool(7, this->oscillating); + buffer.encode_bool(8, this->has_direction); + buffer.encode_enum(9, this->direction); + 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]; + __attribute__((unused)) char buffer[64]; out.append("FanCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -964,24 +1282,54 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" oscillating: "); out.append(YESNO(this->oscillating)); out.append("\n"); + + out.append(" has_direction: "); + out.append(YESNO(this->has_direction)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); + + out.append(" has_speed_level: "); + out.append(YESNO(this->has_speed_level)); + out.append("\n"); + + out.append(" speed_level: "); + sprintf(buffer, "%d", this->speed_level); + out.append(buffer); + 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; + } + case 15: { + this->entity_category = value.as_enum(); return true; } default: @@ -1006,6 +1354,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; } @@ -1033,18 +1385,25 @@ 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); + buffer.encode_enum(15, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesLightResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -1063,20 +1422,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: "); @@ -1094,14 +1459,31 @@ 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + 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; } @@ -1126,6 +1508,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; @@ -1146,6 +1532,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; } @@ -1154,15 +1548,20 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("LightStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -1178,6 +1577,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); @@ -1203,11 +1611,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: { @@ -1222,6 +1641,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; @@ -1234,6 +1665,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; @@ -1278,6 +1717,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; @@ -1298,6 +1741,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; } @@ -1308,6 +1759,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); @@ -1316,6 +1771,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); @@ -1323,8 +1782,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("LightCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -1348,6 +1808,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"); @@ -1385,6 +1862,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"); @@ -1412,6 +1907,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: { @@ -1422,6 +1918,22 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->force_update = value.as_bool(); return true; } + case 10: { + 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; + } + case 13: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -1448,6 +1960,10 @@ bool ListEntitiesSensorResponse::decode_length(uint32_t field_id, ProtoLengthDel this->unit_of_measurement = value.as_string(); return true; } + case 9: { + this->device_class = value.as_string(); + return true; + } default: return false; } @@ -1471,9 +1987,15 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(6, this->unit_of_measurement); buffer.encode_int32(7, this->accuracy_decimals); buffer.encode_bool(8, this->force_update); + buffer.encode_string(9, this->device_class); + buffer.encode_enum(10, this->state_class); + buffer.encode_enum(11, this->legacy_last_reset_type); + buffer.encode_bool(12, this->disabled_by_default); + buffer.encode_enum(13, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSensorResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -1508,8 +2030,29 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" force_update: "); out.append(YESNO(this->force_update)); out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -1539,8 +2082,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("SensorStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -1557,12 +2101,21 @@ 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; + } + case 8: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -1606,9 +2159,12 @@ 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); + buffer.encode_enum(8, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSwitchResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -1634,8 +2190,17 @@ 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1660,8 +2225,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("SwitchStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -1673,6 +2239,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: { @@ -1697,8 +2264,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("SwitchCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -1710,6 +2278,21 @@ 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; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -1748,9 +2331,12 @@ 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); + buffer.encode_enum(7, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesTextSensorResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -1772,8 +2358,17 @@ 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -1809,8 +2404,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("TextSensorStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -1826,6 +2422,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: { @@ -1844,8 +2441,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("SubscribeLogsRequest {\n"); out.append(" level: "); out.append(proto_enum_to_string(this->level)); @@ -1856,6 +2454,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: { @@ -1872,10 +2471,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; @@ -1886,21 +2481,17 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("SubscribeLogsResponse {\n"); out.append(" level: "); 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"); @@ -1910,10 +2501,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: { @@ -1932,8 +2526,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("HomeassistantServiceMap {\n"); out.append(" key: "); out.append("'").append(this->key).append("'"); @@ -1944,6 +2539,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: { @@ -1989,8 +2585,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("HomeassistantServiceResponse {\n"); out.append(" service: "); out.append("'").append(this->service).append("'"); @@ -2019,31 +2616,45 @@ 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: { this->entity_id = value.as_string(); return true; } + case 2: { + this->attribute = value.as_string(); + return true; + } default: return false; } } 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]; + __attribute__((unused)) char buffer[64]; out.append("SubscribeHomeAssistantStateResponse {\n"); out.append(" entity_id: "); out.append("'").append(this->entity_id).append("'"); out.append("\n"); + + out.append(" attribute: "); + out.append("'").append(this->attribute).append("'"); + out.append("\n"); out.append("}"); } +#endif bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2054,6 +2665,10 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel this->state = value.as_string(); return true; } + case 3: { + this->attribute = value.as_string(); + return true; + } default: return false; } @@ -2061,9 +2676,11 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->entity_id); 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]; + __attribute__((unused)) char buffer[64]; out.append("HomeAssistantStateResponse {\n"); out.append(" entity_id: "); out.append("'").append(this->entity_id).append("'"); @@ -2072,10 +2689,17 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { out.append(" state: "); out.append("'").append(this->state).append("'"); out.append("\n"); + + out.append(" attribute: "); + out.append("'").append(this->attribute).append("'"); + 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: { @@ -2087,8 +2711,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("GetTimeResponse {\n"); out.append(" epoch_seconds: "); sprintf(buffer, "%u", this->epoch_seconds); @@ -2096,6 +2721,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: { @@ -2120,8 +2746,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesServicesArgument {\n"); out.append(" name: "); out.append("'").append(this->name).append("'"); @@ -2132,6 +2759,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: { @@ -2163,8 +2791,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesServicesResponse {\n"); out.append(" name: "); out.append("'").append(this->name).append("'"); @@ -2182,6 +2811,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: { @@ -2255,8 +2885,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("ExecuteServiceArgument {\n"); out.append(" bool_: "); out.append(YESNO(this->bool_)); @@ -2308,6 +2939,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: { @@ -2334,8 +2966,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("ExecuteServiceRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -2349,6 +2982,21 @@ 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; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2363,6 +3011,10 @@ bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDel this->unique_id = value.as_string(); return true; } + case 6: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -2382,9 +3034,13 @@ 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); + buffer.encode_string(6, this->icon); + buffer.encode_enum(7, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesCameraResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -2402,8 +3058,21 @@ 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(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -2439,8 +3108,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("CameraImageResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -2456,6 +3126,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: { @@ -2474,8 +3145,9 @@ 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]; + __attribute__((unused)) char buffer[64]; out.append("CameraImageRequest {\n"); out.append(" single: "); out.append(YESNO(this->single)); @@ -2486,6 +3158,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: { @@ -2501,7 +3174,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: { @@ -2516,6 +3189,18 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supported_swing_modes.push_back(value.as_enum()); return true; } + case 16: { + this->supported_presets.push_back(value.as_enum()); + return true; + } + case 18: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 20: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2534,6 +3219,18 @@ bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDe this->unique_id = value.as_string(); return true; } + case 15: { + this->supported_custom_fan_modes.push_back(value.as_string()); + return true; + } + case 17: { + this->supported_custom_presets.push_back(value.as_string()); + return true; + } + case 19: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -2573,7 +3270,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); @@ -2581,9 +3278,22 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_swing_modes) { buffer.encode_enum(14, it, true); } + for (auto &it : this->supported_custom_fan_modes) { + buffer.encode_string(15, it, true); + } + for (auto &it : this->supported_presets) { + buffer.encode_enum(16, it, true); + } + 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); + buffer.encode_enum(20, this->entity_category); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesClimateResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -2631,8 +3341,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: "); @@ -2650,8 +3360,39 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(it)); out.append("\n"); } + + for (const auto &it : this->supported_custom_fan_modes) { + out.append(" supported_custom_fan_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + for (const auto &it : this->supported_presets) { + out.append(" supported_presets: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_custom_presets) { + out.append(" supported_custom_presets: "); + 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } +#endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2659,7 +3400,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: { @@ -2674,6 +3415,24 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->swing_mode = value.as_enum(); return true; } + case 12: { + this->preset = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ClimateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 11: { + this->custom_fan_mode = value.as_string(); + return true; + } + case 13: { + this->custom_preset = value.as_string(); + return true; + } default: return false; } @@ -2711,13 +3470,17 @@ 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); + buffer.encode_string(11, this->custom_fan_mode); + 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]; + __attribute__((unused)) char buffer[64]; out.append("ClimateStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -2748,8 +3511,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: "); @@ -2763,8 +3526,21 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" swing_mode: "); out.append(proto_enum_to_string(this->swing_mode)); out.append("\n"); + + out.append(" custom_fan_mode: "); + out.append("'").append(this->custom_fan_mode).append("'"); + out.append("\n"); + + out.append(" preset: "); + out.append(proto_enum_to_string(this->preset)); + out.append("\n"); + + out.append(" custom_preset: "); + out.append("'").append(this->custom_preset).append("'"); + out.append("\n"); out.append("}"); } +#endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2788,11 +3564,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: { @@ -2811,6 +3587,36 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->swing_mode = value.as_enum(); return true; } + case 16: { + this->has_custom_fan_mode = value.as_bool(); + return true; + } + case 18: { + this->has_preset = value.as_bool(); + return true; + } + case 19: { + this->preset = value.as_enum(); + return true; + } + case 20: { + this->has_custom_preset = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 17: { + this->custom_fan_mode = value.as_string(); + return true; + } + case 21: { + this->custom_preset = value.as_string(); + return true; + } default: return false; } @@ -2847,15 +3653,22 @@ 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); buffer.encode_enum(15, this->swing_mode); + buffer.encode_bool(16, this->has_custom_fan_mode); + buffer.encode_string(17, this->custom_fan_mode); + buffer.encode_bool(18, this->has_preset); + buffer.encode_enum(19, this->preset); + 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]; + __attribute__((unused)) char buffer[64]; out.append("ClimateCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -2897,12 +3710,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: "); @@ -2920,8 +3733,571 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" swing_mode: "); out.append(proto_enum_to_string(this->swing_mode)); out.append("\n"); + + out.append(" has_custom_fan_mode: "); + out.append(YESNO(this->has_custom_fan_mode)); + out.append("\n"); + + out.append(" custom_fan_mode: "); + out.append("'").append(this->custom_fan_mode).append("'"); + out.append("\n"); + + out.append(" has_preset: "); + out.append(YESNO(this->has_preset)); + out.append("\n"); + + out.append(" preset: "); + out.append(proto_enum_to_string(this->preset)); + out.append("\n"); + + out.append(" has_custom_preset: "); + out.append(YESNO(this->has_custom_preset)); + out.append("\n"); + + out.append(" custom_preset: "); + out.append("'").append(this->custom_preset).append("'"); + 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; + } + case 10: { + this->entity_category = value.as_enum(); + return true; + } + case 12: { + this->mode = value.as_enum(); + 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; + } + case 11: { + this->unit_of_measurement = 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); + buffer.encode_enum(10, this->entity_category); + buffer.encode_string(11, this->unit_of_measurement); + buffer.encode_enum(12, this->mode); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesNumberResponse::dump_to(std::string &out) const { + __attribute__((unused)) 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" unit_of_measurement: "); + out.append("'").append(this->unit_of_measurement).append("'"); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + 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 { + __attribute__((unused)) 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 { + __attribute__((unused)) 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; + } + case 8: { + this->entity_category = value.as_enum(); + 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); + buffer.encode_enum(8, this->entity_category); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesSelectResponse::dump_to(std::string &out) const { + __attribute__((unused)) 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(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + 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 { + __attribute__((unused)) 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 { + __attribute__((unused)) 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 +bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesButtonResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesButtonResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_string(8, this->device_class); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesButtonResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesButtonResponse {\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(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); } +#ifdef HAS_PROTO_MESSAGE_DUMP +void ButtonCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("ButtonCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + 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 8be89f0365..e92b2fa4b6 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -9,6 +9,11 @@ namespace api { namespace enums { +enum EntityCategory : uint32_t { + ENTITY_CATEGORY_NONE = 0, + ENTITY_CATEGORY_CONFIG = 1, + ENTITY_CATEGORY_DIAGNOSTIC = 2, +}; enum LegacyCoverState : uint32_t { LEGACY_COVER_STATE_OPEN = 0, LEGACY_COVER_STATE_CLOSED = 1, @@ -28,14 +33,41 @@ enum FanSpeed : uint32_t { FAN_SPEED_MEDIUM = 1, FAN_SPEED_HIGH = 2, }; +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, @@ -49,11 +81,12 @@ enum ServiceArgType : uint32_t { }; enum ClimateMode : uint32_t { CLIMATE_MODE_OFF = 0, - CLIMATE_MODE_AUTO = 1, + CLIMATE_MODE_HEAT_COOL = 1, CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, CLIMATE_MODE_FAN_ONLY = 4, CLIMATE_MODE_DRY = 5, + CLIMATE_MODE_AUTO = 6, }; enum ClimateFanMode : uint32_t { CLIMATE_FAN_ON = 0, @@ -70,7 +103,7 @@ enum ClimateSwingMode : uint32_t { CLIMATE_SWING_OFF = 0, CLIMATE_SWING_BOTH = 1, CLIMATE_SWING_VERTICAL = 2, - CLIMATE_SWINT_HORIZONTAL = 3, + CLIMATE_SWING_HORIZONTAL = 3, }; enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, @@ -80,25 +113,44 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_DRYING = 5, CLIMATE_ACTION_FAN = 6, }; +enum ClimatePreset : uint32_t { + 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, +}; +enum NumberMode : uint32_t { + NUMBER_MODE_AUTO = 0, + NUMBER_MODE_BOX = 1, + NUMBER_MODE_SLIDER = 2, +}; } // namespace enums class HelloRequest : public ProtoMessage { public: - std::string client_info{}; // NOLINT + 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; }; class HelloResponse : public ProtoMessage { public: - uint32_t api_version_major{0}; // NOLINT - uint32_t api_version_minor{0}; // NOLINT - std::string server_info{}; // NOLINT + uint32_t api_version_major{0}; + 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; @@ -106,18 +158,22 @@ class HelloResponse : public ProtoMessage { }; class ConnectRequest : public ProtoMessage { public: - std::string password{}; // NOLINT + 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; }; class ConnectResponse : public ProtoMessage { public: - bool invalid_password{false}; // NOLINT + 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; @@ -125,49 +181,64 @@ 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: }; class DeviceInfoResponse : public ProtoMessage { public: - bool uses_password{false}; // NOLINT - std::string name{}; // NOLINT - std::string mac_address{}; // NOLINT - std::string esphome_version{}; // NOLINT - std::string compilation_time{}; // NOLINT - std::string model{}; // NOLINT - bool has_deep_sleep{false}; // NOLINT + bool uses_password{false}; + std::string name{}; + std::string mac_address{}; + std::string esphome_version{}; + std::string compilation_time{}; + std::string model{}; + bool has_deep_sleep{false}; + std::string project_name{}; + std::string project_version{}; + uint32_t webserver_port{0}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -176,34 +247,45 @@ 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: }; class ListEntitiesBinarySensorResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - std::string device_class{}; // NOLINT - bool is_status_binary_sensor{false}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string device_class{}; + bool is_status_binary_sensor{false}; + bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -212,11 +294,13 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { }; class BinarySensorStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - bool missing_state{false}; // NOLINT + uint32_t key{0}; + 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; @@ -224,16 +308,21 @@ class BinarySensorStateResponse : public ProtoMessage { }; class ListEntitiesCoverResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - bool assumed_state{false}; // NOLINT - bool supports_position{false}; // NOLINT - bool supports_tilt{false}; // NOLINT - std::string device_class{}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + bool assumed_state{false}; + bool supports_position{false}; + bool supports_tilt{false}; + std::string device_class{}; + bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -242,13 +331,15 @@ class ListEntitiesCoverResponse : public ProtoMessage { }; class CoverStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - enums::LegacyCoverState legacy_state{}; // NOLINT - float position{0.0f}; // NOLINT - float tilt{0.0f}; // NOLINT - enums::CoverOperation current_operation{}; // NOLINT + uint32_t key{0}; + enums::LegacyCoverState legacy_state{}; + float position{0.0f}; + 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; @@ -256,16 +347,18 @@ class CoverStateResponse : public ProtoMessage { }; class CoverCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_legacy_command{false}; // NOLINT - enums::LegacyCoverCommand legacy_command{}; // NOLINT - bool has_position{false}; // NOLINT - float position{0.0f}; // NOLINT - bool has_tilt{false}; // NOLINT - float tilt{0.0f}; // NOLINT - bool stop{false}; // NOLINT + uint32_t key{0}; + bool has_legacy_command{false}; + enums::LegacyCoverCommand legacy_command{}; + bool has_position{false}; + float position{0.0f}; + bool has_tilt{false}; + 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; @@ -273,14 +366,21 @@ class CoverCommandRequest : public ProtoMessage { }; class ListEntitiesFanResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - bool supports_oscillation{false}; // NOLINT - bool supports_speed{false}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + bool supports_oscillation{false}; + bool supports_speed{false}; + bool supports_direction{false}; + int32_t supported_speed_count{0}; + bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -289,12 +389,16 @@ class ListEntitiesFanResponse : public ProtoMessage { }; class FanStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - bool oscillating{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT + uint32_t key{0}; + bool state{false}; + bool oscillating{false}; + enums::FanSpeed speed{}; + 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; @@ -302,15 +406,21 @@ class FanStateResponse : public ProtoMessage { }; class FanCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_state{false}; // NOLINT - bool state{false}; // NOLINT - bool has_speed{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT - bool has_oscillating{false}; // NOLINT - bool oscillating{false}; // NOLINT + uint32_t key{0}; + bool has_state{false}; + bool state{false}; + bool has_speed{false}; + enums::FanSpeed speed{}; + bool has_oscillating{false}; + bool oscillating{false}; + bool has_direction{false}; + enums::FanDirection direction{}; + 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; @@ -318,19 +428,25 @@ class FanCommandRequest : public ProtoMessage { }; class ListEntitiesLightResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - bool supports_brightness{false}; // NOLINT - bool supports_rgb{false}; // NOLINT - bool supports_white_value{false}; // NOLINT - bool supports_color_temperature{false}; // NOLINT - float min_mireds{0.0f}; // NOLINT - float max_mireds{0.0f}; // NOLINT - std::vector effects{}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + 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{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -339,17 +455,23 @@ class ListEntitiesLightResponse : public ProtoMessage { }; class LightStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - float brightness{0.0f}; // NOLINT - float red{0.0f}; // NOLINT - float green{0.0f}; // NOLINT - float blue{0.0f}; // NOLINT - float white{0.0f}; // NOLINT - float color_temperature{0.0f}; // NOLINT - std::string effect{}; // NOLINT + 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; @@ -358,27 +480,37 @@ class LightStateResponse : public ProtoMessage { }; class LightCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_state{false}; // NOLINT - bool state{false}; // NOLINT - bool has_brightness{false}; // NOLINT - float brightness{0.0f}; // NOLINT - bool has_rgb{false}; // NOLINT - float red{0.0f}; // NOLINT - float green{0.0f}; // NOLINT - float blue{0.0f}; // NOLINT - bool has_white{false}; // NOLINT - float white{0.0f}; // NOLINT - bool has_color_temperature{false}; // NOLINT - float color_temperature{0.0f}; // NOLINT - bool has_transition_length{false}; // NOLINT - uint32_t transition_length{0}; // NOLINT - bool has_flash_length{false}; // NOLINT - uint32_t flash_length{0}; // NOLINT - bool has_effect{false}; // NOLINT - std::string effect{}; // NOLINT + uint32_t key{0}; + bool has_state{false}; + 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}; + float blue{0.0f}; + bool has_white{false}; + 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}; + uint32_t flash_length{0}; + 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; @@ -387,16 +519,23 @@ class LightCommandRequest : public ProtoMessage { }; class ListEntitiesSensorResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - std::string icon{}; // NOLINT - std::string unit_of_measurement{}; // NOLINT - int32_t accuracy_decimals{0}; // NOLINT - bool force_update{false}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + std::string unit_of_measurement{}; + int32_t accuracy_decimals{0}; + bool force_update{false}; + std::string device_class{}; + enums::SensorStateClass state_class{}; + enums::SensorLastResetType legacy_last_reset_type{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -405,11 +544,13 @@ class ListEntitiesSensorResponse : public ProtoMessage { }; class SensorStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - float state{0.0f}; // NOLINT - bool missing_state{false}; // NOLINT + 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; @@ -417,14 +558,18 @@ class SensorStateResponse : public ProtoMessage { }; class ListEntitiesSwitchResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - std::string icon{}; // NOLINT - bool assumed_state{false}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool assumed_state{false}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -433,10 +578,12 @@ class ListEntitiesSwitchResponse : public ProtoMessage { }; class SwitchStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT + 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; @@ -444,10 +591,12 @@ class SwitchStateResponse : public ProtoMessage { }; class SwitchCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT + 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; @@ -455,25 +604,32 @@ class SwitchCommandRequest : public ProtoMessage { }; class ListEntitiesTextSensorResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - std::string icon{}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class TextSensorStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - std::string state{}; // NOLINT - bool missing_state{false}; // NOLINT + 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; @@ -482,22 +638,25 @@ class TextSensorStateResponse : public ProtoMessage { }; class SubscribeLogsRequest : public ProtoMessage { public: - enums::LogLevel level{}; // NOLINT - bool dump_config{false}; // NOLINT + 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; }; class SubscribeLogsResponse : public ProtoMessage { public: - enums::LogLevel level{}; // NOLINT - std::string tag{}; // NOLINT - std::string message{}; // NOLINT - bool send_failed{false}; // NOLINT + enums::LogLevel level{}; + 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; @@ -506,29 +665,35 @@ 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: }; class HomeassistantServiceMap : public ProtoMessage { public: - std::string key{}; // NOLINT - std::string value{}; // NOLINT + 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; }; class HomeassistantServiceResponse : public ProtoMessage { public: - std::string service{}; // NOLINT - std::vector data{}; // NOLINT - std::vector data_template{}; // NOLINT - std::vector variables{}; // NOLINT - bool is_event{false}; // NOLINT + std::string service{}; + std::vector data{}; + std::vector data_template{}; + 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; @@ -537,25 +702,33 @@ 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: }; class SubscribeHomeAssistantStateResponse : public ProtoMessage { public: - std::string entity_id{}; // NOLINT + 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; }; class HomeAssistantStateResponse : public ProtoMessage { public: - std::string entity_id{}; // NOLINT - std::string state{}; // NOLINT + std::string entity_id{}; + 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; @@ -563,25 +736,31 @@ 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: }; class GetTimeResponse : public ProtoMessage { public: - uint32_t epoch_seconds{0}; // NOLINT + uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; class ListEntitiesServicesArgument : public ProtoMessage { public: - std::string name{}; // NOLINT - enums::ServiceArgType type{}; // NOLINT + 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; @@ -589,11 +768,13 @@ class ListEntitiesServicesArgument : public ProtoMessage { }; class ListEntitiesServicesResponse : public ProtoMessage { public: - std::string name{}; // NOLINT - uint32_t key{0}; // NOLINT - std::vector args{}; // NOLINT + std::string name{}; + 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; @@ -601,17 +782,19 @@ class ListEntitiesServicesResponse : public ProtoMessage { }; class ExecuteServiceArgument : public ProtoMessage { public: - bool bool_{false}; // NOLINT - int32_t legacy_int{0}; // NOLINT - float float_{0.0f}; // NOLINT - std::string string_{}; // NOLINT - int32_t int_{0}; // NOLINT - std::vector bool_array{}; // NOLINT - std::vector int_array{}; // NOLINT - std::vector float_array{}; // NOLINT - std::vector string_array{}; // NOLINT + bool bool_{false}; + int32_t legacy_int{0}; + float float_{0.0f}; + std::string string_{}; + int32_t int_{0}; + std::vector bool_array{}; + std::vector int_array{}; + 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; @@ -620,10 +803,12 @@ class ExecuteServiceArgument : public ProtoMessage { }; class ExecuteServiceRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - std::vector args{}; // NOLINT + 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; @@ -631,24 +816,32 @@ class ExecuteServiceRequest : public ProtoMessage { }; class ListEntitiesCameraResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class CameraImageResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - std::string data{}; // NOLINT - bool done{false}; // NOLINT + uint32_t key{0}; + 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; @@ -657,32 +850,42 @@ class CameraImageResponse : public ProtoMessage { }; class CameraImageRequest : public ProtoMessage { public: - bool single{false}; // NOLINT - bool stream{false}; // NOLINT + 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; }; class ListEntitiesClimateResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - bool supports_current_temperature{false}; // NOLINT - bool supports_two_point_target_temperature{false}; // NOLINT - std::vector supported_modes{}; // NOLINT - float visual_min_temperature{0.0f}; // NOLINT - float visual_max_temperature{0.0f}; // NOLINT - float visual_temperature_step{0.0f}; // NOLINT - bool supports_away{false}; // NOLINT - bool supports_action{false}; // NOLINT - std::vector supported_fan_modes{}; // NOLINT - std::vector supported_swing_modes{}; // NOLINT + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + bool supports_current_temperature{false}; + bool supports_two_point_target_temperature{false}; + std::vector supported_modes{}; + float visual_min_temperature{0.0f}; + float visual_max_temperature{0.0f}; + float visual_temperature_step{0.0f}; + 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{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -691,47 +894,191 @@ class ListEntitiesClimateResponse : public ProtoMessage { }; class ClimateStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - enums::ClimateMode mode{}; // NOLINT - float current_temperature{0.0f}; // NOLINT - float target_temperature{0.0f}; // NOLINT - float target_temperature_low{0.0f}; // NOLINT - float target_temperature_high{0.0f}; // NOLINT - bool away{false}; // NOLINT - enums::ClimateAction action{}; // NOLINT - enums::ClimateFanMode fan_mode{}; // NOLINT - enums::ClimateSwingMode swing_mode{}; // NOLINT + uint32_t key{0}; + enums::ClimateMode mode{}; + float current_temperature{0.0f}; + float target_temperature{0.0f}; + float target_temperature_low{0.0f}; + float target_temperature_high{0.0f}; + bool legacy_away{false}; + enums::ClimateAction action{}; + enums::ClimateFanMode fan_mode{}; + enums::ClimateSwingMode swing_mode{}; + std::string custom_fan_mode{}; + 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; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ClimateCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_mode{false}; // NOLINT - enums::ClimateMode mode{}; // NOLINT - bool has_target_temperature{false}; // NOLINT - float target_temperature{0.0f}; // NOLINT - bool has_target_temperature_low{false}; // NOLINT - float target_temperature_low{0.0f}; // NOLINT - bool has_target_temperature_high{false}; // NOLINT - float target_temperature_high{0.0f}; // NOLINT - bool has_away{false}; // NOLINT - bool away{false}; // NOLINT - bool has_fan_mode{false}; // NOLINT - enums::ClimateFanMode fan_mode{}; // NOLINT - bool has_swing_mode{false}; // NOLINT - enums::ClimateSwingMode swing_mode{}; // NOLINT + uint32_t key{0}; + bool has_mode{false}; + enums::ClimateMode mode{}; + bool has_target_temperature{false}; + float target_temperature{0.0f}; + bool has_target_temperature_low{false}; + float target_temperature_low{0.0f}; + bool has_target_temperature_high{false}; + float target_temperature_high{0.0f}; + bool has_legacy_away{false}; + bool legacy_away{false}; + bool has_fan_mode{false}; + enums::ClimateFanMode fan_mode{}; + bool has_swing_mode{false}; + enums::ClimateSwingMode swing_mode{}; + bool has_custom_fan_mode{false}; + std::string custom_fan_mode{}; + bool has_preset{false}; + enums::ClimatePreset preset{}; + 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}; + enums::EntityCategory entity_category{}; + std::string unit_of_measurement{}; + enums::NumberMode mode{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class 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}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class 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; +}; +class ListEntitiesButtonResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + std::string device_class{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ButtonCommandRequest : public ProtoMessage { + public: + uint32_t key{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; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 06345296a7..567fbf02c9 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -6,61 +6,85 @@ namespace esphome { namespace api { -static const char *TAG = "api.service"; +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,157 @@ 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 +#ifdef USE_BUTTON +bool APIServerConnectionBase::send_list_entities_button_response(const ListEntitiesButtonResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_button_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 61); +} +#endif +#ifdef USE_BUTTON +#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 +388,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 +399,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 +410,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 +421,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 +431,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 +486,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 +497,43 @@ 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; + } + case 62: { +#ifdef USE_BUTTON + ButtonCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); +#endif + this->on_button_command_request(msg); #endif break; } @@ -547,6 +732,45 @@ 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 +#ifdef USE_BUTTON +void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->button_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..50b08d3ec4 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -111,6 +111,30 @@ 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 +#ifdef USE_BUTTON + bool send_list_entities_button_response(const ListEntitiesButtonResponse &msg); +#endif +#ifdef USE_BUTTON + virtual void on_button_command_request(const ButtonCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -147,6 +171,15 @@ 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 +#ifdef USE_BUTTON + virtual void button_command(const ButtonCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -179,6 +212,15 @@ 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 +#ifdef USE_BUTTON + void on_button_command_request(const ButtonCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 25ae9a98a3..25081a809a 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" @@ -15,30 +18,55 @@ namespace esphome { namespace api { -static const char *TAG = "api"; +static const char *const TAG = "api"; // APIServer 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); } @@ -49,31 +77,42 @@ void APIServer::setup() { this->last_connected_ = millis(); #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); - }); + if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { + 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,16 +218,16 @@ 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 #ifdef USE_TEXT_SENSOR -void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string 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,25 +236,45 @@ void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string 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; +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); } } APIServer::APIServer() { global_api_server = this; } -void APIServer::subscribe_home_assistant_state(std::string entity_id, std::function f) { +void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { this->state_subs_.push_back(HomeAssistantStateSubscription{ .entity_id = std::move(entity_id), + .attribute = std::move(attribute), .callback = std::move(f), }); } @@ -221,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(); } @@ -229,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 db826c55c2..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; @@ -56,10 +56,16 @@ class APIServer : public Component, public Controller { void on_switch_update(switch_::Switch *obj, bool state) override; #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; #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); } @@ -71,25 +77,31 @@ class APIServer : public Component, public Controller { struct HomeAssistantStateSubscription { std::string entity_id; + optional attribute; std::function callback; }; - void subscribe_home_assistant_state(std::string entity_id, std::function f); + void subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f); const std::vector &get_state_subs() const; const std::vector &get_user_services() const { return this->user_services_; } 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; +extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class APIConnectedCondition : public Condition { public: diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py new file mode 100644 index 0000000000..b2920f239b --- /dev/null +++ b/esphome/components/api/client.py @@ -0,0 +1,72 @@ +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( + 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 aac91244b6..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,17 +72,17 @@ 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); } - /** Subscribe to the state of an entity from Home Assistant. + /** Subscribe to the state (or attribute state) of an entity from Home Assistant. * * Usage: * * ```cpp * void setup() override { - * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); + * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature"); * } * * void on_state_changed(std::string state) { @@ -93,17 +93,19 @@ class CustomAPIDevice { * @tparam T The class type creating the service, automatically deduced from the function pointer. * @param callback The member function to call when the entity state changes. * @param entity_id The entity_id to track. + * @param attribute The entity state attribute to track. */ template - void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id) { + void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, + const std::string &attribute = "") { auto f = std::bind(callback, (T *) this, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, f); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), f); } - /** Subscribe to the state of an entity from Home Assistant. + /** Subscribe to the state (or attribute state) of an entity from Home Assistant. * * Usage: - * + *å * ```cpp * void setup() override { * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); @@ -117,11 +119,13 @@ class CustomAPIDevice { * @tparam T The class type creating the service, automatically deduced from the function pointer. * @param callback The member function to call when the entity state changes. * @param entity_id The entity_id to track. + * @param attribute The entity state attribute to track. */ template - void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id) { + void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, + const std::string &attribute = "") { auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, f); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), f); } /** Call a Home Assistant service from ESPHome. diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d68dac3b61..26269bcae4 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -8,6 +8,18 @@ namespace esphome { namespace api { +template class TemplatableStringValue : public TemplatableValue { + public: + TemplatableStringValue() : TemplatableValue() {} + + template::value, int> = 0> + TemplatableStringValue(F value) : TemplatableValue(value) {} + + template::value, int> = 0> + TemplatableStringValue(F f) + : TemplatableValue([f](X... x) -> std::string { return to_string(f(x...)); }) {} +}; + template class TemplatableKeyValuePair { public: template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} @@ -19,7 +31,8 @@ template class HomeAssistantServiceCallAction : public Action void set_service(T service) { this->service_ = service; } + template void add_data(std::string key, T value) { this->data_.push_back(TemplatableKeyValuePair(key, value)); } @@ -29,6 +42,7 @@ template class HomeAssistantServiceCallAction : public Action void add_variable(std::string key, T value) { this->variables_.push_back(TemplatableKeyValuePair(key, value)); } + void play(Ts... x) override { HomeassistantServiceResponse resp; resp.service = this->service_.value(x...); @@ -57,6 +71,7 @@ template class HomeAssistantServiceCallAction : public Action service_{}; std::vector> data_; std::vector> data_template_; std::vector> variables_; diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index d4245136ae..cb97df8ca1 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -27,6 +27,9 @@ bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->clie #ifdef USE_SWITCH bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); } #endif +#ifdef USE_BUTTON +bool ListEntitiesIterator::on_button(button::Button *button) { return this->client_->send_button_info(button); } +#endif #ifdef USE_TEXT_SENSOR bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { return this->client_->send_text_sensor_info(text_sensor); @@ -51,5 +54,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..714edaa91f 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -30,6 +30,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_SWITCH bool on_switch(switch_::Switch *a_switch) override; #endif +#ifdef USE_BUTTON + bool on_button(button::Button *button) override; +#endif #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; #endif @@ -39,6 +42,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 3d2f669f54..0ba277d90a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace api { -static const char *TAG = "api.proto"; +static const char *const TAG = "api.proto"; void ProtoMessage::decode(const uint8_t *buffer, size_t length) { uint32_t i = 0; @@ -62,8 +62,7 @@ void ProtoMessage::decode(const uint8_t *buffer, size_t length) { error = true; break; } - uint32_t val = (uint32_t(buffer[i]) << 0) | (uint32_t(buffer[i + 1]) << 8) | (uint32_t(buffer[i + 2]) << 16) | - (uint32_t(buffer[i + 3]) << 24); + uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]); if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %u with value %u!", field_id, val); } @@ -81,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..d3f2d3aa45 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -31,11 +31,20 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_SWITCH bool on_switch(switch_::Switch *a_switch) override; #endif +#ifdef USE_BUTTON + bool on_button(button::Button *button) override { return true; }; +#endif #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; #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/user_services.h b/esphome/components/api/user_services.h index 3094ba397c..1f9ffc5914 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "api_pb2.h" @@ -20,8 +22,8 @@ template enums::ServiceArgType to_service_arg_type(); template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(const std::string &name, const std::array &arg_names) - : name_(name), arg_names_(arg_names) { + UserServiceBase(std::string name, const std::array &arg_names) + : name_(std::move(name)), arg_names_(arg_names) { this->key_ = fnv1_hash(this->name_); } diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index f929db5d6a..f5fd752101 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -116,6 +116,21 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_BUTTON + case IteratorState::BUTTON: + if (this->at_ >= App.get_buttons().size()) { + advance_platform = true; + } else { + auto *button = App.get_buttons()[this->at_]; + if (button->is_internal()) { + success = true; + break; + } else { + success = this->on_button(button); + } + } + break; +#endif #ifdef USE_TEXT_SENSOR case IteratorState::TEXT_SENSOR: if (this->at_ >= App.get_text_sensors().size()) { @@ -167,6 +182,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..7849b3e028 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -38,6 +38,9 @@ class ComponentIterator { #ifdef USE_SWITCH virtual bool on_switch(switch_::Switch *a_switch) = 0; #endif +#ifdef USE_BUTTON + virtual bool on_button(button::Button *button) = 0; +#endif #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif @@ -47,6 +50,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(); @@ -72,6 +81,9 @@ class ComponentIterator { #ifdef USE_SWITCH SWITCH, #endif +#ifdef USE_BUTTON + BUTTON, +#endif #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif @@ -81,6 +93,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/__init__.py b/esphome/components/as3935/__init__.py index de25060623..0951d01e68 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -1,40 +1,48 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \ - CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \ - CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAPACITANCE -from esphome.core import coroutine +from esphome.const import ( + CONF_INDOOR, + CONF_WATCHDOG_THRESHOLD, + CONF_NOISE_LEVEL, + CONF_SPIKE_REJECTION, + CONF_LIGHTNING_THRESHOLD, + CONF_MASK_DISTURBER, + CONF_DIV_RATIO, + CONF_CAPACITANCE, +) -AUTO_LOAD = ['sensor', 'binary_sensor'] +AUTO_LOAD = ["sensor", "binary_sensor"] MULTI_CONF = True -CONF_AS3935_ID = 'as3935_id' +CONF_AS3935_ID = "as3935_id" -as3935_ns = cg.esphome_ns.namespace('as3935') -AS3935 = as3935_ns.class_('AS3935Component', cg.Component) +as3935_ns = cg.esphome_ns.namespace("as3935") +AS3935 = as3935_ns.class_("AS3935Component", cg.Component) -CONF_IRQ_PIN = 'irq_pin' -AS3935_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(AS3935), - cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, - - cv.Optional(CONF_INDOOR, default=True): cv.boolean, - cv.Optional(CONF_NOISE_LEVEL, default=2): cv.int_range(min=1, max=7), - cv.Optional(CONF_WATCHDOG_THRESHOLD, default=2): cv.int_range(min=1, max=10), - cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11), - cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of(1, 5, 9, 16, int=True), - cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean, - cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 22, 64, 128, int=True), - cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15), -}) +CONF_IRQ_PIN = "irq_pin" +AS3935_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(AS3935), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_INDOOR, default=True): cv.boolean, + cv.Optional(CONF_NOISE_LEVEL, default=2): cv.int_range(min=1, max=7), + cv.Optional(CONF_WATCHDOG_THRESHOLD, default=2): cv.int_range(min=1, max=10), + cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11), + cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of( + 1, 5, 9, 16, int=True + ), + cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean, + cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 32, 64, 128, int=True), + cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15), + } +) -@coroutine -def setup_as3935(var, config): - yield cg.register_component(var, config) +async def setup_as3935(var, config): + await cg.register_component(var, config) - irq_pin = yield cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) cg.add(var.set_irq_pin(irq_pin)) cg.add(var.set_indoor(config[CONF_INDOOR])) cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL])) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index f8272e6036..1cc400bb7b 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace as3935 { -static const char *TAG = "as3935"; +static const char *const TAG = "as3935"; void AS3935Component::setup() { ESP_LOGCONFIG(TAG, "Setting up AS3935..."); @@ -26,6 +26,9 @@ void AS3935Component::setup() { void AS3935Component::dump_config() { ESP_LOGCONFIG(TAG, "AS3935:"); LOG_PIN(" Interrupt Pin: ", this->irq_pin_); + LOG_BINARY_SENSOR(" ", "Thunder alert", this->thunder_alert_binary_sensor_); + LOG_SENSOR(" ", "Distance", this->distance_sensor_); + LOG_SENSOR(" ", "Lightning energy", this->energy_sensor_); } float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } 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/binary_sensor.py b/esphome/components/as3935/binary_sensor.py index 3748c3484a..11b2ac812c 100644 --- a/esphome/components/as3935/binary_sensor.py +++ b/esphome/components/as3935/binary_sensor.py @@ -3,14 +3,16 @@ import esphome.config_validation as cv from esphome.components import binary_sensor from . import AS3935, CONF_AS3935_ID -DEPENDENCIES = ['as3935'] +DEPENDENCIES = ["as3935"] -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), + } +) -def to_code(config): - hub = yield cg.get_variable(config[CONF_AS3935_ID]) - var = yield binary_sensor.new_binary_sensor(config) +async def to_code(config): + hub = await cg.get_variable(config[CONF_AS3935_ID]) + var = await binary_sensor.new_binary_sensor(config) cg.add(hub.set_thunder_alert_binary_sensor(var)) diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 3374ada6a8..271a29e0fc 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -1,30 +1,45 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_DISTANCE, CONF_LIGHTNING_ENERGY, \ - UNIT_KILOMETER, UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH +from esphome.const import ( + CONF_DISTANCE, + CONF_LIGHTNING_ENERGY, + STATE_CLASS_NONE, + UNIT_KILOMETER, + ICON_SIGNAL_DISTANCE_VARIANT, + ICON_FLASH, +) from . import AS3935, CONF_AS3935_ID -DEPENDENCIES = ['as3935'] +DEPENDENCIES = ["as3935"] -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), - cv.Optional(CONF_LIGHTNING_ENERGY): - sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 1), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + 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( + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, + ), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): - hub = yield cg.get_variable(config[CONF_AS3935_ID]) +async def to_code(config): + hub = await cg.get_variable(config[CONF_AS3935_ID]) if CONF_DISTANCE in config: conf = config[CONF_DISTANCE] - distance_sensor = yield sensor.new_sensor(conf) + distance_sensor = await sensor.new_sensor(conf) cg.add(hub.set_distance_sensor(distance_sensor)) if CONF_LIGHTNING_ENERGY in config: conf = config[CONF_LIGHTNING_ENERGY] - lightning_energy_sensor = yield sensor.new_sensor(conf) - cg.add(hub.set_distance_sensor(lightning_energy_sensor)) + lightning_energy_sensor = await sensor.new_sensor(conf) + cg.add(hub.set_energy_sensor(lightning_energy_sensor)) diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py index e22937ab81..aa741b2ea6 100644 --- a/esphome/components/as3935_i2c/__init__.py +++ b/esphome/components/as3935_i2c/__init__.py @@ -3,18 +3,24 @@ import esphome.config_validation as cv from esphome.components import as3935, i2c from esphome.const import CONF_ID -AUTO_LOAD = ['as3935'] -DEPENDENCIES = ['i2c'] +AUTO_LOAD = ["as3935"] +DEPENDENCIES = ["i2c"] -as3935_i2c_ns = cg.esphome_ns.namespace('as3935_i2c') -I2CAS3935 = as3935_i2c_ns.class_('I2CAS3935Component', as3935.AS3935, i2c.I2CDevice) +as3935_i2c_ns = cg.esphome_ns.namespace("as3935_i2c") +I2CAS3935 = as3935_i2c_ns.class_("I2CAS3935Component", as3935.AS3935, i2c.I2CDevice) -CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(I2CAS3935), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x03))) +CONFIG_SCHEMA = cv.All( + as3935.AS3935_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2CAS3935), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x03)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield as3935.setup_as3935(var, config) - yield i2c.register_i2c_device(var, config) + await as3935.setup_as3935(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp index a522116815..3a7fa7bf84 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.cpp +++ b/esphome/components/as3935_i2c/as3935_i2c.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace as3935_i2c { -static const char *TAG = "as3935_i2c"; +static const char *const TAG = "as3935_i2c"; void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) { uint8_t write_reg; @@ -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/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py index fa27c2b0f5..849539f092 100644 --- a/esphome/components/as3935_spi/__init__.py +++ b/esphome/components/as3935_spi/__init__.py @@ -3,18 +3,24 @@ import esphome.config_validation as cv from esphome.components import as3935, spi from esphome.const import CONF_ID -AUTO_LOAD = ['as3935'] -DEPENDENCIES = ['spi'] +AUTO_LOAD = ["as3935"] +DEPENDENCIES = ["spi"] -as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi') -SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice) +as3935_spi_ns = cg.esphome_ns.namespace("as3935_spi") +SPIAS3935 = as3935_spi_ns.class_("SPIAS3935Component", as3935.AS3935, spi.SPIDevice) -CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SPIAS3935) -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA)) +CONFIG_SCHEMA = cv.All( + as3935.AS3935_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPIAS3935), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=True)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield as3935.setup_as3935(var, config) - yield spi.register_spi_device(var, config) + await as3935.setup_as3935(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp index 73752a8ee0..3a517df56d 100644 --- a/esphome/components/as3935_spi/as3935_spi.cpp +++ b/esphome/components/as3935_spi/as3935_spi.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace as3935_spi { -static const char *TAG = "as3935_spi"; +static const char *const TAG = "as3935_spi"; void SPIAS3935Component::setup() { ESP_LOGI(TAG, "SPIAS3935Component setup started!"); @@ -33,7 +33,7 @@ void SPIAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t SPIAS3935Component::read_register(uint8_t reg) { uint8_t value = 0; this->enable(); - this->write_byte(reg |= SPI_READ_M); + this->write_byte(reg | SPI_READ_M); value = this->read_byte(); // According to datsheet, the chip select must be written HIGH, LOW, HIGH // to correctly end the READ command. diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index cf9d2f1585..8789448792 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -1,13 +1,21 @@ # 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) -def to_code(config): +async def to_code(config): if CORE.is_esp32: - # https://github.com/OttoWinter/AsyncTCP/blob/master/library.json - cg.add_library('AsyncTCP-esphome', '1.1.1') + # https://github.com/esphome/AsyncTCP/blob/master/library.json + 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.2') + cg.add_library("ottowinter/ESPAsyncTCP-esphome", "1.2.3") diff --git a/esphome/py_compat.py b/esphome/components/atc_mithermometer/__init__.py similarity index 100% rename from esphome/py_compat.py rename to esphome/components/atc_mithermometer/__init__.py diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp new file mode 100644 index 0000000000..42c30598ad --- /dev/null +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -0,0 +1,131 @@ +#include "atc_mithermometer.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace atc_mithermometer { + +static const char *const TAG = "atc_mithermometer"; + +void ATCMiThermometer::dump_config() { + ESP_LOGCONFIG(TAG, "ATC 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 ATCMiThermometer::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 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."); + return {}; + } + + auto raw = service_data.data; + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[12]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + return {}; + } + last_frame_count = raw[12]; + + return 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 + // Byte 9 Battery in percent + // Byte 10-11 Battery in mV uint16_t + // Byte 12 frame packet counter + + 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; + } + + // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + const int16_t temperature = uint16_t(data[7]) | (uint16_t(data[6]) << 8); + result.temperature = temperature / 10.0f; + + // humidity, 1 byte, 8-bit unsigned integer, 1.0 % + result.humidity = data[8]; + + // battery, 1 byte, 8-bit unsigned integer, 1.0 % + result.battery_level = data[9]; + + // battery, 2 bytes, 16-bit unsigned integer, 0.001 V + const int16_t battery_voltage = uint16_t(data[11]) | (uint16_t(data[10]) << 8); + result.battery_voltage = battery_voltage / 1.0e3f; + + return true; +} + +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; + } + + ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f °C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.0f %%", *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 atc_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h new file mode 100644 index 0000000000..ca079bf8c1 --- /dev/null +++ b/esphome/components/atc_mithermometer/atc_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 atc_mithermometer { + +struct ParseResult { + optional temperature; + optional humidity; + optional battery_level; + optional battery_voltage; + int raw_offset; +}; + +class ATCMiThermometer : 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 atc_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py new file mode 100644 index 0000000000..bde83c28b6 --- /dev/null +++ b/esphome/components/atc_mithermometer/sensor.py @@ -0,0 +1,87 @@ +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, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@ahpohl"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +atc_mithermometer_ns = cg.esphome_ns.namespace("atc_mithermometer") +ATCMiThermometer = atc_mithermometer_ns.class_( + "ATCMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ATCMiThermometer), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .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/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 85e38fce3e..e4b8448da6 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace atm90e32 { -static const char *TAG = "atm90e32"; +static const char *const TAG = "atm90e32"; void ATM90E32Component::update() { if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) { @@ -58,6 +58,24 @@ void ATM90E32Component::update() { if (this->phase_[2].power_factor_sensor_ != nullptr) { this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_()); } + if (this->phase_[0].forward_active_energy_sensor_ != nullptr) { + this->phase_[0].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_a_()); + } + if (this->phase_[1].forward_active_energy_sensor_ != nullptr) { + this->phase_[1].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_b_()); + } + if (this->phase_[2].forward_active_energy_sensor_ != nullptr) { + this->phase_[2].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_c_()); + } + if (this->phase_[0].reverse_active_energy_sensor_ != nullptr) { + this->phase_[0].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_a_()); + } + if (this->phase_[1].reverse_active_energy_sensor_ != nullptr) { + this->phase_[1].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_b_()); + } + if (this->phase_[2].reverse_active_energy_sensor_ != nullptr) { + this->phase_[2].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_c_()); + } if (this->freq_sensor_ != nullptr) { this->freq_sensor_->publish_state(this->get_frequency_()); } @@ -119,16 +137,22 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[0].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[0].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[1].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[1].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[2].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[2].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } @@ -239,6 +263,60 @@ float ATM90E32Component::get_power_factor_c_() { int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC); return (float) pf / 1000; } +float ATM90E32Component::get_forward_active_energy_a_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA); + if ((UINT32_MAX - this->phase_[0].cumulative_forward_active_energy_) > val) { + this->phase_[0].cumulative_forward_active_energy_ += val; + } else { + this->phase_[0].cumulative_forward_active_energy_ = val; + } + return ((float) this->phase_[0].cumulative_forward_active_energy_ * 10 / 3200); +} +float ATM90E32Component::get_forward_active_energy_b_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB); + if (UINT32_MAX - this->phase_[1].cumulative_forward_active_energy_ > val) { + this->phase_[1].cumulative_forward_active_energy_ += val; + } else { + this->phase_[1].cumulative_forward_active_energy_ = val; + } + return ((float) this->phase_[1].cumulative_forward_active_energy_ * 10 / 3200); +} +float ATM90E32Component::get_forward_active_energy_c_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC); + if (UINT32_MAX - this->phase_[2].cumulative_forward_active_energy_ > val) { + this->phase_[2].cumulative_forward_active_energy_ += val; + } else { + this->phase_[2].cumulative_forward_active_energy_ = val; + } + return ((float) this->phase_[2].cumulative_forward_active_energy_ * 10 / 3200); +} +float ATM90E32Component::get_reverse_active_energy_a_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA); + if (UINT32_MAX - this->phase_[0].cumulative_reverse_active_energy_ > val) { + this->phase_[0].cumulative_reverse_active_energy_ += val; + } else { + this->phase_[0].cumulative_reverse_active_energy_ = val; + } + return ((float) this->phase_[0].cumulative_reverse_active_energy_ * 10 / 3200); +} +float ATM90E32Component::get_reverse_active_energy_b_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB); + if (UINT32_MAX - this->phase_[1].cumulative_reverse_active_energy_ > val) { + this->phase_[1].cumulative_reverse_active_energy_ += val; + } else { + this->phase_[1].cumulative_reverse_active_energy_ = val; + } + return ((float) this->phase_[1].cumulative_reverse_active_energy_ * 10 / 3200); +} +float ATM90E32Component::get_reverse_active_energy_c_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC); + if (UINT32_MAX - this->phase_[2].cumulative_reverse_active_energy_ > val) { + this->phase_[2].cumulative_reverse_active_energy_ += val; + } else { + this->phase_[2].cumulative_reverse_active_energy_ = val; + } + return ((float) this->phase_[2].cumulative_reverse_active_energy_ * 10 / 3200); +} float ATM90E32Component::get_frequency_() { uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); return (float) freq / 100; diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index eb5de3878c..c9662df26e 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -20,6 +20,12 @@ class ATM90E32Component : public PollingComponent, void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; } + void set_forward_active_energy_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].forward_active_energy_sensor_ = obj; + } + void set_reverse_active_energy_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].reverse_active_energy_sensor_ = obj; + } void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } @@ -52,6 +58,12 @@ class ATM90E32Component : public PollingComponent, float get_power_factor_a_(); float get_power_factor_b_(); float get_power_factor_c_(); + float get_forward_active_energy_a_(); + float get_forward_active_energy_b_(); + float get_forward_active_energy_c_(); + float get_reverse_active_energy_a_(); + float get_reverse_active_energy_b_(); + float get_reverse_active_energy_c_(); float get_frequency_(); float get_chip_temperature_(); @@ -63,6 +75,10 @@ class ATM90E32Component : public PollingComponent, sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *reactive_power_sensor_{nullptr}; sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *forward_active_energy_sensor_{nullptr}; + sensor::Sensor *reverse_active_energy_sensor_{nullptr}; + uint32_t cumulative_forward_active_energy_{0}; + uint32_t cumulative_reverse_active_energy_{0}; } 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 fc526dfbc0..9c876bb62c 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -1,67 +1,149 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import \ - CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_POWER_FACTOR, CONF_FREQUENCY, \ - ICON_FLASH, ICON_LIGHTBULB, ICON_CURRENT_AC, ICON_THERMOMETER, \ - UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE +from esphome.const import ( + CONF_ID, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + CONF_POWER_FACTOR, + CONF_FREQUENCY, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_LIGHTBULB, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_CELSIUS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT_HOURS, +) -CONF_PHASE_A = 'phase_a' -CONF_PHASE_B = 'phase_b' -CONF_PHASE_C = 'phase_c' +CONF_PHASE_A = "phase_a" +CONF_PHASE_B = "phase_b" +CONF_PHASE_C = "phase_c" -CONF_REACTIVE_POWER = 'reactive_power' -CONF_LINE_FREQUENCY = 'line_frequency' -CONF_CHIP_TEMPERATURE = 'chip_temperature' -CONF_GAIN_PGA = 'gain_pga' -CONF_CURRENT_PHASES = 'current_phases' -CONF_GAIN_VOLTAGE = 'gain_voltage' -CONF_GAIN_CT = 'gain_ct' +CONF_LINE_FREQUENCY = "line_frequency" +CONF_CHIP_TEMPERATURE = "chip_temperature" +CONF_GAIN_PGA = "gain_pga" +CONF_CURRENT_PHASES = "current_phases" +CONF_GAIN_VOLTAGE = "gain_voltage" +CONF_GAIN_CT = "gain_ct" LINE_FREQS = { - '50HZ': 50, - '60HZ': 60, + "50HZ": 50, + "60HZ": 60, } CURRENT_PHASES = { - '2': 2, - '3': 3, + "2": 2, + "3": 3, } PGA_GAINS = { - '1X': 0x0, - '2X': 0x15, - '4X': 0x2A, + "1X": 0x0, + "2X": 0x15, + "4X": 0x2A, } -atm90e32_ns = cg.esphome_ns.namespace('atm90e32') -ATM90E32Component = atm90e32_ns.class_('ATM90E32Component', cg.PollingComponent, spi.SPIDevice) +atm90e32_ns = cg.esphome_ns.namespace("atm90e32") +ATM90E32Component = atm90e32_ns.class_( + "ATM90E32Component", cg.PollingComponent, spi.SPIDevice +) -ATM90E32_PHASE_SCHEMA = cv.Schema({ - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 2), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), - cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(UNIT_VOLT_AMPS_REACTIVE, - ICON_LIGHTBULB, 2), - cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), - cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, - cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, -}) +ATM90E32_PHASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + icon=ICON_LIGHTBULB, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, + cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, + } +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(ATM90E32Component), - cv.Optional(CONF_PHASE_A): ATM90E32_PHASE_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), - cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), - cv.Optional(CONF_CURRENT_PHASES, default='3'): cv.enum(CURRENT_PHASES, upper=True), - cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ATM90E32Component), + cv.Optional(CONF_PHASE_A): ATM90E32_PHASE_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_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_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( + CURRENT_PHASES, upper=True + ), + cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]): if phase not in config: @@ -70,25 +152,31 @@ def to_code(config): cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) if CONF_VOLTAGE in conf: - sens = yield sensor.new_sensor(conf[CONF_VOLTAGE]) + sens = await sensor.new_sensor(conf[CONF_VOLTAGE]) cg.add(var.set_voltage_sensor(i, sens)) if CONF_CURRENT in conf: - sens = yield sensor.new_sensor(conf[CONF_CURRENT]) + sens = await sensor.new_sensor(conf[CONF_CURRENT]) cg.add(var.set_current_sensor(i, sens)) if CONF_POWER in conf: - sens = yield sensor.new_sensor(conf[CONF_POWER]) + sens = await sensor.new_sensor(conf[CONF_POWER]) cg.add(var.set_power_sensor(i, sens)) if CONF_REACTIVE_POWER in conf: - sens = yield sensor.new_sensor(conf[CONF_REACTIVE_POWER]) + sens = await sensor.new_sensor(conf[CONF_REACTIVE_POWER]) cg.add(var.set_reactive_power_sensor(i, sens)) if CONF_POWER_FACTOR in conf: - sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR]) + sens = await sensor.new_sensor(conf[CONF_POWER_FACTOR]) cg.add(var.set_power_factor_sensor(i, sens)) + if CONF_FORWARD_ACTIVE_ENERGY in conf: + sens = await sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY]) + cg.add(var.set_forward_active_energy_sensor(i, sens)) + if CONF_REVERSE_ACTIVE_ENERGY in conf: + sens = await sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY]) + cg.add(var.set_reverse_active_energy_sensor(i, sens)) if CONF_FREQUENCY in config: - sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_freq_sensor(sens)) if CONF_CHIP_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) diff --git a/esphome/components/b_parasite/__init__.py b/esphome/components/b_parasite/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp new file mode 100644 index 0000000000..ee12226977 --- /dev/null +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -0,0 +1,102 @@ +#include "b_parasite.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace b_parasite { + +static const char *const TAG = "b_parasite"; + +void BParasite::dump_config() { + ESP_LOGCONFIG(TAG, "b_parasite"); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + 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) { + if (device.address_uint64() != 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()); + const auto &service_datas = device.get_service_datas(); + if (service_datas.size() != 1) { + ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size()); + return false; + } + const auto &service_data = service_datas[0]; + + ESP_LOGVV(TAG, "Service data:"); + for (const uint8_t byte : service_data.data) { + ESP_LOGVV(TAG, "0x%02x", byte); + } + + 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) { + ESP_LOGVV(TAG, "Skipping already processed counter (%u)", counter); + return false; + } + + // Battery voltage in millivolts. + uint16_t battery_millivolt = data[2] << 8 | data[3]; + float battery_voltage = battery_millivolt / 1000.0f; + + // Temperature in 1000 * Celsius. + uint16_t temp_millicelcius = data[4] << 8 | data[5]; + float temp_celcius = temp_millicelcius / 1000.0f; + + // Relative air humidity in the range [0, 2^16). + uint16_t humidity = data[6] << 8 | data[7]; + float humidity_percent = (100.0f * humidity) / (1 << 16); + + // Relative soil moisture in [0 - 2^16). + 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); + } + if (temperature_ != nullptr) { + temperature_->publish_state(temp_celcius); + } + if (humidity_ != nullptr) { + humidity_->publish_state(humidity_percent); + } + 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; +} + +} // namespace b_parasite +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h new file mode 100644 index 0000000000..70ee4ab23c --- /dev/null +++ b/esphome/components/b_parasite/b_parasite.h @@ -0,0 +1,42 @@ +#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 b_parasite { + +class BParasite : public Component, 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_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + 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 + // for deduplicating messages. + int8_t last_processed_counter_ = -1; + uint64_t address_; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *soil_moisture_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; +}; + +} // namespace b_parasite +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py new file mode 100644 index 0000000000..201685adc4 --- /dev/null +++ b/esphome/components/b_parasite/sensor.py @@ -0,0 +1,92 @@ +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_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, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_LUX, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@rbaron"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +b_parasite_ns = cg.esphome_ns.namespace("b_parasite") +BParasite = b_parasite_ns.class_( + "BParasite", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BParasite), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema( + 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, + ), + } + ) + .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)) + + for (config_key, setter) in [ + (CONF_TEMPERATURE, var.set_temperature), + (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]) + cg.add(setter(sens)) diff --git a/esphome/components/ballu/__init__.py b/esphome/components/ballu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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/__init__.py b/esphome/components/bang_bang/__init__.py index e69de29bb2..71a87b6ae5 100644 --- a/esphome/components/bang_bang/__init__.py +++ b/esphome/components/bang_bang/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index cf527988fe..5645f46f1c 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace bang_bang { -static const char *TAG = "bang_bang.climate"; +static const char *const TAG = "bang_bang.climate"; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { @@ -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; @@ -108,7 +120,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { } if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); this->prev_trigger_ = nullptr; } Trigger<> *trig; @@ -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/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 4ef811c55d..5c935987de 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -2,56 +2,76 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor -from esphome.const import CONF_AWAY_CONFIG, CONF_COOL_ACTION, \ - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, \ - CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR +from esphome.const import ( + CONF_AWAY_CONFIG, + CONF_COOL_ACTION, + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, + CONF_HEAT_ACTION, + CONF_ID, + CONF_IDLE_ACTION, + CONF_SENSOR, +) -bang_bang_ns = cg.esphome_ns.namespace('bang_bang') -BangBangClimate = bang_bang_ns.class_('BangBangClimate', climate.Climate, cg.Component) -BangBangClimateTargetTempConfig = bang_bang_ns.struct('BangBangClimateTargetTempConfig') +bang_bang_ns = cg.esphome_ns.namespace("bang_bang") +BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Component) +BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig") -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(BangBangClimate), - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, - cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, - }), -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION)) +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BangBangClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_AWAY_CONFIG): cv.Schema( + { + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION), +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) + await cg.register_component(var, config) + await climate.register_climate(var, config) - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) normal_config = BangBangClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], ) cg.add(var.set_normal_config(normal_config)) - yield automation.build_automation(var.get_idle_trigger(), [], config[CONF_IDLE_ACTION]) + await automation.build_automation( + var.get_idle_trigger(), [], config[CONF_IDLE_ACTION] + ) if CONF_COOL_ACTION in config: - yield automation.build_automation(var.get_cool_trigger(), [], config[CONF_COOL_ACTION]) + await automation.build_automation( + var.get_cool_trigger(), [], config[CONF_COOL_ACTION] + ) cg.add(var.set_supports_cool(True)) if CONF_HEAT_ACTION in config: - yield automation.build_automation(var.get_heat_trigger(), [], config[CONF_HEAT_ACTION]) + await automation.build_automation( + var.get_heat_trigger(), [], config[CONF_HEAT_ACTION] + ) cg.add(var.set_supports_heat(True)) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] away_config = BangBangClimateTargetTempConfig( away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], ) cg.add(var.set_away_config(away_config)) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 9cd152e1ef..4e6bb3c563 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -4,9 +4,11 @@ namespace esphome { namespace bh1750 { -static const char *TAG = "bh1750.sensor"; +static const char *const TAG = "bh1750.sensor"; static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001; +static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000; // last 3 bits +static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000; // last 5 bits void BH1750Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str()); @@ -14,7 +16,13 @@ void BH1750Sensor::setup() { this->mark_failed(); return; } + + uint8_t mtreg_hi = (this->measurement_duration_ >> 5) & 0b111; + uint8_t mtreg_lo = (this->measurement_duration_ >> 0) & 0b11111; + this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); + this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); } + void BH1750Sensor::dump_config() { LOG_SENSOR("", "BH1750", this); LOG_I2C_DEVICE(this); @@ -59,19 +67,26 @@ void BH1750Sensor::update() { this->set_timeout("illuminance", wait, [this]() { this->read_data_(); }); } + 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_; + if (this->resolution_ == BH1750_RESOLUTION_0P5_LX) { + lx /= 2.0f; + } ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); this->publish_state(lx); this->status_clear_warning(); } + void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; } } // namespace bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 8df0bda02a..c88fa10832 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -28,6 +28,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: * @param resolution The new resolution of the sensor. */ void set_resolution(BH1750Resolution resolution); + void set_measurement_duration(uint8_t measurement_duration) { measurement_duration_ = measurement_duration; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -40,6 +41,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void read_data_(); BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; + uint8_t measurement_duration_; }; } // namespace bh1750 diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index b3ce0eaf88..156c7bb375 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -1,30 +1,61 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, CONF_RESOLUTION, UNIT_LUX, ICON_BRIGHTNESS_5 +from esphome.const import ( + CONF_ID, + CONF_RESOLUTION, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, + CONF_MEASUREMENT_DURATION, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -bh1750_ns = cg.esphome_ns.namespace('bh1750') -BH1750Resolution = bh1750_ns.enum('BH1750Resolution') +bh1750_ns = cg.esphome_ns.namespace("bh1750") +BH1750Resolution = bh1750_ns.enum("BH1750Resolution") BH1750_RESOLUTIONS = { 4.0: BH1750Resolution.BH1750_RESOLUTION_4P0_LX, 1.0: BH1750Resolution.BH1750_RESOLUTION_1P0_LX, 0.5: BH1750Resolution.BH1750_RESOLUTION_0P5_LX, } -BH1750Sensor = bh1750_ns.class_('BH1750Sensor', sensor.Sensor, cg.PollingComponent, i2c.I2CDevice) +BH1750Sensor = bh1750_ns.class_( + "BH1750Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 1).extend({ - cv.GenerateID(): cv.declare_id(BH1750Sensor), - cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum(BH1750_RESOLUTIONS, float=True), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x23)) +CONF_MEASUREMENT_TIME = "measurement_time" +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(BH1750Sensor), + cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum( + BH1750_RESOLUTIONS, float=True + ), + cv.Optional(CONF_MEASUREMENT_DURATION, default=69): cv.int_range( + min=31, max=254 + ), + cv.Optional(CONF_MEASUREMENT_TIME): cv.invalid( + "The 'measurement_time' option has been replaced with 'measurement_duration' in 1.18.0" + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x23)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION])) diff --git a/esphome/components/binary/__init__.py b/esphome/components/binary/__init__.py index 7a8031df54..3f6a45d28a 100644 --- a/esphome/components/binary/__init__.py +++ b/esphome/components/binary/__init__.py @@ -1,3 +1,3 @@ import esphome.codegen as cg -binary_ns = cg.esphome_ns.namespace('binary') +binary_ns = cg.esphome_ns.namespace("binary") diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index dbfe1a8286..e6c8d9bfe9 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,27 +1,39 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_OUTPUT_ID +from esphome.const import ( + CONF_DIRECTION_OUTPUT, + CONF_OSCILLATION_OUTPUT, + CONF_OUTPUT, + CONF_OUTPUT_ID, +) from .. import binary_ns -BinaryFan = binary_ns.class_('BinaryFan', cg.Component) +BinaryFan = binary_ns.class_("BinaryFan", cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - fan_ = yield fan.create_fan_state(config) + fan_ = await fan.create_fan_state(config) cg.add(var.set_fan(fan_)) - output_ = yield cg.get_variable(config[CONF_OUTPUT]) + output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) if CONF_OSCILLATION_OUTPUT in config: - oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) + oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 986902efe5..2201fe576e 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -4,16 +4,19 @@ namespace esphome { namespace binary { -static const char *TAG = "binary.fan"; +static const char *const TAG = "binary.fan"; void binary::BinaryFan::dump_config() { ESP_LOGCONFIG(TAG, "Fan '%s':", this->fan_->get_name().c_str()); if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void BinaryFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, false); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -41,8 +44,21 @@ void BinaryFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + 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/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 980d2629f6..93294b8dee 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,11 +16,13 @@ class BinaryFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } protected: fan::FanState *fan_; output::BinaryOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; bool next_update_{true}; }; diff --git a/esphome/components/binary/light/__init__.py b/esphome/components/binary/light/__init__.py index 6167ae239f..49227ccadc 100644 --- a/esphome/components/binary/light/__init__.py +++ b/esphome/components/binary/light/__init__.py @@ -4,17 +4,19 @@ from esphome.components import light, output from esphome.const import CONF_OUTPUT_ID, CONF_OUTPUT from .. import binary_ns -BinaryLightOutput = binary_ns.class_('BinaryLightOutput', light.LightOutput) +BinaryLightOutput = binary_ns.class_("BinaryLightOutput", light.LightOutput) -CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryLightOutput), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), -}) +CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryLightOutput), + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield light.register_light(var, config) + await light.register_light(var, config) - out = yield cg.get_variable(config[CONF_OUTPUT]) + out = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(out)) 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 7c78c3a369..1eab76d54e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,131 +1,266 @@ 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 -from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ - CONF_ID, CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERTED, \ - CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_ON_CLICK, \ - CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \ - CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR, CONF_NAME, CONF_MQTT_ID -from esphome.core import CORE, coroutine, coroutine_with_priority +from esphome.const import ( + CONF_DELAY, + CONF_DEVICE_CLASS, + CONF_FILTERS, + CONF_ID, + CONF_INVALID_COOLDOWN, + CONF_INVERTED, + CONF_MAX_LENGTH, + CONF_MIN_LENGTH, + CONF_ON_CLICK, + CONF_ON_DOUBLE_CLICK, + CONF_ON_MULTI_CLICK, + CONF_ON_PRESS, + CONF_ON_RELEASE, + CONF_ON_STATE, + CONF_STATE, + CONF_TIMING, + CONF_TRIGGER_ID, + CONF_NAME, + CONF_MQTT_ID, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, + DEVICE_CLASS_UPDATE, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) +from esphome.core import CORE, coroutine_with_priority from esphome.util import Registry +CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - '', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas', - 'heat', 'light', 'lock', 'moisture', 'motion', 'moving', 'occupancy', - 'opening', 'plug', 'power', 'presence', 'problem', 'safety', 'smoke', - 'sound', 'vibration', 'window' + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, + DEVICE_CLASS_UPDATE, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, ] IS_PLATFORM_COMPONENT = True -binary_sensor_ns = cg.esphome_ns.namespace('binary_sensor') -BinarySensor = binary_sensor_ns.class_('BinarySensor', cg.Nameable) -BinarySensorInitiallyOff = binary_sensor_ns.class_('BinarySensorInitiallyOff', BinarySensor) -BinarySensorPtr = BinarySensor.operator('ptr') +binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor") +BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase) +BinarySensorInitiallyOff = binary_sensor_ns.class_( + "BinarySensorInitiallyOff", BinarySensor +) +BinarySensorPtr = BinarySensor.operator("ptr") # Triggers -PressTrigger = binary_sensor_ns.class_('PressTrigger', automation.Trigger.template()) -ReleaseTrigger = binary_sensor_ns.class_('ReleaseTrigger', automation.Trigger.template()) -ClickTrigger = binary_sensor_ns.class_('ClickTrigger', automation.Trigger.template()) -DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', automation.Trigger.template()) -MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', automation.Trigger.template(), - cg.Component) -MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent') -StateTrigger = binary_sensor_ns.class_('StateTrigger', automation.Trigger.template(bool)) -BinarySensorPublishAction = binary_sensor_ns.class_('BinarySensorPublishAction', automation.Action) +PressTrigger = binary_sensor_ns.class_("PressTrigger", automation.Trigger.template()) +ReleaseTrigger = binary_sensor_ns.class_( + "ReleaseTrigger", automation.Trigger.template() +) +ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template()) +DoubleClickTrigger = binary_sensor_ns.class_( + "DoubleClickTrigger", automation.Trigger.template() +) +MultiClickTrigger = binary_sensor_ns.class_( + "MultiClickTrigger", automation.Trigger.template(), cg.Component +) +MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") +StateTrigger = binary_sensor_ns.class_( + "StateTrigger", automation.Trigger.template(bool) +) +BinarySensorPublishAction = binary_sensor_ns.class_( + "BinarySensorPublishAction", automation.Action +) # Condition -BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition) +BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition) # Filters -Filter = binary_sensor_ns.class_('Filter') -DelayedOnOffFilter = binary_sensor_ns.class_('DelayedOnOffFilter', Filter, cg.Component) -DelayedOnFilter = binary_sensor_ns.class_('DelayedOnFilter', Filter, cg.Component) -DelayedOffFilter = binary_sensor_ns.class_('DelayedOffFilter', Filter, cg.Component) -InvertFilter = binary_sensor_ns.class_('InvertFilter', Filter) -LambdaFilter = binary_sensor_ns.class_('LambdaFilter', Filter) +Filter = binary_sensor_ns.class_("Filter") +DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) +DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) +DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) +InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) +AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) +LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) FILTER_REGISTRY = Registry() -validate_filters = cv.validate_registry('filter', FILTER_REGISTRY) +validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) -@FILTER_REGISTRY.register('invert', InvertFilter, {}) -def invert_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id) +@FILTER_REGISTRY.register("invert", InvertFilter, {}) +async def invert_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id) -@FILTER_REGISTRY.register('delayed_on_off', DelayedOnOffFilter, - cv.positive_time_period_milliseconds) -def delayed_on_off_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register( + "delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds +) +async def delayed_on_off_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id, config) - yield cg.register_component(var, {}) - yield var + await cg.register_component(var, {}) + return var -@FILTER_REGISTRY.register('delayed_on', DelayedOnFilter, - cv.positive_time_period_milliseconds) -def delayed_on_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register( + "delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds +) +async def delayed_on_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id, config) - yield cg.register_component(var, {}) - yield var + await cg.register_component(var, {}) + return var -@FILTER_REGISTRY.register('delayed_off', DelayedOffFilter, cv.positive_time_period_milliseconds) -def delayed_off_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register( + "delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds +) +async def delayed_off_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id, config) - yield cg.register_component(var, {}) - yield var + await cg.register_component(var, {}) + return var -@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.returning_lambda) -def lambda_filter_to_code(config, filter_id): - lambda_ = yield cg.process_lambda(config, [(bool, 'x')], return_type=cg.optional.template(bool)) - yield cg.new_Pvariable(filter_id, lambda_) +CONF_TIME_OFF = "time_off" +CONF_TIME_ON = "time_on" + +DEFAULT_DELAY = "1s" +DEFAULT_TIME_OFF = "100ms" +DEFAULT_TIME_ON = "900ms" -MULTI_CLICK_TIMING_SCHEMA = cv.Schema({ - cv.Optional(CONF_STATE): cv.boolean, - cv.Optional(CONF_MIN_LENGTH): cv.positive_time_period_milliseconds, - cv.Optional(CONF_MAX_LENGTH): cv.positive_time_period_milliseconds, -}) +@FILTER_REGISTRY.register( + "autorepeat", + AutorepeatFilter, + cv.All( + cv.ensure_list( + { + cv.Optional( + CONF_DELAY, default=DEFAULT_DELAY + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TIME_OFF, default=DEFAULT_TIME_OFF + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TIME_ON, default=DEFAULT_TIME_ON + ): cv.positive_time_period_milliseconds, + } + ), + ), +) +async def autorepeat_filter_to_code(config, filter_id): + timings = [] + if len(config) > 0: + for conf in config: + timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])) + else: + timings.append( + ( + cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds, + cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, + cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, + ) + ) + var = cg.new_Pvariable(filter_id, timings) + 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( + config, [(bool, "x")], return_type=cg.optional.template(bool) + ) + return cg.new_Pvariable(filter_id, lambda_) + + +MULTI_CLICK_TIMING_SCHEMA = cv.Schema( + { + cv.Optional(CONF_STATE): cv.boolean, + cv.Optional(CONF_MIN_LENGTH): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_LENGTH): cv.positive_time_period_milliseconds, + } +) def parse_multi_click_timing_str(value): if not isinstance(value, str): return value - parts = value.lower().split(' ') + parts = value.lower().split(" ") if len(parts) != 5: - raise cv.Invalid("Multi click timing grammar consists of exactly 5 words, not {}" - "".format(len(parts))) + raise cv.Invalid( + f"Multi click timing grammar consists of exactly 5 words, not {len(parts)}" + ) try: state = cv.boolean(parts[0]) except cv.Invalid: - raise cv.Invalid("First word must either be ON or OFF, not {}".format(parts[0])) + # pylint: disable=raise-missing-from + 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])) + if parts[1] != "for": + raise cv.Invalid(f"Second word must be 'for', got {parts[1]}") - if parts[2] == 'at': - if parts[3] == 'least': + if parts[2] == "at": + if parts[3] == "least": key = CONF_MIN_LENGTH - elif parts[3] == 'most': + elif parts[3] == "most": key = CONF_MAX_LENGTH else: - raise cv.Invalid("Third word after at must either be 'least' or 'most', got {}" - "".format(parts[3])) + raise cv.Invalid( + f"Third word after at must either be 'least' or 'most', got {parts[3]}" + ) try: length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}") - return { - CONF_STATE: state, - key: str(length) - } + return {CONF_STATE: state, key: str(length)} - if parts[3] != 'to': + if parts[3] != "to": raise cv.Invalid("Multi click grammar: 4th word must be 'to'") try: @@ -141,7 +276,7 @@ def parse_multi_click_timing_str(value): return { CONF_STATE: state, CONF_MIN_LENGTH: str(min_length), - CONF_MAX_LENGTH: str(max_length) + CONF_MAX_LENGTH: str(max_length), } @@ -161,11 +296,13 @@ 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)) + raise cv.Invalid( + 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)) + raise cv.Invalid( + f"Max length ({max_length}) must be larger than min length ({min_length})." + ) state = new_state tim = { @@ -178,140 +315,163 @@ def validate_multi_click_timing(value): return timings -device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space='_') - -BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(BinarySensor), - cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTBinarySensorComponent), - - cv.Optional(CONF_DEVICE_CLASS): device_class, - cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_PRESS): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), - }), - cv.Optional(CONF_ON_RELEASE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), - }), - cv.Optional(CONF_ON_CLICK): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), - cv.Optional(CONF_MIN_LENGTH, default='50ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_MAX_LENGTH, default='350ms'): cv.positive_time_period_milliseconds, - }), - cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), - cv.Optional(CONF_MIN_LENGTH, default='50ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_MAX_LENGTH, default='350ms'): cv.positive_time_period_milliseconds, - }), - cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), - cv.Required(CONF_TIMING): cv.All([parse_multi_click_timing_str], - validate_multi_click_timing), - cv.Optional(CONF_INVALID_COOLDOWN, default='1s'): cv.positive_time_period_milliseconds, - }), - cv.Optional(CONF_ON_STATE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - }), - - 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." - ), -}) +device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -@coroutine -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(CONF_INTERNAL)) +BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(BinarySensor), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTBinarySensorComponent + ), + cv.Optional(CONF_DEVICE_CLASS): device_class, + cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional(CONF_ON_PRESS): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), + } + ), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), + } + ), + cv.Optional(CONF_ON_CLICK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), + cv.Required(CONF_TIMING): cv.All( + [parse_multi_click_timing_str], validate_multi_click_timing + ), + cv.Optional( + CONF_INVALID_COOLDOWN, default="1s" + ): cv.positive_time_period_milliseconds, + } + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + } +) + + +async def setup_binary_sensor_core_(var, config): + await setup_entity(var, config) + if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) if CONF_INVERTED in config: cg.add(var.set_inverted(config[CONF_INVERTED])) if CONF_FILTERS in config: - filters = yield cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS]) + filters = await cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS]) cg.add(var.add_filters(filters)) for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_RELEASE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_CLICK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, - conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]) - yield automation.build_automation(trigger, [], conf) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH] + ) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_DOUBLE_CLICK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, - conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]) - yield automation.build_automation(trigger, [], conf) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH] + ) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_MULTI_CLICK, []): timings = [] for tim in conf[CONF_TIMING]: - timings.append(cg.StructInitializer( - MultiClickTriggerEvent, - ('state', tim[CONF_STATE]), - ('min_length', tim[CONF_MIN_LENGTH]), - ('max_length', tim.get(CONF_MAX_LENGTH, 4294967294)), - )) + timings.append( + cg.StructInitializer( + MultiClickTriggerEvent, + ("state", tim[CONF_STATE]), + ("min_length", tim[CONF_MIN_LENGTH]), + ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)), + ) + ) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) if CONF_INVALID_COOLDOWN in conf: cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) - yield cg.register_component(trigger, conf) - yield automation.build_automation(trigger, [], conf) + await cg.register_component(trigger, conf) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [(bool, 'x')], conf) + await automation.build_automation(trigger, [(bool, "x")], conf) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) - yield mqtt.register_mqtt_component(mqtt_, config) + await mqtt.register_mqtt_component(mqtt_, config) -@coroutine -def register_binary_sensor(var, config): +async def register_binary_sensor(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_binary_sensor(var)) - yield setup_binary_sensor_core_(var, config) + await setup_binary_sensor_core_(var, config) -@coroutine -def new_binary_sensor(config): +async def new_binary_sensor(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) - yield register_binary_sensor(var, config) - yield var + await register_binary_sensor(var, config) + return var -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."), -}) +BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(BinarySensor), + } +) -@automation.register_condition('binary_sensor.is_on', BinarySensorCondition, - BINARY_SENSOR_CONDITION_SCHEMA) -def binary_sensor_is_on_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren, True) +@automation.register_condition( + "binary_sensor.is_on", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA +) +async def binary_sensor_is_on_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, True) -@automation.register_condition('binary_sensor.is_off', BinarySensorCondition, - BINARY_SENSOR_CONDITION_SCHEMA) -def binary_sensor_is_off_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren, False) +@automation.register_condition( + "binary_sensor.is_off", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA +) +async def binary_sensor_is_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, False) @coroutine_with_priority(100.0) -def to_code(config): - cg.add_define('USE_BINARY_SENSOR') +async def to_code(config): + cg.add_define("USE_BINARY_SENSOR") cg.add_global(binary_sensor_ns.using) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index b8be2d1b1f..ce082aafb3 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace binary_sensor { -static const char *TAG = "binary_sensor.automation"; +static const char *const TAG = "binary_sensor.automation"; void binary_sensor::MultiClickTrigger::on_state_(bool state) { // Handle duplicate events @@ -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 e9ff37446d..31bf1a5565 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -1,7 +1,10 @@ #pragma once +#include + #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 { @@ -87,8 +90,8 @@ class DoubleClickTrigger : public Trigger<> { class MultiClickTrigger : public Trigger<>, public Component { public: - explicit MultiClickTrigger(BinarySensor *parent, const std::vector &timing) - : parent_(parent), timing_(timing) {} + explicit MultiClickTrigger(BinarySensor *parent, std::vector timing) + : parent_(parent), timing_(std::move(timing)) {} void setup() override { this->last_state_ = this->parent_->state; @@ -137,6 +140,7 @@ template class BinarySensorPublishAction : public Action public: explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 1cde692dd4..41da83aa3e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace binary_sensor { -static const char *TAG = "binary_sensor"; +static const char *const TAG = "binary_sensor"; void BinarySensor::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); @@ -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() { @@ -61,7 +61,7 @@ void BinarySensor::add_filter(Filter *filter) { last_filter->next_ = filter; } } -void BinarySensor::add_filters(std::vector filters) { +void BinarySensor::add_filters(const std::vector &filters) { for (Filter *filter : filters) { this->add_filter(filter); } diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index f91c93c424..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" @@ -9,10 +10,10 @@ namespace esphome { 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()); \ - if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ } @@ -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 @@ -60,7 +61,7 @@ class BinarySensor : public Nameable { std::string get_device_class(); void add_filter(Filter *filter); - void add_filters(std::vector filters); + void add_filters(const std::vector &filters); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index f4612d62e9..53c2daf42d 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,11 +1,13 @@ #include "filter.h" + #include "binary_sensor.h" +#include namespace esphome { namespace binary_sensor { -static const char *TAG = "sensor.filter"; +static const char *const TAG = "sensor.filter"; void Filter::output(bool value, bool is_initial) { if (!this->dedup_.next(value)) @@ -64,7 +66,51 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } -LambdaFilter::LambdaFilter(const std::function(bool)> &f) : f_(f) {} +AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} + +optional AutorepeatFilter::new_value(bool value, bool is_initial) { + if (value) { + // Ignore if already running + if (this->active_timing_ != 0) + return {}; + + this->next_timing_(); + return true; + } else { + this->cancel_timeout("TIMING"); + this->cancel_timeout("ON_OFF"); + this->active_timing_ = 0; + return false; + } +} + +void AutorepeatFilter::next_timing_() { + // Entering this method + // 1st time: starts waiting the first delay + // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on + // last time: no delay to start but have to bump the index to reflect the last + if (this->active_timing_ < this->timings_.size()) + this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); + + if (this->active_timing_ <= this->timings_.size()) { + this->active_timing_++; + } + + if (this->active_timing_ == 2) + this->next_value_(false); + + // Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals +} + +void AutorepeatFilter::next_value_(bool val) { + const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; + this->output(val, false); // This is at least the second one so not initial + this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); +} + +float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + +LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 0b54251cda..59068634af 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -66,9 +66,36 @@ class InvertFilter : public Filter { optional new_value(bool value, bool is_initial) override; }; +struct AutorepeatFilterTiming { + AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) { + this->delay = delay; + this->time_off = off; + this->time_on = on; + } + uint32_t delay; + uint32_t time_off; + uint32_t time_on; +}; + +class AutorepeatFilter : public Filter, public Component { + public: + explicit AutorepeatFilter(std::vector timings); + + optional new_value(bool value, bool is_initial) override; + + float get_setup_priority() const override; + + protected: + void next_timing_(); + void next_value_(bool val); + + std::vector timings_; + uint8_t active_timing_{0}; +}; + class LambdaFilter : public Filter { public: - explicit LambdaFilter(const std::function(bool)> &f); + explicit LambdaFilter(std::function(bool)> f); optional new_value(bool value, bool is_initial) override; diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.cpp b/esphome/components/binary_sensor_map/binary_sensor_map.cpp index 7a2eb66cf8..d1123ddff0 100644 --- a/esphome/components/binary_sensor_map/binary_sensor_map.cpp +++ b/esphome/components/binary_sensor_map/binary_sensor_map.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace binary_sensor_map { -static const char *TAG = "binary_sensor_map"; +static const char *const TAG = "binary_sensor_map"; void BinarySensorMap::dump_config() { LOG_SENSOR(" ", "binary_sensor_map", this); } diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 27f4654ded..946e2f9e62 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -2,14 +2,24 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, binary_sensor -from esphome.const import CONF_ID, CONF_CHANNELS, CONF_VALUE, CONF_TYPE, UNIT_EMPTY, \ - ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP +from esphome.const import ( + CONF_ID, + CONF_CHANNELS, + CONF_VALUE, + CONF_TYPE, + ICON_CHECK_CIRCLE_OUTLINE, + CONF_BINARY_SENSOR, + CONF_GROUP, + STATE_CLASS_NONE, +) -DEPENDENCIES = ['binary_sensor'] +DEPENDENCIES = ["binary_sensor"] -binary_sensor_map_ns = cg.esphome_ns.namespace('binary_sensor_map') -BinarySensorMap = binary_sensor_map_ns.class_('BinarySensorMap', cg.Component, sensor.Sensor) -SensorMapType = binary_sensor_map_ns.enum('SensorMapType') +binary_sensor_map_ns = cg.esphome_ns.namespace("binary_sensor_map") +BinarySensorMap = binary_sensor_map_ns.class_( + "BinarySensorMap", cg.Component, sensor.Sensor +) +SensorMapType = binary_sensor_map_ns.enum("SensorMapType") SENSOR_MAP_TYPES = { CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, @@ -20,22 +30,33 @@ entry = { cv.Required(CONF_VALUE): cv.float_, } -CONFIG_SCHEMA = cv.typed_schema({ - CONF_GROUP: sensor.sensor_schema(UNIT_EMPTY, ICON_CHECK_CIRCLE_OUTLINE, 0).extend({ - cv.GenerateID(): cv.declare_id(BinarySensorMap), - cv.Required(CONF_CHANNELS): cv.All(cv.ensure_list(entry), cv.Length(min=1)), - }), -}, lower=True) +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_GROUP: sensor.sensor_schema( + icon=ICON_CHECK_CIRCLE_OUTLINE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + ).extend( + { + cv.GenerateID(): cv.declare_id(BinarySensorMap), + cv.Required(CONF_CHANNELS): cv.All( + cv.ensure_list(entry), cv.Length(min=1) + ), + } + ), + }, + lower=True, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) constant = SENSOR_MAP_TYPES[config[CONF_TYPE]] cg.add(var.set_sensor_type(constant)) for ch in config[CONF_CHANNELS]: - input_var = yield cg.get_variable(ch[CONF_BINARY_SENSOR]) + input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR]) cg.add(var.add_channel(input_var, ch[CONF_VALUE])) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py new file mode 100644 index 0000000000..4bd5c25246 --- /dev/null +++ b/esphome/components/ble_client/__init__.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import ( + CONF_ID, + CONF_MAC_ADDRESS, + CONF_NAME, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_TRIGGER_ID, +) +from esphome import automation + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["esp32_ble_tracker"] + +ble_client_ns = cg.esphome_ns.namespace("ble_client") +BLEClient = ble_client_ns.class_( + "BLEClient", cg.Component, esp32_ble_tracker.ESPBTClient +) +BLEClientNode = ble_client_ns.class_("BLEClientNode") +BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const") +# Triggers +BLEClientConnectTrigger = ble_client_ns.class_( + "BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) +BLEClientDisconnectTrigger = ble_client_ns.class_( + "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) + +# Espressif platformio framework is built with MAX_BLE_CONN to 3, so +# enforce this in yaml checks. +MULTI_CONF = 3 + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEClient), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientConnectTrigger + ), + } + ), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientDisconnectTrigger + ), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) +) + +CONF_BLE_CLIENT_ID = "ble_client_id" + +BLE_CLIENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient), + } +) + + +async def register_ble_node(var, config): + parent = await cg.get_variable(config[CONF_BLE_CLIENT_ID]) + cg.add(parent.register_ble_node(var)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_client(var, config) + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + for conf in config.get(CONF_ON_CONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DISCONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h new file mode 100644 index 0000000000..6c374046ba --- /dev/null +++ b/esphome/components/ble_client/automation.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/ble_client.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { +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) 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; + } +}; + +class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (event == ESP_GATTC_DISCONNECT_EVT && memcmp(param->disconnect.remote_bda, this->parent_->remote_bda, 6) == 0) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::ESTABLISHED; + } +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp new file mode 100644 index 0000000000..407f1a1d17 --- /dev/null +++ b/esphome/components/ble_client/ble_client.cpp @@ -0,0 +1,403 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "ble_client.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_client"; + +float BLEClient::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + +void BLEClient::setup() { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_states_(espbt::ClientState::IDLE); + this->enabled = true; +} + +void BLEClient::loop() { + if (this->state() == espbt::ClientState::DISCOVERED) { + this->connect(); + } + for (auto *node : this->nodes_) + node->loop(); +} + +void BLEClient::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Client:"); + ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); +} + +bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { + if (!this->enabled) + return false; + if (device.address_uint64() != this->address) + return false; + 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); + + auto addr = device.address_uint64(); + this->remote_bda[0] = (addr >> 40) & 0xFF; + this->remote_bda[1] = (addr >> 32) & 0xFF; + this->remote_bda[2] = (addr >> 24) & 0xFF; + this->remote_bda[3] = (addr >> 16) & 0xFF; + this->remote_bda[4] = (addr >> 8) & 0xFF; + this->remote_bda[5] = (addr >> 0) & 0xFF; + return true; +} + +std::string BLEClient::address_str() const { + char buf[20]; + sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", (uint8_t)(this->address >> 40) & 0xff, + (uint8_t)(this->address >> 32) & 0xff, (uint8_t)(this->address >> 24) & 0xff, + (uint8_t)(this->address >> 16) & 0xff, (uint8_t)(this->address >> 8) & 0xff, + (uint8_t)(this->address >> 0) & 0xff); + std::string ret; + ret = buf; + return ret; +} + +void BLEClient::set_enabled(bool enabled) { + if (enabled == this->enabled) + return; + if (!enabled && this->state() != espbt::ClientState::IDLE) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + auto ret = esp_ble_gattc_close(this->gattc_if, this->conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); + } + } + this->enabled = enabled; +} + +void BLEClient::connect() { + ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); + 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); + } else { + this->set_states_(espbt::ClientState::CONNECTING); + } +} + +void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return; + 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_(); + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "gattc registered app id %d", this->app_id); + this->gattc_if = esp_gattc_if; + } else { + ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + 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); + break; + } + this->conn_id = param->open.conn_id; + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret); + } + break; + } + 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); + 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, nullptr); + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { + return; + } + ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) + delete svc; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.clear(); + this->set_states_(espbt::ClientState::IDLE); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + 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; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) { + ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str()); + 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); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + auto descr = this->get_config_descriptor(param->reg_for_notify.handle); + if (descr == nullptr) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle); + break; + } + if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle, + descr->uuid.to_string().c_str()); + break; + } + uint8_t notify_en = 1; + auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), + ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + } + break; + } + + default: + break; + } + for (auto *node : this->nodes_) + node->gattc_event_handler(event, esp_gattc_if, param); + + // Delete characteristics after clients have used them to save RAM. + if (!all_established && this->all_nodes_established_()) { + for (auto &svc : this->services_) + delete svc; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.clear(); + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + if (length > 2) { + return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); + } + case 0x7: // uint24. + if (length > 3) { + return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); + } + case 0x8: // uint32. + if (length > 4) { + return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + + (uint32_t)(value[4])); + } + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + if (length > 2) { + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + } + case 0xF: // int24. + if (length > 3) { + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + } + case 0x10: // int32. + if (length > 4) { + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } + } + ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length); + return NAN; +} + +BLEService *BLEClient::get_service(espbt::ESPBTUUID uuid) { + for (auto svc : this->services_) + if (svc->uuid == uuid) + return svc; + return nullptr; +} + +BLEService *BLEClient::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClient::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLEDescriptor *BLEClient::get_config_descriptor(uint16_t handle) { + for (auto &svc : this->services_) + for (auto &chr : svc->characteristics) + if (chr->handle == handle) + for (auto &desc : chr->descriptors) + if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902)) + return desc; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + for (auto &chr : this->characteristics) + if (chr->uuid == uuid) + return chr; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEDescriptor *BLEClient::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLEService::parse_characteristics() { + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_char( + this->client->gattc_if, this->client->conn_id, this->start_handle, this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + 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; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(), + characteristic->handle, characteristic->properties); + characteristic->parse_descriptors(); + offset++; + } +} + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLECharacteristic::parse_descriptors() { + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_descr( + this->service->client->gattc_if, this->service->client->conn_id, this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory) + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + for (auto &desc : this->descriptors) + if (desc->uuid == uuid) + return desc; + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} + +void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { + auto client = this->service->client; + auto status = esp_ble_gattc_write_char(client->gattc_if, client->conn_id, this->handle, new_val_size, new_val, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", status); + } +} + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h new file mode 100644 index 0000000000..5680b69f72 --- /dev/null +++ b/esphome/components/ble_client/ble_client.h @@ -0,0 +1,142 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +#include +#include +#include +#include +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClient; +class BLEService; +class BLECharacteristic; + +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(){}; + void set_address(uint64_t address) { address_ = address; } + espbt::ESPBTClient *client; + // This should be transitioned to Established once the node no longer needs + // the services/descriptors/characteristics of the parent client. This will + // allow some memory to be freed. + espbt::ClientState node_state; + + BLEClient *parent() { return this->parent_; } + void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } + + protected: + BLEClient *parent_; + uint64_t address_; +}; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + void write_value(uint8_t *new_val, int16_t new_val_size); + BLEService *service; +}; + +class BLEService { + public: + ~BLEService(); + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClient *client; + void parse_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +class BLEClient : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const 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; + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + void connect() override; + + void set_address(uint64_t address) { this->address = address; } + + void set_enabled(bool enabled); + + void register_ble_node(BLEClientNode *node) { + node->client = this; + node->set_ble_client_parent(this); + this->nodes_.push_back(node); + } + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int gattc_if; + esp_bd_addr_t remote_bda; + uint16_t conn_id; + uint64_t address; + bool enabled; + std::string address_str() const; + + protected: + 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) + return false; + for (auto &node : nodes_) + if (node->node_state != espbt::ClientState::ESTABLISHED) + return false; + return true; + } + + std::vector nodes_; + std::vector services_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py new file mode 100644 index 0000000000..fe5835ca82 --- /dev/null +++ b/esphome/components/ble_client/output/__init__.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output, ble_client, esp32_ble_tracker +from esphome.const import CONF_ID, CONF_SERVICE_UUID +from .. import ble_client_ns + + +DEPENDENCIES = ["ble_client"] + +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" + +BLEBinaryOutput = ble_client_ns.class_( + "BLEBinaryOutput", output.BinaryOutput, ble_client.BLEClientNode, cg.Component +) + +CONFIG_SCHEMA = cv.All( + output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(BLEBinaryOutput), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + + yield output.register_output(var, config) + yield ble_client.register_ble_node(var, config) + yield cg.register_component(var, config) diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp new file mode 100644 index 0000000000..ff3711e842 --- /dev/null +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -0,0 +1,71 @@ +#include "ble_binary_output.h" +#include "esphome/core/log.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_binary_output"; + +void BLEBinaryOutput::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Binary Output:"); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent_->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + LOG_BINARY_OUTPUT(this); +} + +void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: + this->client_state_ = espbt::ClientState::ESTABLISHED; + ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str()); + break; + case ESP_GATTC_DISCONNECT_EVT: + ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str()); + this->client_state_ = espbt::ClientState::IDLE; + break; + case ESP_GATTC_WRITE_CHAR_EVT: { + if (param->write.status == 0) { + break; + } + + auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->char_uuid_.to_string().c_str()); + break; + } + if (param->write.handle == chr->handle) { + ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); + } + break; + } + default: + break; + } +} + +void BLEBinaryOutput::write_state(bool state) { + if (this->client_state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", + this->char_uuid_.to_string().c_str()); + return; + } + + auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] Characteristic not found. State update can not be written.", + this->char_uuid_.to_string().c_str()); + return; + } + + uint8_t state_as_uint = (uint8_t) state; + ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint); + chr->write_value(&state_as_uint, sizeof(state_as_uint)); +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h new file mode 100644 index 0000000000..e1d62a267b --- /dev/null +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -0,0 +1,39 @@ +#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/output/binary_output.h" + +#ifdef USE_ESP32 +#include +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, public Component { + public: + void dump_config() override; + void loop() override {} + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + protected: + void write_state(bool state) override; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ClientState client_state_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py new file mode 100644 index 0000000000..4aa6a92ba5 --- /dev/null +++ b/esphome/components/ble_client/sensor/__init__.py @@ -0,0 +1,131 @@ +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 ( + CONF_ID, + CONF_LAMBDA, + STATE_CLASS_NONE, + CONF_TRIGGER_ID, + CONF_SERVICE_UUID, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" + +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") + +BLESensor = ble_client_ns.class_( + "BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode +) +BLESensorNotifyTrigger = ble_client_ns.class_( + "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(BLESensor), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLESensorNotifyTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) + cg.add(var.set_descr_uuid128(uuid128)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(adv_data_t_const_ref, "x")], return_type=cg.float_ + ) + cg.add(var.set_data_to_value(lambda_)) + + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + await sensor.register_sensor(var, config) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await ble_client.register_ble_node(trigger, config) + await automation.build_automation(trigger, [(float, "x")], conf) diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h new file mode 100644 index 0000000000..2baaafe2ec --- /dev/null +++ b/esphome/components/ble_client/sensor/automation.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/sensor/ble_sensor.h" + +#ifdef USE_ESP32 + +namespace esphome { +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) override { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLESensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp new file mode 100644 index 0000000000..7a2e3ddc8b --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -0,0 +1,138 @@ +#include "ble_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_sensor"; + +uint32_t BLESensor::hash_base() { return 343459825UL; } + +void BLESensor::loop() {} + +void BLESensor::dump_config() { + LOG_SENSOR("", "BLE Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = + esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::ESTABLISHED; + } + 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) { + this->status_clear_warning(); + this->publish_state(this->parse_data_(param->read.value, param->read.value_len)); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + break; + ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + 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; + break; + } + default: + break; + } +} + +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); + } else { + return value[0]; + } +} + +void BLESensor::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h new file mode 100644 index 0000000000..d9f310b575 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +using data_to_value_t = std::function)>; + +class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + 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; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void 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_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); + optional data_to_value_func_{}; + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py new file mode 100644 index 0000000000..e5b5ab281b --- /dev/null +++ b/esphome/components/ble_client/switch/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch, ble_client +from esphome.const import CONF_ICON, CONF_ID, CONF_INVERTED, ICON_BLUETOOTH +from .. import ble_client_ns + +BLEClientSwitch = ble_client_ns.class_( + "BLEClientSwitch", switch.Switch, cg.Component, ble_client.BLEClientNode +) + +CONFIG_SCHEMA = ( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLEClientSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "BLE client switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_BLUETOOTH): switch.icon, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .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) + await ble_client.register_ble_node(var, config) diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp new file mode 100644 index 0000000000..6de5252404 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -0,0 +1,39 @@ +#include "ble_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_switch"; + +void BLEClientSwitch::write_state(bool state) { + this->parent_->set_enabled(state); + this->publish_state(state); +} + +void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + this->publish_state(this->parent_->enabled); + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + break; + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::IDLE; + this->publish_state(this->parent_->enabled); + break; + default: + break; + } +} + +void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h new file mode 100644 index 0000000000..2e19c8aeef --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -0,0 +1,30 @@ +#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/switch/switch.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientSwitch : public switch_::Switch, public Component, public BLEClientNode { + public: + 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) override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 1cf8009384..2a242c3aca 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,36 +1,84 @@ 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'] +DEPENDENCIES = ["esp32_ble_tracker"] -ble_presence_ns = cg.esphome_ns.namespace('ble_presence') -BLEPresenceDevice = ble_presence_ns.class_('BLEPresenceDevice', binary_sensor.BinarySensor, - cg.Component, esp32_ble_tracker.ESPBTDeviceListener) - -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, -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( - cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID)) +ble_presence_ns = cg.esphome_ns.namespace("ble_presence") +BLEPresenceDevice = ble_presence_ns.class_( + "BLEPresenceDevice", + binary_sensor.BinarySensor, + cg.Component, + esp32_ble_tracker.ESPBTDeviceListener, +) -def to_code(config): +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, CONF_IBEACON_UUID), + _validate, +) + + +async 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) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + await binary_sensor.register_binary_sensor(var, config) if CONF_MAC_ADDRESS in config: cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) if CONF_SERVICE_UUID in config: if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): - cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + cg.add( + var.set_service_uuid16( + esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) + ) + ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): - cg.add(var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + cg.add( + var.set_service_uuid32( + esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) + ) + ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_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 ad6b5ece88..e482bb9a78 100644 --- a/esphome/components/ble_presence/ble_presence_device.cpp +++ b/esphome/components/ble_presence/ble_presence_device.cpp @@ -1,12 +1,12 @@ #include "ble_presence_device.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_presence { -static const char *TAG = "ble_presence"; +static const char *const TAG = "ble_presence"; void BLEPresenceDevice::dump_config() { LOG_BINARY_SENSOR("", "BLE Presence", this); } 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 6424837770..4b37fcc6ef 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.cpp +++ b/esphome/components/ble_rssi/ble_rssi_sensor.cpp @@ -1,12 +1,12 @@ #include "ble_rssi_sensor.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_rssi { -static const char *TAG = "ble_rssi"; +static const char *const TAG = "ble_rssi"; void BLERSSISensor::dump_config() { LOG_SENSOR("", "BLE RSSI Sensor", this); } 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 76a27e6f2b..0c4308b11a 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -1,36 +1,64 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_SERVICE_UUID, CONF_MAC_ADDRESS, CONF_ID, UNIT_DECIBEL, ICON_SIGNAL +from esphome.const import ( + CONF_SERVICE_UUID, + CONF_MAC_ADDRESS, + CONF_ID, + DEVICE_CLASS_SIGNAL_STRENGTH, + STATE_CLASS_MEASUREMENT, + UNIT_DECIBEL, +) -DEPENDENCIES = ['esp32_ble_tracker'] +DEPENDENCIES = ["esp32_ble_tracker"] -ble_rssi_ns = cg.esphome_ns.namespace('ble_rssi') -BLERSSISensor = ble_rssi_ns.class_('BLERSSISensor', sensor.Sensor, cg.Component, - esp32_ble_tracker.ESPBTDeviceListener) +ble_rssi_ns = cg.esphome_ns.namespace("ble_rssi") +BLERSSISensor = ble_rssi_ns.class_( + "BLERSSISensor", sensor.Sensor, cg.Component, esp32_ble_tracker.ESPBTDeviceListener +) -CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_DECIBEL, ICON_SIGNAL, 0).extend({ - cv.GenerateID(): cv.declare_id(BLERSSISensor), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( - cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID)) +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(BLERSSISensor), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID), +) -def to_code(config): +async 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 sensor.register_sensor(var, config) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + await sensor.register_sensor(var, config) if CONF_MAC_ADDRESS in config: cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) if CONF_SERVICE_UUID in config: if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): - cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + cg.add( + var.set_service_uuid16( + esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) + ) + ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): - cg.add(var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + cg.add( + var.set_service_uuid32( + esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) + ) + ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_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/__init__.py b/esphome/components/ble_scanner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ble_scanner/ble_scanner.cpp b/esphome/components/ble_scanner/ble_scanner.cpp new file mode 100644 index 0000000000..f2cda227bb --- /dev/null +++ b/esphome/components/ble_scanner/ble_scanner.cpp @@ -0,0 +1,16 @@ +#include "ble_scanner.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_scanner { + +static const char *const TAG = "ble_scanner"; + +void BLEScanner::dump_config() { LOG_TEXT_SENSOR("", "BLE Scanner", this); } + +} // namespace ble_scanner +} // namespace esphome + +#endif diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h new file mode 100644 index 0000000000..b330eff696 --- /dev/null +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_scanner { + +class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + this->publish_state("{\"timestamp\":" + to_string(::time(nullptr)) + + "," + "\"address\":\"" + + device.address_str() + + "\"," + "\"rssi\":" + + to_string(device.get_rssi()) + + "," + "\"name\":\"" + + device.get_name() + "\"}"); + + return true; + } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } +}; + +} // namespace ble_scanner +} // namespace esphome + +#endif diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py new file mode 100644 index 0000000000..0e140aa701 --- /dev/null +++ b/esphome/components/ble_scanner/text_sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor, esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ["esp32_ble_tracker"] + +ble_scanner_ns = cg.esphome_ns.namespace("ble_scanner") +BLEScanner = ble_scanner_ns.class_( + "BLEScanner", + text_sensor.TextSensor, + cg.Component, + esp32_ble_tracker.ESPBTDeviceListener, +) + +CONFIG_SCHEMA = cv.All( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLEScanner), + } + ) + .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) + await text_sensor.register_text_sensor(var, config) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index 6bb5ac9800..d3a228328b 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace bme280 { -static const char *TAG = "bme280.sensor"; +static const char *const TAG = "bme280.sensor"; static const uint8_t BME280_REGISTER_DIG_T1 = 0x88; static const uint8_t BME280_REGISTER_DIG_T2 = 0x8A; @@ -33,6 +33,7 @@ static const uint8_t BME280_REGISTER_CONTROLHUMID = 0xF2; static const uint8_t BME280_REGISTER_STATUS = 0xF3; static const uint8_t BME280_REGISTER_CONTROL = 0xF4; static const uint8_t BME280_REGISTER_CONFIG = 0xF5; +static const uint8_t BME280_REGISTER_MEASUREMENTS = 0xF7; static const uint8_t BME280_REGISTER_PRESSUREDATA = 0xF7; static const uint8_t BME280_REGISTER_TEMPDATA = 0xFA; static const uint8_t BME280_REGISTER_HUMIDDATA = 0xFD; @@ -113,14 +114,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,32 +170,38 @@ 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; } - float meas_time = 1.5; + float meas_time = 1.5f; meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f; this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + uint8_t data[8]; + if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { + ESP_LOGW(TAG, "Error reading registers."); + this->status_set_warning(); + return; + } int32_t t_fine = 0; - float temperature = this->read_temperature_(&t_fine); - if (isnan(temperature)) { + float temperature = this->read_temperature_(data, &t_fine); + if (std::isnan(temperature)) { ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); this->status_set_warning(); return; } - float pressure = this->read_pressure_(t_fine); - float humidity = this->read_humidity_(t_fine); + float pressure = this->read_pressure_(data, t_fine); + float humidity = this->read_humidity_(data, t_fine); - ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); + ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) @@ -204,11 +211,8 @@ void BME280Component::update() { this->status_clear_warning(); }); } -float BME280Component::read_temperature_(int32_t *t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BME280_REGISTER_TEMPDATA, data, 3)) - return NAN; - int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); +float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) { + int32_t adc = ((data[3] & 0xFF) << 16) | ((data[4] & 0xFF) << 8) | (data[5] & 0xFF); adc >>= 4; if (adc == 0x80000) // temperature was disabled @@ -226,10 +230,7 @@ float BME280Component::read_temperature_(int32_t *t_fine) { return temperature / 100.0f; } -float BME280Component::read_pressure_(int32_t t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BME280_REGISTER_PRESSUREDATA, data, 3)) - return NAN; +float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; if (adc == 0x80000) @@ -265,9 +266,9 @@ float BME280Component::read_pressure_(int32_t t_fine) { return (p / 256.0f) / 100.0f; } -float BME280Component::read_humidity_(int32_t t_fine) { - uint16_t raw_adc; - if (!this->read_byte_16(BME280_REGISTER_HUMIDDATA, &raw_adc) || raw_adc == 0x8000) +float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { + uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); + if (raw_adc == 0x8000) return NAN; int32_t adc = raw_adc; diff --git a/esphome/components/bme280/bme280.h b/esphome/components/bme280/bme280.h index 82724d6887..8511f73382 100644 --- a/esphome/components/bme280/bme280.h +++ b/esphome/components/bme280/bme280.h @@ -82,11 +82,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice { protected: /// Read the temperature value and store the calculated ambient temperature in t_fine. - float read_temperature_(int32_t *t_fine); + float read_temperature_(const uint8_t *data, int32_t *t_fine); /// Read the pressure value in hPa using the provided t_fine value. - float read_pressure_(int32_t t_fine); + float read_pressure_(const uint8_t *data, int32_t t_fine); /// Read the humidity value in % using the provided t_fine value. - float read_humidity_(int32_t t_fine); + float read_humidity_(const uint8_t *data, int32_t t_fine); uint8_t read_u8_(uint8_t a_register); uint16_t read_u16_le_(uint8_t a_register); int16_t read_s16_le_(uint8_t a_register); diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py index 651752102f..dcb842d879 100644 --- a/esphome/components/bme280/sensor.py +++ b/esphome/components/bme280/sensor.py @@ -1,75 +1,118 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_IIR_FILTER, CONF_OVERSAMPLING, \ - CONF_PRESSURE, CONF_TEMPERATURE, ICON_THERMOMETER, \ - UNIT_CELSIUS, UNIT_HECTOPASCAL, ICON_GAUGE, ICON_WATER_PERCENT, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -bme280_ns = cg.esphome_ns.namespace('bme280') -BME280Oversampling = bme280_ns.enum('BME280Oversampling') +bme280_ns = cg.esphome_ns.namespace("bme280") +BME280Oversampling = bme280_ns.enum("BME280Oversampling") OVERSAMPLING_OPTIONS = { - 'NONE': BME280Oversampling.BME280_OVERSAMPLING_NONE, - '1X': BME280Oversampling.BME280_OVERSAMPLING_1X, - '2X': BME280Oversampling.BME280_OVERSAMPLING_2X, - '4X': BME280Oversampling.BME280_OVERSAMPLING_4X, - '8X': BME280Oversampling.BME280_OVERSAMPLING_8X, - '16X': BME280Oversampling.BME280_OVERSAMPLING_16X, + "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, + "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, + "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, + "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, + "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, + "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, } -BME280IIRFilter = bme280_ns.enum('BME280IIRFilter') +BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") IIR_FILTER_OPTIONS = { - 'OFF': BME280IIRFilter.BME280_IIR_FILTER_OFF, - '2X': BME280IIRFilter.BME280_IIR_FILTER_2X, - '4X': BME280IIRFilter.BME280_IIR_FILTER_4X, - '8X': BME280IIRFilter.BME280_IIR_FILTER_8X, - '16X': BME280IIRFilter.BME280_IIR_FILTER_16X, + "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, + "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, + "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, + "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, + "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, } -BME280Component = bme280_ns.class_('BME280Component', cg.PollingComponent, i2c.I2CDevice) +BME280Component = bme280_ns.class_( + "BME280Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(BME280Component), - cv.Optional(CONF_TEMPERATURE): - sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): - cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_PRESSURE): - sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): - cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_HUMIDITY): - sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): - cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_IIR_FILTER, default='OFF'): cv.enum(IIR_FILTER_OPTIONS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x77)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME280Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: conf = config[CONF_TEMPERATURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) if CONF_PRESSURE in config: conf = config[CONF_PRESSURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_pressure_sensor(sens)) cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) if CONF_HUMIDITY in config: conf = config[CONF_HUMIDITY] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_humidity_sensor(sens)) cg.add(var.set_humidity_oversampling(conf[CONF_OVERSAMPLING])) diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 77f381664b..99e0b6f860 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -1,10 +1,11 @@ #include "bme680.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace bme680 { -static const char *TAG = "bme680.sensor"; +static const char *const TAG = "bme680.sensor"; static const uint8_t BME680_REGISTER_COEFF1 = 0x89; static const uint8_t BME680_REGISTER_COEFF2 = 0xE1; diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index 64973fb91c..76472c7562 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -2,92 +2,155 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core from esphome.components import i2c, sensor -from esphome.const import CONF_DURATION, CONF_GAS_RESISTANCE, CONF_HEATER, \ - CONF_HUMIDITY, CONF_ID, CONF_IIR_FILTER, CONF_OVERSAMPLING, CONF_PRESSURE, \ - CONF_TEMPERATURE, UNIT_OHM, ICON_GAS_CYLINDER, UNIT_CELSIUS, \ - ICON_THERMOMETER, UNIT_HECTOPASCAL, ICON_GAUGE, ICON_WATER_PERCENT, UNIT_PERCENT +from esphome.const import ( + CONF_DURATION, + CONF_GAS_RESISTANCE, + CONF_HEATER, + CONF_HUMIDITY, + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_OHM, + ICON_GAS_CYLINDER, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -bme680_ns = cg.esphome_ns.namespace('bme680') -BME680Oversampling = bme680_ns.enum('BME680Oversampling') +bme680_ns = cg.esphome_ns.namespace("bme680") +BME680Oversampling = bme680_ns.enum("BME680Oversampling") OVERSAMPLING_OPTIONS = { - 'NONE': BME680Oversampling.BME680_OVERSAMPLING_NONE, - '1X': BME680Oversampling.BME680_OVERSAMPLING_1X, - '2X': BME680Oversampling.BME680_OVERSAMPLING_2X, - '4X': BME680Oversampling.BME680_OVERSAMPLING_4X, - '8X': BME680Oversampling.BME680_OVERSAMPLING_8X, - '16X': BME680Oversampling.BME680_OVERSAMPLING_16X, + "NONE": BME680Oversampling.BME680_OVERSAMPLING_NONE, + "1X": BME680Oversampling.BME680_OVERSAMPLING_1X, + "2X": BME680Oversampling.BME680_OVERSAMPLING_2X, + "4X": BME680Oversampling.BME680_OVERSAMPLING_4X, + "8X": BME680Oversampling.BME680_OVERSAMPLING_8X, + "16X": BME680Oversampling.BME680_OVERSAMPLING_16X, } -BME680IIRFilter = bme680_ns.enum('BME680IIRFilter') +BME680IIRFilter = bme680_ns.enum("BME680IIRFilter") IIR_FILTER_OPTIONS = { - 'OFF': BME680IIRFilter.BME680_IIR_FILTER_OFF, - '1X': BME680IIRFilter.BME680_IIR_FILTER_1X, - '3X': BME680IIRFilter.BME680_IIR_FILTER_3X, - '7X': BME680IIRFilter.BME680_IIR_FILTER_7X, - '15X': BME680IIRFilter.BME680_IIR_FILTER_15X, - '31X': BME680IIRFilter.BME680_IIR_FILTER_31X, - '63X': BME680IIRFilter.BME680_IIR_FILTER_63X, - '127X': BME680IIRFilter.BME680_IIR_FILTER_127X, + "OFF": BME680IIRFilter.BME680_IIR_FILTER_OFF, + "1X": BME680IIRFilter.BME680_IIR_FILTER_1X, + "3X": BME680IIRFilter.BME680_IIR_FILTER_3X, + "7X": BME680IIRFilter.BME680_IIR_FILTER_7X, + "15X": BME680IIRFilter.BME680_IIR_FILTER_15X, + "31X": BME680IIRFilter.BME680_IIR_FILTER_31X, + "63X": BME680IIRFilter.BME680_IIR_FILTER_63X, + "127X": BME680IIRFilter.BME680_IIR_FILTER_127X, } -BME680Component = bme680_ns.class_('BME680Component', cg.PollingComponent, i2c.I2CDevice) +BME680Component = bme680_ns.class_( + "BME680Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(BME680Component), - cv.Optional(CONF_TEMPERATURE): - sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): - cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_PRESSURE): - sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): - cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_HUMIDITY): - sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): - cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_GAS_RESISTANCE): - sensor.sensor_schema(UNIT_OHM, ICON_GAS_CYLINDER, 1), - cv.Optional(CONF_IIR_FILTER, default='OFF'): cv.enum(IIR_FILTER_OPTIONS, upper=True), - cv.Optional(CONF_HEATER): cv.Any(None, cv.All(cv.Schema({ - cv.Optional(CONF_TEMPERATURE, default=320): cv.int_range(min=200, max=400), - cv.Optional(CONF_DURATION, default='150ms'): cv.All( - cv.positive_time_period_milliseconds, cv.Range(max=core.TimePeriod(milliseconds=4032))) - }), cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_DURATION))), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x76)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME680Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + 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 + ), + cv.Optional(CONF_HEATER): cv.Any( + None, + cv.All( + cv.Schema( + { + cv.Optional(CONF_TEMPERATURE, default=320): cv.int_range( + min=200, max=400 + ), + cv.Optional(CONF_DURATION, default="150ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=4032)), + ), + } + ), + cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_DURATION), + ), + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x76)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: conf = config[CONF_TEMPERATURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) if CONF_PRESSURE in config: conf = config[CONF_PRESSURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_pressure_sensor(sens)) cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) if CONF_HUMIDITY in config: conf = config[CONF_HUMIDITY] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_humidity_sensor(sens)) cg.add(var.set_humidity_oversampling(conf[CONF_OVERSAMPLING])) if CONF_GAS_RESISTANCE in config: conf = config[CONF_GAS_RESISTANCE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_gas_resistance_sensor(sens)) cg.add(var.set_iir_filter(IIR_FILTER_OPTIONS[config[CONF_IIR_FILTER]])) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py new file mode 100644 index 0000000000..83e519f8aa --- /dev/null +++ b/esphome/components/bme680_bsec/__init__.py @@ -0,0 +1,68 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@trvrnrth"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_BME680_BSEC_ID = "bme680_bsec_id" +CONF_TEMPERATURE_OFFSET = "temperature_offset" +CONF_IAQ_MODE = "iaq_mode" +CONF_SAMPLE_RATE = "sample_rate" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" + +bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec") + +IAQMode = bme680_bsec_ns.enum("IAQMode") +IAQ_MODE_OPTIONS = { + "STATIC": IAQMode.IAQ_MODE_STATIC, + "MOBILE": IAQMode.IAQ_MODE_MOBILE, +} + +SampleRate = bme680_bsec_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +BME680BSECComponent = bme680_bsec_ns.class_( + "BME680BSECComponent", cg.Component, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( + IAQ_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + }, + cv.only_with_arduino, +).extend(i2c.i2c_device_schema(0x76)) + + +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_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + # Although this component does not use SPI, the BSEC library requires the SPI library + cg.add_library("SPI", None) + + cg.add_define("USE_BSEC") + cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp new file mode 100644 index 0000000000..0a8ca7f3c3 --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -0,0 +1,426 @@ +#include "bme680_bsec.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome { +namespace bme680_bsec { +#ifdef USE_BSEC +static const char *const TAG = "bme680_bsec.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +void BME680BSECComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); + BME680BSECComponent::instance = this; + + this->bsec_status_ = bsec_init(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->bme680_.dev_id = this->address_; + this->bme680_.intf = BME680_I2C_INTF; + this->bme680_.read = BME680BSECComponent::read_bytes_wrapper; + this->bme680_.write = BME680BSECComponent::write_bytes_wrapper; + this->bme680_.delay_ms = BME680BSECComponent::delay_ms; + this->bme680_.amb_temp = 25; + + this->bme680_status_ = bme680_init(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + this->mark_failed(); + return; + } + + if (this->sample_rate_ == SAMPLE_RATE_ULP) { + const uint8_t bsec_config[] = { +#include "config/generic_33v_300s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + } else { + const uint8_t bsec_config[] = { +#include "config/generic_33v_3s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + } + this->update_subscription_(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->load_state_(); +} + +void BME680BSECComponent::set_config_(const uint8_t *config) { + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer)); +} + +float BME680BSECComponent::calc_sensor_sample_rate_(SampleRate sample_rate) { + if (sample_rate == SAMPLE_RATE_DEFAULT) { + sample_rate = this->sample_rate_; + } + return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP; +} + +void BME680BSECComponent::update_subscription_() { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + int num_virtual_sensors = 0; + + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = + this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->pressure_sample_rate_); + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->temperature_sample_rate_); + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->humidity_sample_rate_); + num_virtual_sensors++; + } + + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = + bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); +} + +void BME680BSECComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BME680 via BSEC:"); + + bsec_version_t version; + bsec_get_version(&version); + ESP_LOGCONFIG(TAG, " BSEC Version: %d.%d.%d.%d", version.major, version.minor, version.major_bugfix, + version.minor_bugfix); + + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC Status: %d, BME680 Status: %d)", this->bsec_status_, + this->bme680_status_); + } + + ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); + ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_)); + ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->temperature_sample_rate_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->pressure_sample_rate_)); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->humidity_sample_rate_)); + LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_); + LOG_TEXT_SENSOR(" ", "IAQ Accuracy", this->iaq_accuracy_text_sensor_); + LOG_SENSOR(" ", "CO2 Equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); +} + +float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } + +void BME680BSECComponent::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme680_status_ < BME680_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme680_status_ > BME680_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +void BME680BSECComponent::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + + ESP_LOGV(TAG, "Performing sensor run"); + + bsec_bme_settings_t bme680_settings; + this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = bme680_settings.next_call; + + if (bme680_settings.trigger_measurement) { + this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling; + this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling; + this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling; + this->bme680_.gas_sett.run_gas = bme680_settings.run_gas; + this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature; + this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration; + this->bme680_.power_mode = BME680_FORCED_MODE; + uint16_t desired_settings = BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_GAS_SENSOR_SEL; + this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_); + return; + } + + this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; + } + + uint16_t meas_dur = 0; + bme680_get_profile_dur(&meas_dur, &this->bme680_); + ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); + this->set_timeout("read", meas_dur, + [this, curr_time_ns, bme680_settings]() { this->read_(curr_time_ns, bme680_settings); }); + } else { + ESP_LOGV(TAG, "Measurement not required"); + this->read_(curr_time_ns, bme680_settings); + } +} + +void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings) { + ESP_LOGV(TAG, "Reading data"); + + if (bme680_settings.trigger_measurement) { + while (this->bme680_.power_mode != BME680_SLEEP_MODE) { + this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + } + } + } + + if (!bme680_settings.process_data) { + ESP_LOGV(TAG, "Data processing not required"); + return; + } + + struct bme680_field_data data; + this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_); + + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME680 Error Code %d)", this->bme680_status_); + return; + } + if (!(data.status & BME680_NEW_DATA_MSK)) { + ESP_LOGD(TAG, "BME680 did not report new data"); + return; + } + + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + + if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data.temperature / 100.0f; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + + // Temperature offset from the real temperature due to external heat sources + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data.humidity / 1000.0f; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data.pressure; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_GAS) { + if (data.status & BME680_GASM_VALID_MSK) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data.gas_resistance; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } else { + ESP_LOGD(TAG, "BME680 did not report gas data"); + } + } + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC"); + return; + } + + this->publish_(outputs, num_outputs); +} + +void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + for (uint8_t i = 0; i < num_outputs; i++) { + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + case BSEC_OUTPUT_STATIC_IAQ: + uint8_t accuracy; + accuracy = outputs[i].accuracy; + this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); + this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); + this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); + + // Queue up an opportunity to save state + this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: + this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: + this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_RAW_PRESSURE: + this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); + break; + case BSEC_OUTPUT_RAW_GAS: + this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: + this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: + this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); + break; + } + } +} + +int64_t BME680BSECComponent::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME680BSECComponent::delay_ms(uint32_t period) { + ESP_LOGV(TAG, "Delaying for %ums", period); + delay(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); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME680BSECComponent::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = + bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} +#endif +} // namespace bme680_bsec +} // namespace esphome diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h new file mode 100644 index 0000000000..53bc5c3280 --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -0,0 +1,111 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#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 +#include +#endif + +namespace esphome { +namespace bme680_bsec { +#ifdef USE_BSEC + +enum IAQMode { + IAQ_MODE_STATIC = 0, + IAQ_MODE_MOBILE = 1, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, + SAMPLE_RATE_DEFAULT = 2, +}; + +#define BME680_BSEC_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP")) + +class BME680BSECComponent : public Component, public i2c::I2CDevice { + public: + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; } + void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; } + void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; } + + void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; } + void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; } + void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; } + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; } + void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; } + void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; } + + static BME680BSECComponent *instance; + static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static void delay_ms(uint32_t period); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void set_config_(const uint8_t *config); + float calc_sensor_sample_rate_(SampleRate sample_rate); + void update_subscription_(); + + void run_(); + void read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + 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, const std::string &value); + + void load_state_(); + void save_state_(uint8_t accuracy); + + struct bme680_dev bme680_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme680_status_{BME680_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + IAQMode iaq_mode_{IAQ_MODE_STATIC}; + + SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate + SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + sensor::Sensor *humidity_sensor_; + sensor::Sensor *gas_resistance_sensor_; + sensor::Sensor *iaq_sensor_; + text_sensor::TextSensor *iaq_accuracy_text_sensor_; + sensor::Sensor *iaq_accuracy_sensor_; + sensor::Sensor *co2_equivalent_sensor_; + sensor::Sensor *breath_voc_equivalent_sensor_; +}; +#endif +} // namespace bme680_bsec +} // namespace esphome diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py new file mode 100644 index 0000000000..8d00012150 --- /dev/null +++ b/esphome/components/bme680_bsec/sensor.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, +) +from . import ( + BME680BSECComponent, + CONF_BME680_BSEC_ID, + CONF_SAMPLE_RATE, + SAMPLE_RATE_OPTIONS, +) + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ = "iaq" +CONF_IAQ_ACCURACY = "iaq_accuracy" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +UNIT_IAQ = "IAQ" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" + +TYPES = [ + CONF_TEMPERATURE, + CONF_PRESSURE, + CONF_HUMIDITY, + CONF_GAS_RESISTANCE, + CONF_IAQ, + CONF_IAQ_ACCURACY, + CONF_CO2_EQUIVALENT, + CONF_BREATH_VOC_EQUIVALENT, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + + +async def setup_conf(config, key, hub): + if key in config: + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + if CONF_SAMPLE_RATE in conf: + cg.add(getattr(hub, f"set_{key}_sample_rate")(conf[CONF_SAMPLE_RATE])) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py new file mode 100644 index 0000000000..96020544e7 --- /dev/null +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID, CONF_ICON +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ_ACCURACY = "iaq_accuracy" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = [CONF_IAQ_ACCURACY] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon, + } + ), + } +) + + +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_BME680_BSEC_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 88c5313df5..44e686fe1a 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace bmp085 { -static const char *TAG = "bmp085.sensor"; +static const char *const TAG = "bmp085.sensor"; static const uint8_t BMP085_ADDRESS = 0x77; static const uint8_t BMP085_REGISTER_AC1_H = 0xAA; diff --git a/esphome/components/bmp085/sensor.py b/esphome/components/bmp085/sensor.py index 558c6978b1..52f554120a 100644 --- a/esphome/components/bmp085/sensor.py +++ b/esphome/components/bmp085/sensor.py @@ -1,32 +1,58 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, CONF_PRESSURE, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_GAUGE, UNIT_HECTOPASCAL +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -bmp085_ns = cg.esphome_ns.namespace('bmp085') -BMP085Component = bmp085_ns.class_('BMP085Component', cg.PollingComponent, i2c.I2CDevice) +bmp085_ns = cg.esphome_ns.namespace("bmp085") +BMP085Component = bmp085_ns.class_( + "BMP085Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(BMP085Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x77)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP085Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: conf = config[CONF_TEMPERATURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_temperature(sens)) if CONF_PRESSURE in config: conf = config[CONF_PRESSURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_pressure(sens)) diff --git a/esphome/components/bmp280/bmp280.cpp b/esphome/components/bmp280/bmp280.cpp index aed9f3e515..b4348e8a74 100644 --- a/esphome/components/bmp280/bmp280.cpp +++ b/esphome/components/bmp280/bmp280.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace bmp280 { -static const char *TAG = "bmp280.sensor"; +static const char *const TAG = "bmp280.sensor"; static const uint8_t BMP280_REGISTER_STATUS = 0xF3; static const uint8_t BMP280_REGISTER_CONTROL = 0xF4; @@ -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 63c9655331..95a9577f7e 100644 --- a/esphome/components/bmp280/sensor.py +++ b/esphome/components/bmp280/sensor.py @@ -1,59 +1,96 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, CONF_PRESSURE, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_GAUGE, UNIT_HECTOPASCAL, \ - CONF_IIR_FILTER, CONF_OVERSAMPLING +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -bmp280_ns = cg.esphome_ns.namespace('bmp280') -BMP280Oversampling = bmp280_ns.enum('BMP280Oversampling') +bmp280_ns = cg.esphome_ns.namespace("bmp280") +BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling") OVERSAMPLING_OPTIONS = { - 'NONE': BMP280Oversampling.BMP280_OVERSAMPLING_NONE, - '1X': BMP280Oversampling.BMP280_OVERSAMPLING_1X, - '2X': BMP280Oversampling.BMP280_OVERSAMPLING_2X, - '4X': BMP280Oversampling.BMP280_OVERSAMPLING_4X, - '8X': BMP280Oversampling.BMP280_OVERSAMPLING_8X, - '16X': BMP280Oversampling.BMP280_OVERSAMPLING_16X, + "NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE, + "1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X, + "2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X, + "4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X, + "8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X, + "16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X, } -BMP280IIRFilter = bmp280_ns.enum('BMP280IIRFilter') +BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter") IIR_FILTER_OPTIONS = { - 'OFF': BMP280IIRFilter.BMP280_IIR_FILTER_OFF, - '2X': BMP280IIRFilter.BMP280_IIR_FILTER_2X, - '4X': BMP280IIRFilter.BMP280_IIR_FILTER_4X, - '8X': BMP280IIRFilter.BMP280_IIR_FILTER_8X, - '16X': BMP280IIRFilter.BMP280_IIR_FILTER_16X, + "OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF, + "2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X, + "4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X, + "8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X, + "16X": BMP280IIRFilter.BMP280_IIR_FILTER_16X, } -BMP280Component = bmp280_ns.class_('BMP280Component', cg.PollingComponent, i2c.I2CDevice) +BMP280Component = bmp280_ns.class_( + "BMP280Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(BMP280Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 1).extend({ - cv.Optional(CONF_OVERSAMPLING, default='16X'): cv.enum(OVERSAMPLING_OPTIONS, upper=True), - }), - cv.Optional(CONF_IIR_FILTER, default='OFF'): cv.enum(IIR_FILTER_OPTIONS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x77)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP280Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: conf = config[CONF_TEMPERATURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) if CONF_PRESSURE in config: conf = config[CONF_PRESSURE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_pressure_sensor(sens)) cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py new file mode 100644 index 0000000000..1e248ddf07 --- /dev/null +++ b/esphome/components/button/__init__.py @@ -0,0 +1,127 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.components import mqtt +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_ON_PRESS, + CONF_TRIGGER_ID, + CONF_MQTT_ID, + DEVICE_CLASS_RESTART, + DEVICE_CLASS_UPDATE, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +DEVICE_CLASSES = [ + DEVICE_CLASS_RESTART, + DEVICE_CLASS_UPDATE, +] + +button_ns = cg.esphome_ns.namespace("button") +Button = button_ns.class_("Button", cg.EntityBase) +ButtonPtr = Button.operator("ptr") + +PressAction = button_ns.class_("PressAction", automation.Action) + +ButtonPressTrigger = button_ns.class_( + "ButtonPressTrigger", automation.Trigger.template() +) + +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + + +BUTTON_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_ON_PRESS): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger), + } + ), + } +) + +_UNDEF = object() + + +def button_schema( + icon: str = _UNDEF, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, +) -> cv.Schema: + schema = BUTTON_SCHEMA + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) + return schema + + +async def setup_button_core_(var, config): + await setup_entity(var, config) + + for conf in config.get(CONF_ON_PRESS, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + if CONF_DEVICE_CLASS in config: + cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_button(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_button(var)) + await setup_button_core_(var, config) + + +async def new_button(config): + var = cg.new_Pvariable(config[CONF_ID]) + await register_button(var, config) + return var + + +BUTTON_PRESS_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Button), + } +) + + +@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA) +async def button_press_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(button_ns.using) + cg.add_define("USE_BUTTON") diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h new file mode 100644 index 0000000000..a5fb9f35b7 --- /dev/null +++ b/esphome/components/button/automation.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace button { + +template class PressAction : public Action { + public: + explicit PressAction(Button *button) : button_(button) {} + + void play(Ts... x) override { this->button_->press(); } + + protected: + Button *button_; +}; + +class ButtonPressTrigger : public Trigger<> { + public: + ButtonPressTrigger(Button *button) { + button->add_on_press_callback([this]() { this->trigger(); }); + } +}; + +} // namespace button +} // namespace esphome diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp new file mode 100644 index 0000000000..d57b46e9aa --- /dev/null +++ b/esphome/components/button/button.cpp @@ -0,0 +1,28 @@ +#include "button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace button { + +static const char *const TAG = "button"; + +Button::Button(const std::string &name) : EntityBase(name) {} +Button::Button() : Button("") {} + +void Button::press() { + ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); + this->press_action(); + this->press_callback_.call(); +} +void Button::add_on_press_callback(std::function &&callback) { this->press_callback_.add(std::move(callback)); } +uint32_t Button::hash_base() { return 1495763804UL; } + +void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } +std::string Button::get_device_class() { + if (this->device_class_.has_value()) + return *this->device_class_; + return ""; +} + +} // namespace button +} // namespace esphome diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h new file mode 100644 index 0000000000..b21a96b8e1 --- /dev/null +++ b/esphome/components/button/button.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace button { + +#define LOG_BUTTON(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +/** Base class for all buttons. + * + * A button is just a momentary switch that does not have a state, only a trigger. + */ +class Button : public EntityBase { + public: + explicit Button(); + explicit Button(const std::string &name); + + /** Press this button. This is called by the front-end. + * + * For implementing buttons, please override press_action. + */ + void press(); + + /** Set callback for state changes. + * + * @param callback The void() callback. + */ + void add_on_press_callback(std::function &&callback); + + /// Set the Home Assistant device class (see button::device_class). + void set_device_class(const std::string &device_class); + + /// Get the device class for this button. + std::string get_device_class(); + + protected: + /** You should implement this virtual method if you want to create your own button. + */ + virtual void press_action(){}; + + uint32_t hash_base() override; + + CallbackManager press_callback_{}; + optional device_class_{}; +}; + +} // namespace button +} // namespace esphome diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py new file mode 100644 index 0000000000..3a3cece579 --- /dev/null +++ b/esphome/components/canbus/__init__.py @@ -0,0 +1,145 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.core import CORE +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_DATA + +CODEOWNERS = ["@mvturnho", "@danielschramm"] +IS_PLATFORM_COMPONENT = True + +CONF_CAN_ID = "can_id" +CONF_USE_EXTENDED_ID = "use_extended_id" +CONF_CANBUS_ID = "canbus_id" +CONF_BIT_RATE = "bit_rate" +CONF_ON_FRAME = "on_frame" + + +def validate_id(id_value, id_ext): + if not id_ext: + if id_value > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +canbus_ns = cg.esphome_ns.namespace("canbus") +CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component) +CanbusTrigger = canbus_ns.class_( + "CanbusTrigger", + automation.Trigger.template(cg.std_vector.template(cg.uint8)), + cg.Component, +) +CanSpeed = canbus_ns.enum("CAN_SPEED") + +CAN_SPEEDS = { + "5KBPS": CanSpeed.CAN_5KBPS, + "10KBPS": CanSpeed.CAN_10KBPS, + "20KBPS": CanSpeed.CAN_20KBPS, + "31K25BPS": CanSpeed.CAN_31K25BPS, + "33KBPS": CanSpeed.CAN_33KBPS, + "40KBPS": CanSpeed.CAN_40KBPS, + "50KBPS": CanSpeed.CAN_50KBPS, + "80KBPS": CanSpeed.CAN_80KBPS, + "83K3BPS": CanSpeed.CAN_83K3BPS, + "95KBPS": CanSpeed.CAN_95KBPS, + "100KBPS": CanSpeed.CAN_100KBPS, + "125KBPS": CanSpeed.CAN_125KBPS, + "200KBPS": CanSpeed.CAN_200KBPS, + "250KBPS": CanSpeed.CAN_250KBPS, + "500KBPS": CanSpeed.CAN_500KBPS, + "1000KBPS": CanSpeed.CAN_1000KBPS, +} + +CANBUS_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CanbusComponent), + cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_ON_FRAME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), + cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_ON_FRAME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), + cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + } + ), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def setup_canbus_core_(var, config): + validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) + await cg.register_component(var, config) + cg.add(var.set_can_id([config[CONF_CAN_ID]])) + cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]])) + cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]])) + + for conf in config.get(CONF_ON_FRAME, []): + can_id = conf[CONF_CAN_ID] + ext_id = conf[CONF_USE_EXTENDED_ID] + validate_id(can_id, ext_id) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id) + await cg.register_component(trigger, conf) + await automation.build_automation( + trigger, [(cg.std_vector.template(cg.uint8), "x")], conf + ) + + +async def register_canbus(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.new_Pvariable(config[CONF_ID], var) + await setup_canbus_core_(var, config) + + +# Actions +@automation.register_action( + "canbus.send", + canbus_ns.class_("CanbusSendAction", automation.Action), + cv.maybe_simple_value( + { + cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent), + cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, + ), +) +async def canbus_action_to_code(config, action_id, template_arg, args): + validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_CANBUS_ID]) + + if CONF_CAN_ID in config: + can_id = await cg.templatable(config[CONF_CAN_ID], args, cg.uint32) + cg.add(var.set_can_id(can_id)) + + use_extended_id = await cg.templatable( + config[CONF_USE_EXTENDED_ID], args, cg.uint32 + ) + cg.add(var.set_use_extended_id(use_extended_id)) + + data = config[CONF_DATA] + if isinstance(data, bytes): + data = [int(x) for x in data] + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + return var diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp new file mode 100644 index 0000000000..b8b6b9e65f --- /dev/null +++ b/esphome/components/canbus/canbus.cpp @@ -0,0 +1,87 @@ +#include "canbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace canbus { + +static const char *const TAG = "canbus"; + +void Canbus::setup() { + ESP_LOGCONFIG(TAG, "Setting up Canbus..."); + if (!this->setup_internal()) { + ESP_LOGE(TAG, "setup error!"); + this->mark_failed(); + } +} + +void Canbus::dump_config() { + if (this->use_extended_id_) { + ESP_LOGCONFIG(TAG, "config extended id=0x%08x", this->can_id_); + } else { + ESP_LOGCONFIG(TAG, "config standard id=0x%03x", this->can_id_); + } +} + +void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { + struct CanFrame can_message; + + uint8_t size = static_cast(data.size()); + if (use_extended_id) { + ESP_LOGD(TAG, "send extended id=0x%08x size=%d", can_id, size); + } else { + ESP_LOGD(TAG, "send extended id=0x%03x size=%d", can_id, size); + } + if (size > CAN_MAX_DATA_LENGTH) + size = CAN_MAX_DATA_LENGTH; + can_message.can_data_length_code = size; + can_message.can_id = can_id; + can_message.use_extended_id = use_extended_id; + + for (int i = 0; i < size; i++) { + can_message.data[i] = data[i]; + ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]); + } + + this->send_message(&can_message); +} + +void Canbus::add_trigger(CanbusTrigger *trigger) { + if (trigger->use_extended_id_) { + ESP_LOGVV(TAG, "add trigger for extended canid=0x%08x", trigger->can_id_); + } else { + ESP_LOGVV(TAG, "add trigger for std canid=0x%03x", trigger->can_id_); + } + this->triggers_.push_back(trigger); +}; + +void Canbus::loop() { + struct CanFrame can_message; + // readmessage + if (this->read_message(&can_message) == canbus::ERROR_OK) { + if (can_message.use_extended_id) { + ESP_LOGD(TAG, "received can message extended can_id=0x%x size=%d", can_message.can_id, + can_message.can_data_length_code); + } else { + ESP_LOGD(TAG, "received can message std can_id=0x%x size=%d", can_message.can_id, + can_message.can_data_length_code); + } + + std::vector data; + + // show data received + for (int i = 0; i < can_message.can_data_length_code; i++) { + ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); + data.push_back(can_message.data[i]); + } + + // fire all triggers + for (auto trigger : this->triggers_) { + if ((trigger->can_id_ == can_message.can_id) && (trigger->use_extended_id_ == can_message.use_extended_id)) { + trigger->trigger(data); + } + } + } +} + +} // namespace canbus +} // namespace esphome diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h new file mode 100644 index 0000000000..37adf0bc9c --- /dev/null +++ b/esphome/components/canbus/canbus.h @@ -0,0 +1,134 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" + +namespace esphome { +namespace canbus { + +enum Error : uint8_t { + ERROR_OK = 0, + ERROR_FAIL = 1, + ERROR_ALLTXBUSY = 2, + ERROR_FAILINIT = 3, + ERROR_FAILTX = 4, + ERROR_NOMSG = 5 +}; + +enum CanSpeed : uint8_t { + CAN_5KBPS, + CAN_10KBPS, + CAN_20KBPS, + CAN_31K25BPS, + CAN_33KBPS, + CAN_40KBPS, + CAN_50KBPS, + CAN_80KBPS, + CAN_83K3BPS, + CAN_95KBPS, + CAN_100KBPS, + CAN_125KBPS, + CAN_200KBPS, + CAN_250KBPS, + CAN_500KBPS, + CAN_1000KBPS +}; + +class CanbusTrigger; +template class CanbusSendAction; + +/* CAN payload length definitions according to ISO 11898-1 */ +static const uint8_t CAN_MAX_DATA_LENGTH = 8; + +/* +Can Frame describes a normative CAN Frame +The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used +So currently the flag is passed to and from the hardware but currently ignored to the user application. +*/ +struct CanFrame { + bool use_extended_id = false; + bool remote_transmission_request = false; + uint32_t can_id; /* 29 or 11 bit CAN_ID */ + uint8_t can_data_length_code; /* frame payload length in byte (0 .. CAN_MAX_DATA_LENGTH) */ + uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8))); +}; + +class Canbus : public Component { + public: + Canbus(){}; + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + void send_data(uint32_t can_id, bool use_extended_id, const std::vector &data); + void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } + void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } + + void add_trigger(CanbusTrigger *trigger); + + protected: + template friend class CanbusSendAction; + std::vector triggers_{}; + uint32_t can_id_; + bool use_extended_id_; + CanSpeed bit_rate_; + + virtual bool setup_internal(); + virtual Error send_message(struct CanFrame *frame); + virtual Error read_message(struct CanFrame *frame); +}; + +template class CanbusSendAction : public Action, public Parented { + public: + void set_data_template(const std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } + + void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + + void play(Ts... x) override { + auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; + auto use_extended_id = + this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; + if (this->static_) { + this->parent_->send_data(can_id, use_extended_id, this->data_static_); + } else { + auto val = this->data_func_(x...); + this->parent_->send_data(can_id, use_extended_id, val); + } + } + + protected: + optional can_id_{}; + optional use_extended_id_{}; + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +class CanbusTrigger : public Trigger>, public Component { + friend class Canbus; + + public: + explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const bool use_extended_id) + : parent_(parent), can_id_(can_id), use_extended_id_(use_extended_id){}; + void setup() override { this->parent_->add_trigger(this); } + + protected: + Canbus *parent_; + uint32_t can_id_; + bool use_extended_id_; +}; + +} // namespace canbus +} // namespace esphome diff --git a/esphome/components/cap1188/__init__.py b/esphome/components/cap1188/__init__.py new file mode 100644 index 0000000000..80794c5146 --- /dev/null +++ b/esphome/components/cap1188/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_RESET_PIN +from esphome import pins + +CONF_TOUCH_THRESHOLD = "touch_threshold" +CONF_ALLOW_MULTIPLE_TOUCHES = "allow_multiple_touches" + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["binary_sensor", "output"] +CODEOWNERS = ["@MrEditor97"] + +cap1188_ns = cg.esphome_ns.namespace("cap1188") +CONF_CAP1188_ID = "cap1188_id" +CAP1188Component = cap1188_ns.class_("CAP1188Component", cg.Component, i2c.I2CDevice) + +MULTI_CONF = True +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CAP1188Component), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_TOUCH_THRESHOLD, default=0x20): cv.int_range( + min=0x01, max=0x80 + ), + cv.Optional(CONF_ALLOW_MULTIPLE_TOUCHES, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x29)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) + cg.add(var.set_allow_multiple_touches(config[CONF_ALLOW_MULTIPLE_TOUCHES])) + + if CONF_RESET_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/cap1188/binary_sensor.py b/esphome/components/cap1188/binary_sensor.py new file mode 100644 index 0000000000..c249eb7330 --- /dev/null +++ b/esphome/components/cap1188/binary_sensor.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_CHANNEL, CONF_ID +from . import cap1188_ns, CAP1188Component, CONF_CAP1188_ID + +DEPENDENCIES = ["cap1188"] +CAP1188Channel = cap1188_ns.class_("CAP1188Channel", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CAP1188Channel), + cv.GenerateID(CONF_CAP1188_ID): cv.use_id(CAP1188Component), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +) + + +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_CAP1188_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + + cg.add(hub.register_channel(var)) diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp new file mode 100644 index 0000000000..10d8325537 --- /dev/null +++ b/esphome/components/cap1188/cap1188.cpp @@ -0,0 +1,88 @@ +#include "cap1188.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace cap1188 { + +static const char *const TAG = "cap1188"; + +void CAP1188Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up CAP1188..."); + + // Reset device using the reset pin + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + delay(100); // NOLINT + this->reset_pin_->digital_write(true); + delay(100); // NOLINT + this->reset_pin_->digital_write(false); + delay(100); // NOLINT + } + + // Check if CAP1188 is actually connected + this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_); + this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_); + this->read_byte(CAP1188_REVISION, &this->cap1188_revision_); + + if ((this->cap1188_product_id_ != 0x50) || (this->cap1188_manufacture_id_ != 0x5D)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + // Set sensitivity + uint8_t sensitivity = 0; + this->read_byte(CAP1188_SENSITVITY, &sensitivity); + sensitivity = sensitivity & 0x0f; + this->write_byte(CAP1188_SENSITVITY, sensitivity | this->touch_threshold_); + + // Allow multiple touches + this->write_byte(CAP1188_MULTI_TOUCH, this->allow_multiple_touches_); + + // Have LEDs follow touches + this->write_byte(CAP1188_LED_LINK, 0xFF); + + // Speed up a bit + this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30); +} + +void CAP1188Component::dump_config() { + ESP_LOGCONFIG(TAG, "CAP1188:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Product ID: 0x%x", this->cap1188_product_id_); + ESP_LOGCONFIG(TAG, " Manufacture ID: 0x%x", this->cap1188_manufacture_id_); + ESP_LOGCONFIG(TAG, " Revision ID: 0x%x", this->cap1188_revision_); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Product ID or Manufacture ID of the connected device does not match a known CAP1188."); + break; + case NONE: + default: + break; + } +} + +void CAP1188Component::loop() { + uint8_t touched = 0; + + this->read_register(CAP1188_SENSOR_INPUT_STATUS, &touched, 1); + + if (touched) { + uint8_t data = 0; + this->read_register(CAP1188_MAIN, &data, 1); + data = data & ~CAP1188_MAIN_INT; + + this->write_register(CAP1188_MAIN, &data, 2); + } + + for (auto *channel : this->channels_) { + channel->process(touched); + } +} + +} // namespace cap1188 +} // namespace esphome diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h new file mode 100644 index 0000000000..a1433deb0f --- /dev/null +++ b/esphome/components/cap1188/cap1188.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace cap1188 { + +enum { + CAP1188_I2CADDR = 0x29, + CAP1188_SENSOR_INPUT_STATUS = 0x3, + CAP1188_MULTI_TOUCH = 0x2A, + CAP1188_LED_LINK = 0x72, + CAP1188_PRODUCT_ID = 0xFD, + CAP1188_MANUFACTURE_ID = 0xFE, + CAP1188_STAND_BY_CONFIGURATION = 0x41, + CAP1188_REVISION = 0xFF, + CAP1188_MAIN = 0x00, + CAP1188_MAIN_INT = 0x01, + CAP1188_LEDPOL = 0x73, + CAP1188_INTERUPT_REPEAT = 0x28, + CAP1188_SENSITVITY = 0x1f, +}; + +class CAP1188Channel : public binary_sensor::BinarySensor { + public: + void set_channel(uint8_t channel) { channel_ = channel; } + void process(uint8_t data) { this->publish_state(static_cast(data & (1 << this->channel_))); } + + protected: + uint8_t channel_{0}; +}; + +class CAP1188Component : public Component, public i2c::I2CDevice { + public: + void register_channel(CAP1188Channel *channel) { this->channels_.push_back(channel); } + void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; + void set_allow_multiple_touches(bool allow_multiple_touches) { + this->allow_multiple_touches_ = allow_multiple_touches ? 0x41 : 0x80; + }; + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + protected: + std::vector channels_{}; + uint8_t touch_threshold_{0x20}; + uint8_t allow_multiple_touches_{0x80}; + + GPIOPin *reset_pin_{nullptr}; + + uint8_t cap1188_product_id_{0}; + uint8_t cap1188_manufacture_id_{0}; + uint8_t cap1188_revision_{0}; + + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + } error_code_{NONE}; +}; + +} // namespace cap1188 +} // namespace esphome diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 52885ae449..f024c94b01 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -3,24 +3,38 @@ 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'] +AUTO_LOAD = ["web_server_base"] +DEPENDENCIES = ["wifi"] +CODEOWNERS = ["@OttoWinter"] -captive_portal_ns = cg.esphome_ns.namespace('captive_portal') -CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component) +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) -def to_code(config): - paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) +async def to_code(config): + paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) - yield cg.register_component(var, config) - cg.add_define('USE_CAPTIVE_PORTAL') + await cg.register_component(var, config) + cg.add_define("USE_CAPTIVE_PORTAL") + + if CORE.is_esp32: + cg.add_library("DNSServer", None) + cg.add_library("WiFi", None) + if CORE.is_esp8266: + cg.add_library("DNSServer", None) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 83f85d354c..d4e37f62f2 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" @@ -6,7 +8,7 @@ namespace esphome { namespace captive_portal { -static const char *TAG = "captive_portal"; +static const char *const TAG = "captive_portal"; void CaptivePortal::handle_index(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); @@ -64,32 +66,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - this->override_sta_(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->start_scanning(); request->redirect("/?save=true"); } -void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) { - CaptivePortalSettings save{}; - strcpy(save.ssid, ssid.c_str()); - strcpy(save.password, password.c_str()); - this->pref_.save(&save); - wifi::WiFiAP sta{}; - sta.set_ssid(ssid); - sta.set_password(password); - wifi::global_wifi_component->set_sta(sta); -} - -void CaptivePortal::setup() { - // Hash with compilation time - // This ensures the AP override is not applied for OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); - this->pref_ = global_preferences.make_preference(hash, true); - - CaptivePortalSettings save{}; - if (this->pref_.load(&save)) { - this->override_sta_(save.ssid, save.password); - } -} +void CaptivePortal::setup() {} void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { @@ -97,26 +79,19 @@ 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()) { - not_found = true; - } - - if (not_found) { + if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { req->send(404, "text/html", "File not found"); 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; @@ -168,7 +143,9 @@ float CaptivePortal::get_setup_priority() const { } void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); } -CaptivePortal *global_captive_portal = nullptr; +CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // 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 3af47546cf..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" @@ -10,11 +13,6 @@ namespace esphome { namespace captive_portal { -struct CaptivePortalSettings { - char ssid[33]; - char password[65]; -} PACKED; // NOLINT - class CaptivePortal : public AsyncWebHandler, public Component { public: CaptivePortal(web_server_base::WebServerBase *base); @@ -31,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 { @@ -67,16 +65,15 @@ class CaptivePortal : public AsyncWebHandler, public Component { void handleRequest(AsyncWebServerRequest *req) override; protected: - void override_sta_(const std::string &ssid, const std::string &password); - web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; - ESPPreferenceObject pref_; - DNSServer *dns_server_{nullptr}; + std::unique_ptr dns_server_{nullptr}; }; -extern CaptivePortal *global_captive_portal; +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 538d7fe1f5..f8cee79c55 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -1,10 +1,11 @@ #include "ccs811.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ccs811 { -static const char *TAG = "ccs811"; +static const char *const TAG = "ccs811"; // based on // - https://cdn.sparkfun.com/datasheets/BreakoutBoards/CCS811_Programming_Guide.pdf @@ -16,7 +17,7 @@ static const char *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,23 +39,58 @@ 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)) if (this->baseline_.has_value()) { // baseline available, write to sensor - this->write_bytes(0x11, decode_uint16(*this->baseline_)); + this->write_bytes(0x11, decode_value(*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,20 +128,24 @@ 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; - // only 0.5 fractions are supported (application note) - auto hum_value = static_cast(roundf(humidity * 2)); - auto temp_value = static_cast(roundf(temperature * 2)); - this->write_bytes(0x05, {hum_value, 0x00, temp_value, 0x00}); + // At page 18 of: + // https://cdn.sparkfun.com/datasheets/BreakoutBoards/CCS811_Programming_Guide.pdf + // Reference code: + // https://github.com/adafruit/Adafruit_CCS811/blob/0990f5c620354d8bc087c4706bec091d8e6e5dfd/Adafruit_CCS811.cpp#L135-L142 + uint16_t hum_conv = static_cast(lroundf(humidity * 512.0f + 0.5f)); + uint16_t temp_conv = static_cast(lroundf(temperature * 512.0f + 0.5f)); + this->write_bytes(0x05, {(uint8_t)((hum_conv >> 8) & 0xff), (uint8_t)((hum_conv & 0xff)), + (uint8_t)((temp_conv >> 8) & 0xff), (uint8_t)((temp_conv & 0xff))}); } void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); @@ -113,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 { @@ -120,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 a8020c77f7..bb8200273d 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -1,46 +1,89 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_HUMIDITY, ICON_PERIODIC_TABLE_CO2 +from esphome.components import i2c, sensor, text_sensor +from esphome.const import ( + CONF_ICON, + CONF_ID, + 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, +) -DEPENDENCIES = ['i2c'] +AUTO_LOAD = ["text_sensor"] +CODEOWNERS = ["@habbie"] +DEPENDENCIES = ["i2c"] -ccs811_ns = cg.esphome_ns.namespace('ccs811') -CCS811Component = ccs811_ns.class_('CCS811Component', cg.PollingComponent, i2c.I2CDevice) +ccs811_ns = cg.esphome_ns.namespace("ccs811") +CCS811Component = ccs811_ns.class_( + "CCS811Component", cg.PollingComponent, i2c.I2CDevice +) -CONF_ECO2 = 'eco2' -CONF_TVOC = 'tvoc' -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_PERIODIC_TABLE_CO2, - 0), - cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), - - cv.Optional(CONF_BASELINE): cv.hex_uint16_t, - cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), - cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x5A)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CCS811Component), + cv.Required(CONF_ECO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + 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), + cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5A)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) - sens = yield sensor.new_sensor(config[CONF_ECO2]) + sens = await sensor.new_sensor(config[CONF_ECO2]) cg.add(var.set_co2(sens)) - sens = yield sensor.new_sensor(config[CONF_TVOC]) + 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])) if CONF_TEMPERATURE in config: - sens = yield cg.get_variable(config[CONF_TEMPERATURE]) + sens = await cg.get_variable(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) if CONF_HUMIDITY in config: - sens = yield cg.get_variable(config[CONF_HUMIDITY]) + sens = await cg.get_variable(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 843b888218..87b9a4b3e2 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -1,77 +1,181 @@ 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_AWAY, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATURE, \ - CONF_MIN_TEMPERATURE, CONF_MODE, CONF_TARGET_TEMPERATURE, \ - CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL, \ - CONF_MQTT_ID, CONF_NAME, CONF_FAN_MODE, CONF_SWING_MODE -from esphome.core import CORE, coroutine, coroutine_with_priority +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_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_MODE, + CONF_MODE_COMMAND_TOPIC, + CONF_MODE_STATE_TOPIC, + CONF_ON_STATE, + 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_TRIGGER_ID, + CONF_VISUAL, + CONF_MQTT_ID, +) +from esphome.core import CORE, coroutine_with_priority IS_PLATFORM_COMPONENT = True -climate_ns = cg.esphome_ns.namespace('climate') +CODEOWNERS = ["@esphome/core"] +climate_ns = cg.esphome_ns.namespace("climate") -Climate = climate_ns.class_('Climate', cg.Nameable) -ClimateCall = climate_ns.class_('ClimateCall') -ClimateTraits = climate_ns.class_('ClimateTraits') +Climate = climate_ns.class_("Climate", cg.EntityBase) +ClimateCall = climate_ns.class_("ClimateCall") +ClimateTraits = climate_ns.class_("ClimateTraits") -ClimateMode = climate_ns.enum('ClimateMode') +ClimateMode = climate_ns.enum("ClimateMode") CLIMATE_MODES = { - 'OFF': ClimateMode.CLIMATE_MODE_OFF, - 'AUTO': ClimateMode.CLIMATE_MODE_AUTO, - 'COOL': ClimateMode.CLIMATE_MODE_COOL, - 'HEAT': ClimateMode.CLIMATE_MODE_HEAT, - 'DRY': ClimateMode.CLIMATE_MODE_DRY, - 'FAN_ONLY': ClimateMode.CLIMATE_MODE_FAN_ONLY, + "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) -ClimateFanMode = climate_ns.enum('ClimateFanMode') +ClimateFanMode = climate_ns.enum("ClimateFanMode") CLIMATE_FAN_MODES = { - 'ON': ClimateFanMode.CLIMATE_FAN_ON, - 'OFF': ClimateFanMode.CLIMATE_FAN_OFF, - 'AUTO': ClimateFanMode.CLIMATE_FAN_AUTO, - 'LOW': ClimateFanMode.CLIMATE_FAN_LOW, - 'MEDIUM': ClimateFanMode.CLIMATE_FAN_MEDIUM, - 'HIGH': ClimateFanMode.CLIMATE_FAN_HIGH, - 'MIDDLE': ClimateFanMode.CLIMATE_FAN_MIDDLE, - 'FOCUS': ClimateFanMode.CLIMATE_FAN_FOCUS, - 'DIFFUSE': ClimateFanMode.CLIMATE_FAN_DIFFUSE, + "ON": ClimateFanMode.CLIMATE_FAN_ON, + "OFF": ClimateFanMode.CLIMATE_FAN_OFF, + "AUTO": ClimateFanMode.CLIMATE_FAN_AUTO, + "LOW": ClimateFanMode.CLIMATE_FAN_LOW, + "MEDIUM": ClimateFanMode.CLIMATE_FAN_MEDIUM, + "HIGH": ClimateFanMode.CLIMATE_FAN_HIGH, + "MIDDLE": ClimateFanMode.CLIMATE_FAN_MIDDLE, + "FOCUS": ClimateFanMode.CLIMATE_FAN_FOCUS, + "DIFFUSE": ClimateFanMode.CLIMATE_FAN_DIFFUSE, } validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) -ClimateSwingMode = climate_ns.enum('ClimateSwingMode') +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, + "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, + "HOME": ClimatePreset.CLIMATE_PRESET_HOME, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, + "ACTIVITY": ClimatePreset.CLIMATE_PRESET_ACTIVITY, +} + +validate_climate_preset = cv.enum(CLIMATE_PRESETS, upper=True) + +ClimateSwingMode = climate_ns.enum("ClimateSwingMode") CLIMATE_SWING_MODES = { - 'OFF': ClimateSwingMode.CLIMATE_SWING_OFF, - 'BOTH': ClimateSwingMode.CLIMATE_SWING_BOTH, - 'VERTICAL': ClimateSwingMode.CLIMATE_SWING_VERTICAL, - 'HORIZONTAL': ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, } validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions -ControlAction = climate_ns.class_('ControlAction', automation.Action) +ControlAction = climate_ns.class_("ControlAction", automation.Action) +StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) -CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(Climate), - cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTClimateComponent), - cv.Optional(CONF_VISUAL, default={}): cv.Schema({ - cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, - cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, - cv.Optional(CONF_TEMPERATURE_STEP): cv.temperature, - }), - # TODO: MQTT topic options -}) +CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(Climate), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), + cv.Optional(CONF_VISUAL, default={}): cv.Schema( + { + cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, + cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, + cv.Optional(CONF_TEMPERATURE_STEP): cv.temperature, + } + ), + 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 + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + } +) -@coroutine -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])) +async def setup_climate_core_(var, config): + await setup_entity(var, config) + visual = config[CONF_VISUAL] if CONF_MIN_TEMPERATURE in visual: cg.add(var.set_visual_min_temperature_override(visual[CONF_MIN_TEMPERATURE])) @@ -82,58 +186,161 @@ def setup_climate_core_(var, config): if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) - yield mqtt.register_mqtt_component(mqtt_, config) + 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_mode_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] + ) + ) + + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) -@coroutine -def register_climate(var, config): +async def register_climate(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_climate(var)) - yield setup_climate_core_(var, config) + await setup_climate_core_(var, config) -CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.use_id(Climate), - cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode), - cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), - cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), - cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), - cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), - cv.Optional(CONF_FAN_MODE): cv.templatable(validate_climate_fan_mode), - cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), -}) +CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Climate), + cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), + cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( + validate_climate_fan_mode + ), + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict, + cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset), + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict, + cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), + } +) -@automation.register_action('climate.control', ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA) -def climate_control_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "climate.control", ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA +) +async def climate_control_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_MODE in config: - template_ = yield cg.templatable(config[CONF_MODE], args, ClimateMode) + template_ = await cg.templatable(config[CONF_MODE], args, ClimateMode) cg.add(var.set_mode(template_)) if CONF_TARGET_TEMPERATURE in config: - template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE], args, float) + template_ = await cg.templatable(config[CONF_TARGET_TEMPERATURE], args, float) cg.add(var.set_target_temperature(template_)) if CONF_TARGET_TEMPERATURE_LOW in config: - template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE_LOW], args, float) + template_ = await cg.templatable( + config[CONF_TARGET_TEMPERATURE_LOW], args, float + ) cg.add(var.set_target_temperature_low(template_)) if CONF_TARGET_TEMPERATURE_HIGH in config: - template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE_HIGH], args, float) + template_ = await cg.templatable( + config[CONF_TARGET_TEMPERATURE_HIGH], args, float + ) cg.add(var.set_target_temperature_high(template_)) if CONF_AWAY in config: - template_ = yield cg.templatable(config[CONF_AWAY], args, bool) + template_ = await cg.templatable(config[CONF_AWAY], args, bool) cg.add(var.set_away(template_)) if CONF_FAN_MODE in config: - template_ = yield cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) + template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) + if CONF_CUSTOM_FAN_MODE in config: + template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str) + cg.add(var.set_custom_fan_mode(template_)) + if CONF_PRESET in config: + template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset) + cg.add(var.set_preset(template_)) + if CONF_CUSTOM_PRESET in config: + template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str) + cg.add(var.set_custom_preset(template_)) if CONF_SWING_MODE in config: - template_ = yield cg.templatable(config[CONF_SWING_MODE], args, ClimateSwingMode) + template_ = await cg.templatable( + config[CONF_SWING_MODE], args, ClimateSwingMode + ) cg.add(var.set_swing_mode(template_)) - yield var + return var @coroutine_with_priority(100.0) -def to_code(config): - cg.add_define('USE_CLIMATE') +async def to_code(config): + cg.add_define("USE_CLIMATE") cg.add_global(climate_ns.using) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 0cd52b1036..3145358dab 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -16,6 +16,9 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature_high) TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) + TEMPLATABLE_VALUE(std::string, custom_fan_mode) + TEMPLATABLE_VALUE(ClimatePreset, preset) + TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) void play(Ts... x) override { @@ -24,8 +27,13 @@ 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...)); + call.set_preset(this->custom_preset_.optional_value(x...)); call.set_swing_mode(this->swing_mode_.optional_value(x...)); call.perform(); } @@ -34,5 +42,12 @@ template class ControlAction : public Action { Climate *climate_; }; +class StateTrigger : public Trigger<> { + public: + StateTrigger(Climate *climate) { + climate->add_on_state_callback([this]() { this->trigger(); }); + } +}; + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 443290ed6d..ebea20ed1f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,25 +1,38 @@ #include "climate.h" -#include "esphome/core/log.h" namespace esphome { namespace climate { -static const char *TAG = "climate"; +static const char *const TAG = "climate"; 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(); + ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); } if (this->fan_mode_.has_value()) { - const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); - ESP_LOGD(TAG, " Fan: %s", fan_mode_s); + this->custom_fan_mode_.reset(); + 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(); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); + } + if (this->preset_.has_value()) { + this->custom_preset_.reset(); + 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_); @@ -30,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_() { @@ -40,21 +50,42 @@ 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(); } } - if (this->fan_mode_.has_value()) { + if (this->custom_fan_mode_.has_value()) { + auto custom_fan_mode = *this->custom_fan_mode_; + if (!traits.supports_custom_fan_mode(custom_fan_mode)) { + ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", custom_fan_mode.c_str()); + this->custom_fan_mode_.reset(); + } + } 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(); } } + if (this->custom_preset_.has_value()) { + auto custom_preset = *this->custom_preset_; + if (!traits.supports_custom_preset(custom_preset)) { + ESP_LOGW(TAG, " Preset %s is not supported by this device!", custom_preset.c_str()); + this->custom_preset_.reset(); + } + } 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!", 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(); } } @@ -64,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(); } @@ -76,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(); } @@ -93,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; @@ -117,6 +142,8 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { this->set_mode(CLIMATE_MODE_FAN_ONLY); } else if (str_equals_case_insensitive(mode, "DRY")) { this->set_mode(CLIMATE_MODE_DRY); + } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) { + this->set_mode(CLIMATE_MODE_HEAT_COOL); } else { ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); } @@ -124,6 +151,7 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { } ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; + this->custom_fan_mode_.reset(); return *this; } ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { @@ -146,11 +174,57 @@ 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 { - ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { + this->custom_fan_mode_ = fan_mode; + this->fan_mode_.reset(); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + } + } + return *this; +} +ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { + if (fan_mode.has_value()) { + this->set_fan_mode(fan_mode.value()); + } + return *this; +} +ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { + this->preset_ = preset; + this->custom_preset_.reset(); + return *this; +} +ClimateCall &ClimateCall::set_preset(const std::string &preset) { + if (str_equals_case_insensitive(preset, "ECO")) { + this->set_preset(CLIMATE_PRESET_ECO); + } else if (str_equals_case_insensitive(preset, "AWAY")) { + this->set_preset(CLIMATE_PRESET_AWAY); + } else if (str_equals_case_insensitive(preset, "BOOST")) { + this->set_preset(CLIMATE_PRESET_BOOST); + } else if (str_equals_case_insensitive(preset, "COMFORT")) { + this->set_preset(CLIMATE_PRESET_COMFORT); + } else if (str_equals_case_insensitive(preset, "HOME")) { + this->set_preset(CLIMATE_PRESET_HOME); + } else if (str_equals_case_insensitive(preset, "SLEEP")) { + this->set_preset(CLIMATE_PRESET_SLEEP); + } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { + this->set_preset(CLIMATE_PRESET_ACTIVITY); + } else { + if (this->parent_->get_traits().supports_custom_preset(preset)) { + this->custom_preset_ = preset; + this->preset_.reset(); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); + } + } + return *this; +} +ClimateCall &ClimateCall::set_preset(optional preset) { + if (preset.has_value()) { + this->set_preset(preset.value()); } return *this; } - ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; return *this; @@ -186,15 +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) { @@ -215,6 +297,12 @@ ClimateCall &ClimateCall::set_mode(optional mode) { } ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { this->fan_mode_ = fan_mode; + this->custom_fan_mode_.reset(); + return *this; +} +ClimateCall &ClimateCall::set_preset(optional preset) { + this->preset_ = preset; + this->custom_preset_.reset(); return *this; } ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) { @@ -226,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(); @@ -246,11 +344,32 @@ 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_supports_fan_modes()) { - state.fan_mode = this->fan_mode; + if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { + state.uses_custom_fan_mode = true; + 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()) { + state.uses_custom_preset = false; + state.preset = this->preset.value(); + } + if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { + state.uses_custom_preset = true; + 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 != vec.cend()) { + state.custom_preset = std::distance(vec.begin(), it); + } } if (traits.get_supports_swing_modes()) { state.swing_mode = this->swing_mode; @@ -262,15 +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()) { - ESP_LOGD(TAG, " Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode)); + if (traits.get_supports_fan_modes() && this->fan_mode.has_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", 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); @@ -281,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(); @@ -315,7 +440,11 @@ 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) {} +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +Climate::Climate(const std::string &name) : EntityBase(name) {} +#pragma GCC diagnostic pop + Climate::Climate() : Climate("") {} ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -329,12 +458,12 @@ 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()) { + if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { + call.set_preset(this->preset); + } if (traits.get_supports_swing_modes()) { call.set_swing_mode(this->swing_mode); } @@ -349,17 +478,102 @@ 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()) { + 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) { + // 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() && uses_custom_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; } 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 786afe097a..852b76686c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -1,8 +1,10 @@ #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" #include "climate_mode.h" #include "climate_traits.h" @@ -10,8 +12,8 @@ namespace esphome { namespace climate { #define LOG_CLIMATE(prefix, type, obj) \ - if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ } class Climate; @@ -62,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); @@ -70,12 +74,22 @@ class ClimateCall { ClimateCall &set_fan_mode(optional fan_mode); /// Set the fan mode of the climate device based on a string. ClimateCall &set_fan_mode(const std::string &fan_mode); + /// Set the fan mode of the climate device based on a string. + ClimateCall &set_fan_mode(optional fan_mode); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(optional swing_mode); /// Set the swing mode of the climate device based on a string. ClimateCall &set_swing_mode(const std::string &swing_mode); + /// Set the preset of the climate device. + ClimateCall &set_preset(ClimatePreset preset); + /// Set the preset of the climate device. + ClimateCall &set_preset(optional preset); + /// Set the preset of the climate device based on a string. + ClimateCall &set_preset(const std::string &preset); + /// Set the preset of the climate device based on a string. + ClimateCall &set_preset(optional preset); void perform(); @@ -83,9 +97,13 @@ 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; + const optional &get_preset() const; + const optional &get_custom_preset() const; protected: void validate_(); @@ -95,16 +113,27 @@ 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_; + optional preset_; + optional custom_preset_; }; /// 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; - ClimateFanMode fan_mode; + bool uses_custom_fan_mode{false}; + union { + ClimateFanMode fan_mode; + uint8_t custom_fan_mode; + }; + bool uses_custom_preset{false}; + union { + ClimatePreset preset; + uint8_t custom_preset; + }; ClimateSwingMode swing_mode; union { float target_temperature; @@ -133,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(); @@ -165,14 +194,24 @@ 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. - ClimateFanMode fan_mode; + optional fan_mode; /// The active swing mode of the climate device. ClimateSwingMode swing_mode; + /// The active custom fan mode of the climate device. + optional custom_fan_mode; + + /// The active preset of the climate device. + optional preset; + + /// The active custom preset mode of the climate device. + optional custom_preset; + /** Add a callback for the climate device state, each time the state of the climate device is updated * (using publish_state), this callback will be called. * @@ -207,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 @@ -232,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 ddcc4af4d9..e46159a750 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -3,80 +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"; + return LOG_STR("OFF"); + case CLIMATE_MODE_HEAT_COOL: + return LOG_STR("HEAT_COOL"); case CLIMATE_MODE_AUTO: - return "AUTO"; + return LOG_STR("AUTO"); case CLIMATE_MODE_COOL: - return "COOL"; + return LOG_STR("COOL"); case CLIMATE_MODE_HEAT: - return "HEAT"; + return LOG_STR("HEAT"); case CLIMATE_MODE_FAN_ONLY: - return "FAN_ONLY"; + return LOG_STR("FAN_ONLY"); case CLIMATE_MODE_DRY: - return "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 LogString *climate_preset_to_string(ClimatePreset preset) { + switch (preset) { + case climate::CLIMATE_PRESET_NONE: + return LOG_STR("NONE"); + case climate::CLIMATE_PRESET_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 LOG_STR("SLEEP"); + case climate::CLIMATE_PRESET_ACTIVITY: + return LOG_STR("ACTIVITY"); + default: + return LOG_STR("UNKNOWN"); } } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 8037ea2196..3e5626919c 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -1,43 +1,48 @@ #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 - CLIMATE_MODE_AUTO = 1, - /// The climate device is manually set to cool mode (not in auto mode!) + /// The climate device is set to heat/cool to reach the target temperature. + CLIMATE_MODE_HEAT_COOL = 1, + /// 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 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 }; /// Enum for the current action of the climate device. Values match those of ClimateMode. 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, @@ -61,7 +66,7 @@ enum ClimateFanMode : uint8_t { /// Enum for all modes a climate swing can be in enum ClimateSwingMode : uint8_t { - /// The sing mode is set to Off + /// The swing mode is set to Off CLIMATE_SWING_OFF = 0, /// The fan mode is set to Both CLIMATE_SWING_BOTH = 1, @@ -71,17 +76,40 @@ enum ClimateSwingMode : uint8_t { CLIMATE_SWING_HORIZONTAL = 3, }; +/// Enum for all modes a climate swing can be in +enum ClimatePreset : uint8_t { + /// 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 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 6e941bddf0..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,95 +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_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 347a7bc1f2..903ce085d8 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,6 +1,8 @@ #pragma once +#include "esphome/core/helpers.h" #include "climate_mode.h" +#include namespace esphome { namespace climate { @@ -23,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: @@ -40,69 +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_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}; + 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/__init__.py b/esphome/components/climate_ir/__init__.py index 1163705faa..1389ebfc6d 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,41 +1,55 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, remote_receiver, sensor, remote_base +from esphome.components import ( + climate, + remote_transmitter, + remote_receiver, + sensor, + remote_base, +) from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR -from esphome.core import coroutine -AUTO_LOAD = ['sensor', 'remote_base'] +AUTO_LOAD = ["sensor", "remote_base"] +CODEOWNERS = ["@glmnet"] -climate_ir_ns = cg.esphome_ns.namespace('climate_ir') -ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, - remote_base.RemoteReceiverListener) +climate_ir_ns = cg.esphome_ns.namespace("climate_ir") +ClimateIR = climate_ir_ns.class_( + "ClimateIR", climate.Climate, cg.Component, remote_base.RemoteReceiverListener +) -CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend({ - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), -}).extend(cv.COMPONENT_SCHEMA) +CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + } +).extend(cv.COMPONENT_SCHEMA) -CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend({ - cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), -}) +CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( + { + cv.Optional(CONF_RECEIVER_ID): cv.use_id( + remote_receiver.RemoteReceiverComponent + ), + } +) -@coroutine -def register_climate_ir(var, config): - yield cg.register_component(var, config) - yield climate.register_climate(var, config) +async def register_climate_ir(var, config): + await cg.register_component(var, config) + await climate.register_climate(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) if CONF_RECEIVER_ID in config: - receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) cg.add(receiver.register_listener(var)) - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 8f06ff2214..b47d9b0141 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -4,68 +4,27 @@ namespace esphome { namespace climate_ir { -static const char *TAG = "climate_ir"; +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 7a69b19786..677021da29 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/components/climate/climate.h" #include "esphome/components/remote_base/remote_base.h" #include "esphome/components/remote_transmitter/remote_transmitter.h" @@ -19,16 +21,15 @@ 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; this->supports_dry_ = supports_dry; this->supports_fan_only_ = supports_fan_only; - this->fan_modes_ = fan_modes; - this->swing_modes_ = swing_modes; + this->fan_modes_ = std::move(fan_modes); + this->swing_modes_ = std::move(swing_modes); } void setup() override; @@ -58,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/__init__.py b/esphome/components/climate_ir_lg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py new file mode 100644 index 0000000000..c58e40f7f4 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate.py @@ -0,0 +1,47 @@ +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"] + +climate_ir_lg_ns = cg.esphome_ns.namespace("climate_ir_lg") +LgIrClimate = climate_ir_lg_ns.class_("LgIrClimate", climate_ir.ClimateIR) + +CONF_HEADER_HIGH = "header_high" +CONF_HEADER_LOW = "header_low" +CONF_BIT_HIGH = "bit_high" +CONF_BIT_ONE_LOW = "bit_one_low" +CONF_BIT_ZERO_LOW = "bit_zero_low" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LgIrClimate), + cv.Optional( + CONF_HEADER_HIGH, default="8000us" + ): cv.positive_time_period_microseconds, + cv.Optional( + CONF_HEADER_LOW, default="4000us" + ): cv.positive_time_period_microseconds, + cv.Optional( + CONF_BIT_HIGH, default="600us" + ): cv.positive_time_period_microseconds, + cv.Optional( + CONF_BIT_ONE_LOW, default="1600us" + ): cv.positive_time_period_microseconds, + cv.Optional( + CONF_BIT_ZERO_LOW, default="550us" + ): cv.positive_time_period_microseconds, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + + cg.add(var.set_header_high(config[CONF_HEADER_HIGH])) + cg.add(var.set_header_low(config[CONF_HEADER_LOW])) + cg.add(var.set_bit_high(config[CONF_BIT_HIGH])) + cg.add(var.set_bit_one_low(config[CONF_BIT_ONE_LOW])) + cg.add(var.set_bit_zero_low(config[CONF_BIT_ZERO_LOW])) diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp new file mode 100644 index 0000000000..cbb1f7699b --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -0,0 +1,206 @@ +#include "climate_ir_lg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace climate_ir_lg { + +static const char *const TAG = "climate.climate_ir_lg"; + +const uint32_t COMMAND_ON = 0x00000; +const uint32_t COMMAND_ON_AI = 0x03000; +const uint32_t COMMAND_COOL = 0x08000; +const uint32_t COMMAND_HEAT = 0x0C000; +const uint32_t COMMAND_OFF = 0xC0000; +const uint32_t COMMAND_SWING = 0x10000; +// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. +const uint32_t COMMAND_AUTO = 0x0B000; +const uint32_t COMMAND_DRY_FAN = 0x09000; + +const uint32_t COMMAND_MASK = 0xFF000; + +const uint32_t FAN_MASK = 0xF0; +const uint32_t FAN_AUTO = 0x50; +const uint32_t FAN_MIN = 0x00; +const uint32_t FAN_MED = 0x20; +const uint32_t FAN_MAX = 0x40; + +// Temperature +const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1; +const uint32_t TEMP_MASK = 0XF00; +const uint32_t TEMP_SHIFT = 8; + +const uint16_t BITS = 28; + +void LgIrClimate::transmit_state() { + uint32_t remote_state = 0x8800000; + + // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state |= COMMAND_SWING; + } else { + if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { + remote_state |= COMMAND_ON_AI; + } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { + remote_state |= COMMAND_ON; + this->mode = climate::CLIMATE_MODE_COOL; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COMMAND_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= COMMAND_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + remote_state |= COMMAND_AUTO; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= COMMAND_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state |= COMMAND_OFF; + break; + } + } + mode_before_ = this->mode; + + ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); + + if (this->mode == climate::CLIMATE_MODE_OFF) { + remote_state |= FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY || + this->mode == climate::CLIMATE_MODE_HEAT) { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= FAN_AUTO; + break; + } + } + + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + // remote_state |= FAN_MODE_AUTO_DRY; + } + if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); + remote_state |= ((temp - 15) << TEMP_SHIFT); + } + } + transmit_(remote_state); + this->publish_state(); +} + +bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t nbits = 0; + uint32_t remote_state = 0; + + if (!data.expect_item(this->header_high_, this->header_low_)) + return false; + + for (nbits = 0; nbits < 32; nbits++) { + if (data.expect_item(this->bit_high_, this->bit_one_low_)) { + remote_state = (remote_state << 1) | 1; + } else if (data.expect_item(this->bit_high_, this->bit_zero_low_)) { + remote_state = (remote_state << 1) | 0; + } else if (nbits == BITS) { + break; + } else { + return false; + } + } + + ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + if ((remote_state & 0xFF00000) != 0x8800000) + return false; + + if ((remote_state & COMMAND_MASK) == COMMAND_ON) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) { + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + } + + if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + } else { + if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) + this->mode = climate::CLIMATE_MODE_DRY; + else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else { + this->mode = climate::CLIMATE_MODE_COOL; + } + + // Temperature + if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) + this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; + + // Fan Speed + 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) { + if ((remote_state & FAN_MASK) == FAN_AUTO) + this->fan_mode = climate::CLIMATE_FAN_AUTO; + else if ((remote_state & FAN_MASK) == FAN_MIN) + this->fan_mode = climate::CLIMATE_FAN_LOW; + else if ((remote_state & FAN_MASK) == FAN_MED) + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + else if ((remote_state & FAN_MASK) == FAN_MAX) + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + } + this->publish_state(); + + return true; +} +void LgIrClimate::transmit_(uint32_t value) { + calc_checksum_(value); + ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02X", value); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + data->reserve(2 + BITS * 2u); + + data->item(this->header_high_, this->header_low_); + + for (uint32_t mask = 1UL << (BITS - 1); mask != 0; mask >>= 1) { + if (value & mask) { + data->item(this->bit_high_, this->bit_one_low_); + } else { + data->item(this->bit_high_, this->bit_zero_low_); + } + } + data->mark(this->bit_high_); + transmit.perform(); +} +void LgIrClimate::calc_checksum_(uint32_t &value) { + uint32_t mask = 0xF; + uint32_t sum = 0; + for (uint8_t i = 1; i < 8; i++) { + sum += (value & (mask << (i * 4))) >> (i * 4); + } + + value |= (sum & mask); +} + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h new file mode 100644 index 0000000000..6b38b3247b --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace climate_ir_lg { + +// Temperature +const uint8_t TEMP_MIN = 18; // Celsius +const uint8_t TEMP_MAX = 30; // Celsius + +class LgIrClimate : public climate_ir::ClimateIR { + public: + LgIrClimate() + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } + void set_header_high(uint32_t header_high) { this->header_high_ = header_high; } + void set_header_low(uint32_t header_low) { this->header_low_ = header_low; } + void set_bit_high(uint32_t bit_high) { this->bit_high_ = bit_high; } + void set_bit_one_low(uint32_t bit_one_low) { this->bit_one_low_ = bit_one_low; } + void set_bit_zero_low(uint32_t bit_zero_low) { this->bit_zero_low_ = bit_zero_low; } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; + + void calc_checksum_(uint32_t &value); + void transmit_(uint32_t value); + + uint32_t header_high_; + uint32_t header_low_; + uint32_t bit_high_; + uint32_t bit_one_low_; + uint32_t bit_zero_low_; + + climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; +}; + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py new file mode 100644 index 0000000000..47679fcc68 --- /dev/null +++ b/esphome/components/color/__init__.py @@ -0,0 +1,57 @@ +from esphome import config_validation as cv +from esphome import codegen as cg +from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE + +ColorStruct = cg.esphome_ns.struct("Color") + +MULTI_CONF = True + +CONF_RED_INT = "red_int" +CONF_GREEN_INT = "green_int" +CONF_BLUE_INT = "blue_int" +CONF_WHITE_INT = "white_int" + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Exclusive(CONF_RED, "red"): cv.percentage, + cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t, + cv.Exclusive(CONF_GREEN, "green"): cv.percentage, + cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t, + cv.Exclusive(CONF_BLUE, "blue"): cv.percentage, + cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t, + cv.Exclusive(CONF_WHITE, "white"): cv.percentage, + cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + r = 0 + if CONF_RED in config: + r = int(config[CONF_RED] * 255) + elif CONF_RED_INT in config: + r = config[CONF_RED_INT] + + g = 0 + if CONF_GREEN in config: + g = int(config[CONF_GREEN] * 255) + elif CONF_GREEN_INT in config: + g = config[CONF_GREEN_INT] + + b = 0 + if CONF_BLUE in config: + b = int(config[CONF_BLUE] * 255) + elif CONF_BLUE_INT in config: + b = config[CONF_BLUE_INT] + + w = 0 + if CONF_WHITE in config: + w = int(config[CONF_WHITE] * 255) + elif CONF_WHITE_INT in config: + w = config[CONF_WHITE_INT] + + cg.new_variable( + config[CONF_ID], + cg.StructInitializer(ColorStruct, ("r", r), ("g", g), ("b", b), ("w", w)), + ) 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/climate.py b/esphome/components/coolix/climate.py index 81412bb586..2cfd1912e5 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -3,16 +3,19 @@ import esphome.config_validation as cv from esphome.components import climate_ir from esphome.const import CONF_ID -AUTO_LOAD = ['climate_ir'] +AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@glmnet"] -coolix_ns = cg.esphome_ns.namespace('coolix') -CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) +coolix_ns = cg.esphome_ns.namespace("coolix") +CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(CoolixClimate), -}) +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CoolixClimate), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield climate_ir.register_climate_ir(var, config) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 441f43b424..c9145e4ecf 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace coolix { -static const char *TAG = "coolix.climate"; +static const char *const TAG = "coolix.climate"; const uint32_t COOLIX_OFF = 0xB27BE0; const uint32_t COOLIX_SWING = 0xB26BE0; @@ -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,16 +84,16 @@ 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 { - switch (this->fan_mode) { + switch (this->fan_mode.value()) { case climate::CLIMATE_FAN_HIGH: remote_state |= COOLIX_FAN_MAX; break; @@ -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 ed6ebe98fa..0fd27f3f27 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -3,128 +3,213 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id, Condition from esphome.components import mqtt -from esphome.const import CONF_ID, CONF_INTERNAL, CONF_DEVICE_CLASS, CONF_STATE, \ - CONF_POSITION, CONF_TILT, CONF_STOP, CONF_MQTT_ID, CONF_NAME -from esphome.core import CORE, coroutine, coroutine_with_priority +from esphome.const import ( + CONF_ID, + 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_TRIGGER_ID, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True +CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - '', 'awning', 'blind', 'curtain', 'damper', 'door', 'garage', - 'shade', 'shutter', 'window' + "", + "awning", + "blind", + "curtain", + "damper", + "door", + "garage", + "gate", + "shade", + "shutter", + "window", ] -cover_ns = cg.esphome_ns.namespace('cover') +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 COVER_STATES = { - 'OPEN': COVER_OPEN, - 'CLOSED': COVER_CLOSED, + "OPEN": COVER_OPEN, + "CLOSED": COVER_CLOSED, } validate_cover_state = cv.enum(COVER_STATES, upper=True) -CoverOperation = cover_ns.enum('CoverOperation') +CoverOperation = cover_ns.enum("CoverOperation") COVER_OPERATIONS = { - 'IDLE': CoverOperation.COVER_OPERATION_IDLE, - 'OPENING': CoverOperation.COVER_OPERATION_OPENING, - 'CLOSING': CoverOperation.COVER_OPERATION_CLOSING, + "IDLE": CoverOperation.COVER_OPERATION_IDLE, + "OPENING": CoverOperation.COVER_OPERATION_OPENING, + "CLOSING": CoverOperation.COVER_OPERATION_CLOSING, } validate_cover_operation = cv.enum(COVER_OPERATIONS, upper=True) # Actions -OpenAction = cover_ns.class_('OpenAction', automation.Action) -CloseAction = cover_ns.class_('CloseAction', automation.Action) -StopAction = cover_ns.class_('StopAction', 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) +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({ - 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 -}) +# 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), + 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), + } + ), + } +) -@coroutine -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])) +async def setup_cover_core_(var, config): + await setup_entity(var, config) + if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + 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) - yield mqtt.register_mqtt_component(mqtt_, config) + 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])) -@coroutine -def register_cover(var, config): +async def register_cover(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_cover(var)) - yield setup_cover_core_(var, config) + await setup_cover_core_(var, config) -COVER_ACTION_SCHEMA = maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Cover), -}) +COVER_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Cover), + } +) -@automation.register_action('cover.open', OpenAction, COVER_ACTION_SCHEMA) -def cover_open_to_code(config, action_id, template_arg, args): +@automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA) +async def cover_open_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA) +async def cover_close_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA) +async def cover_stop_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("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) -@automation.register_action('cover.close', CloseAction, COVER_ACTION_SCHEMA) -def cover_close_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), + cv.Optional(CONF_STOP): cv.templatable(cv.boolean), + cv.Exclusive(CONF_STATE, "pos"): cv.templatable(validate_cover_state), + cv.Exclusive(CONF_POSITION, "pos"): cv.templatable(cv.percentage), + cv.Optional(CONF_TILT): cv.templatable(cv.percentage), + } +) -@automation.register_action('cover.stop', StopAction, COVER_ACTION_SCHEMA) -def cover_stop_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), - cv.Optional(CONF_STOP): cv.templatable(cv.boolean), - cv.Exclusive(CONF_STATE, 'pos'): cv.templatable(validate_cover_state), - cv.Exclusive(CONF_POSITION, 'pos'): cv.templatable(cv.percentage), - cv.Optional(CONF_TILT): cv.templatable(cv.percentage), -}) - - -@automation.register_action('cover.control', ControlAction, COVER_CONTROL_ACTION_SCHEMA) -def cover_control_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA) +async def cover_control_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_STOP in config: - template_ = yield cg.templatable(config[CONF_STOP], args, bool) + template_ = await cg.templatable(config[CONF_STOP], args, bool) cg.add(var.set_stop(template_)) if CONF_STATE in config: - template_ = yield cg.templatable(config[CONF_STATE], args, float) + template_ = await cg.templatable(config[CONF_STATE], args, float) cg.add(var.set_position(template_)) if CONF_POSITION in config: - template_ = yield cg.templatable(config[CONF_POSITION], args, float) + template_ = await cg.templatable(config[CONF_POSITION], args, float) cg.add(var.set_position(template_)) if CONF_TILT in config: - template_ = yield cg.templatable(config[CONF_TILT], args, float) + template_ = await cg.templatable(config[CONF_TILT], args, float) cg.add(var.set_tilt(template_)) - yield var + return var @coroutine_with_priority(100.0) -def to_code(config): - cg.add_define('USE_COVER') +async def to_code(config): + cg.add_define("USE_COVER") cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index a8eb0cdf99..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_; @@ -41,6 +51,10 @@ template class ControlAction : public Action { public: explicit ControlAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(bool, stop) + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + void play(Ts... x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) @@ -52,10 +66,6 @@ template class ControlAction : public Action { call.perform(); } - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - protected: Cover *cover_; }; @@ -63,6 +73,10 @@ template class ControlAction : public Action { template class CoverPublishAction : public Action { public: CoverPublishAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + TEMPLATABLE_VALUE(CoverOperation, current_operation) + void play(Ts... x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); @@ -73,10 +87,6 @@ template class CoverPublishAction : public Action { this->cover_->publish_state(); } - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - TEMPLATABLE_VALUE(CoverOperation, current_operation) - 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 8d87ecfd1a..a8d3d691a4 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace cover { -static const char *TAG = "cover"; +static const char *const TAG = "cover"; const float COVER_OPEN = 1.0f; const float COVER_CLOSED = 0.0f; @@ -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 839cf9207e..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" @@ -12,14 +13,14 @@ const extern float COVER_OPEN; 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()); \ - auto traits_ = obj->get_traits(); \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + auto traits_ = (obj)->get_traits(); \ if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ } @@ -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/__init__.py b/esphome/components/cs5460a/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp new file mode 100644 index 0000000000..b0c0531936 --- /dev/null +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -0,0 +1,340 @@ +#include "cs5460a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cs5460a { + +static const char *const TAG = "cs5460a"; + +void CS5460AComponent::write_register_(enum CS5460ARegister addr, uint32_t value) { + this->write_byte(CMD_WRITE | (addr << 1)); + this->write_byte(value >> 16); + this->write_byte(value >> 8); + this->write_byte(value >> 0); +} + +uint32_t CS5460AComponent::read_register_(uint8_t addr) { + uint32_t value; + + this->write_byte(CMD_READ | (addr << 1)); + value = (uint32_t) this->transfer_byte(CMD_SYNC0) << 16; + value |= (uint32_t) this->transfer_byte(CMD_SYNC0) << 8; + value |= this->transfer_byte(CMD_SYNC0) << 0; + + return value; +} + +bool CS5460AComponent::softreset_() { + uint32_t pc = ((uint8_t) phase_offset_ & 0x3f) | (phase_offset_ < 0 ? 0x40 : 0); + uint32_t config = (1 << 0) | /* K = 0b0001 */ + (current_hpf_ ? 1 << 5 : 0) | /* IHPF */ + (voltage_hpf_ ? 1 << 6 : 0) | /* VHPF */ + (pga_gain_ << 16) | /* Gi */ + (pc << 17); /* PC */ + int cnt = 0; + + /* Serial resynchronization */ + this->write_byte(CMD_SYNC1); + this->write_byte(CMD_SYNC1); + this->write_byte(CMD_SYNC1); + this->write_byte(CMD_SYNC0); + + /* Reset */ + this->write_register_(REG_CONFIG, 1 << 7); + delay(10); + while (cnt++ < 50 && (this->read_register_(REG_CONFIG) & 0x81) != 0x000001) + ; + if (cnt > 50) + return false; + + this->write_register_(REG_CONFIG, config); + return true; +} + +void CS5460AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up CS5460A..."); + + float current_full_scale = (pga_gain_ == CS5460A_PGA_GAIN_10X) ? 0.25 : 0.10; + float voltage_full_scale = 0.25; + current_multiplier_ = current_full_scale / (fabsf(current_gain_) * 0x1000000); + voltage_multiplier_ = voltage_full_scale / (voltage_gain_ * 0x1000000); + + /* + * Calculate power from the Energy register because the Power register + * stores instantaneous power which varies a lot in each AC cycle, + * while the Energy value is accumulated over the "computation cycle" + * which should be an integer number of AC cycles. + */ + power_multiplier_ = + (current_full_scale * voltage_full_scale * 4096) / (current_gain_ * voltage_gain_ * samples_ * 0x800000); + + pulse_freq_ = + (current_full_scale * voltage_full_scale) / (fabsf(current_gain_) * voltage_gain_ * pulse_energy_wh_ * 3600); + + hw_init_(); +} + +void CS5460AComponent::hw_init_() { + this->spi_setup(); + this->enable(); + + if (!this->softreset_()) { + this->disable(); + ESP_LOGE(TAG, "CS5460A reset failed!"); + this->mark_failed(); + return; + } + + uint32_t status = this->read_register_(REG_STATUS); + ESP_LOGCONFIG(TAG, " Version: %x", (status >> 6) & 7); + + this->write_register_(REG_CYCLE_COUNT, samples_); + this->write_register_(REG_PULSE_RATE, lroundf(pulse_freq_ * 32.0f)); + + /* Use one of the power saving features (assuming external oscillator), reset other CONTROL bits, + * sometimes softreset_() is not enough */ + this->write_register_(REG_CONTROL, 0x000004); + + this->restart_(); + this->disable(); + ESP_LOGCONFIG(TAG, " Init ok"); +} + +/* Doesn't reset the register values etc., just restarts the "computation cycle" */ +void CS5460AComponent::restart_() { + this->enable(); + /* Stop running conversion, wake up if needed */ + this->write_byte(CMD_POWER_UP); + /* Start continuous conversion */ + this->write_byte(CMD_START_CONT); + this->disable(); + + this->started_(); +} + +void CS5460AComponent::started_() { + /* + * Try to guess when the next batch of results is going to be ready and + * schedule next STATUS check some time before that moment. This assumes + * two things: + * * a new "computation cycle" started just now. If it started some + * time ago we may be a late next time, but hopefully less late in each + * iteration -- that's why we schedule the next check in some 0.8 of + * the time we actually expect the next reading ready. + * * MCLK rate is 4.096MHz and K == 1. If there's a CS5460A module in + * use with a different clock this will need to be parametrised. + */ + expect_data_ts_ = millis() + samples_ * 1024 / 4096; + + schedule_next_check_(); +} + +void CS5460AComponent::schedule_next_check_() { + int32_t time_left = expect_data_ts_ - millis(); + + /* First try at 0.8 of the actual expected time (if it's in the future) */ + if (time_left > 0) + time_left -= time_left / 5; + + if (time_left > -500) { + /* But not sooner than in 30ms from now */ + if (time_left < 30) + time_left = 30; + } else { + /* + * If the measurement is more than 0.5s overdue start worrying. The + * device may be stuck because of an overcurrent error or similar, + * from now on just retry every 1s. After 15s try a reset, if it + * fails we give up and mark the component "failed". + */ + if (time_left > -15000) { + time_left = 1000; + this->status_momentary_warning("warning", 1000); + } else { + ESP_LOGCONFIG(TAG, "Device officially stuck, resetting"); + this->cancel_timeout("status-check"); + this->hw_init_(); + return; + } + } + + this->set_timeout("status-check", time_left, [this]() { + if (!this->check_status_()) + this->schedule_next_check_(); + }); +} + +bool CS5460AComponent::check_status_() { + this->enable(); + uint32_t status = this->read_register_(REG_STATUS); + + if (!(status & 0xcbf83c)) { + this->disable(); + return false; + } + + uint32_t clear = 1 << 20; + + /* TODO: Report if IC=0 but only once as it can't be cleared */ + + if (status & (1 << 2)) { + clear |= 1 << 2; + ESP_LOGE(TAG, "Low supply detected"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 3)) { + clear |= 1 << 3; + ESP_LOGE(TAG, "Modulator oscillation on current channel"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 4)) { + clear |= 1 << 4; + ESP_LOGE(TAG, "Modulator oscillation on voltage channel"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 5)) { + clear |= 1 << 5; + ESP_LOGE(TAG, "Watch-dog timeout"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 11)) { + clear |= 1 << 11; + ESP_LOGE(TAG, "EOUT Energy Accumulation Register out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 12)) { + clear |= 1 << 12; + ESP_LOGE(TAG, "Energy out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 13)) { + clear |= 1 << 13; + ESP_LOGE(TAG, "RMS voltage out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 14)) { + clear |= 1 << 14; + ESP_LOGE(TAG, "RMS current out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 15)) { + clear |= 1 << 15; + ESP_LOGE(TAG, "Power calculation out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 16)) { + clear |= 1 << 16; + ESP_LOGE(TAG, "Voltage out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 17)) { + clear |= 1 << 17; + ESP_LOGE(TAG, "Current out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 19)) { + clear |= 1 << 19; + ESP_LOGE(TAG, "Divide overflowed"); + } + + if (status & (1 << 22)) { + bool dir = status & (1 << 21); + if (current_gain_ < 0) + dir = !dir; + ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive"); + clear |= 1 << 22; + } + + uint32_t raw_current = 0; /* Calm the validators */ + uint32_t raw_voltage = 0; + uint32_t raw_energy = 0; + + if (status & (1 << 23)) { + clear |= 1 << 23; + + if (current_sensor_ != nullptr) + raw_current = this->read_register_(REG_IRMS); + + if (voltage_sensor_ != nullptr) + raw_voltage = this->read_register_(REG_VRMS); + } + + if (status & ((1 << 23) | (1 << 5))) { + /* Read to clear the WDT bit */ + raw_energy = this->read_register_(REG_E); + } + + this->write_register_(REG_STATUS, clear); + this->disable(); + + /* + * Schedule the next STATUS check assuming that DRDY was asserted very + * recently, then publish the new values. Do this last for reentrancy in + * case the publish triggers a restart() or for whatever reason needs to + * cancel the timeout set in schedule_next_check_(), or needs to use SPI. + * If the current or power values haven't changed one bit it may be that + * the chip somehow forgot to update the registers -- seen happening very + * rarely. In that case don't publish them because the user may have + * the input connected to a multiplexer and may have switched channels + * since the previous reading and we'd be publishing the stale value for + * the new channel. If the value *was* updated it's very unlikely that + * it wouldn't have changed, especially power/energy which are affected + * by the noise on both the current and value channels (in case of energy, + * accumulated over many conversion cycles.) + */ + if (status & (1 << 23)) { + this->started_(); + + if (current_sensor_ != nullptr && raw_current != prev_raw_current_) { + current_sensor_->publish_state(raw_current * current_multiplier_); + prev_raw_current_ = raw_current; + } + + if (voltage_sensor_ != nullptr) + voltage_sensor_->publish_state(raw_voltage * voltage_multiplier_); + + if (power_sensor_ != nullptr && raw_energy != prev_raw_energy_) { + int32_t raw = (int32_t)(raw_energy << 8) >> 8; /* Sign-extend */ + power_sensor_->publish_state(raw * power_multiplier_); + prev_raw_energy_ = raw_energy; + } + + return true; + } + + return false; +} + +void CS5460AComponent::dump_config() { + uint32_t state = this->get_component_state(); + + ESP_LOGCONFIG(TAG, "CS5460A:"); + ESP_LOGCONFIG(TAG, " Init status: %s", + state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other")); + LOG_PIN(" CS Pin: ", cs_); + ESP_LOGCONFIG(TAG, " Samples / cycle: %u", samples_); + ESP_LOGCONFIG(TAG, " Phase offset: %i", phase_offset_); + ESP_LOGCONFIG(TAG, " PGA Gain: %s", pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x"); + ESP_LOGCONFIG(TAG, " Current gain: %.5f", current_gain_); + ESP_LOGCONFIG(TAG, " Voltage gain: %.5f", voltage_gain_); + ESP_LOGCONFIG(TAG, " Current HPF: %s", current_hpf_ ? "enabled" : "disabled"); + ESP_LOGCONFIG(TAG, " Voltage HPF: %s", voltage_hpf_ ? "enabled" : "disabled"); + ESP_LOGCONFIG(TAG, " Pulse energy: %.2f Wh", pulse_energy_wh_); + LOG_SENSOR(" ", "Voltage", voltage_sensor_); + LOG_SENSOR(" ", "Current", current_sensor_); + LOG_SENSOR(" ", "Power", power_sensor_); +} + +} // namespace cs5460a +} // namespace esphome diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h new file mode 100644 index 0000000000..699049757c --- /dev/null +++ b/esphome/components/cs5460a/cs5460a.h @@ -0,0 +1,123 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace cs5460a { + +enum CS5460ACommand { + CMD_SYNC0 = 0xfe, + CMD_SYNC1 = 0xff, + CMD_START_SINGLE = 0xe0, + CMD_START_CONT = 0xe8, + CMD_POWER_UP = 0xa0, + CMD_POWER_STANDBY = 0x88, + CMD_POWER_SLEEP = 0x90, + CMD_CALIBRATION = 0xc0, + CMD_READ = 0x00, + CMD_WRITE = 0x40, +}; + +enum CS5460ARegister { + REG_CONFIG = 0x00, + REG_IDCOFF = 0x01, + REG_IGN = 0x02, + REG_VDCOFF = 0x03, + REG_VGN = 0x04, + REG_CYCLE_COUNT = 0x05, + REG_PULSE_RATE = 0x06, + REG_I = 0x07, + REG_V = 0x08, + REG_P = 0x09, + REG_E = 0x0a, + REG_IRMS = 0x0b, + REG_VRMS = 0x0c, + REG_TBC = 0x0d, + REG_POFF = 0x0e, + REG_STATUS = 0x0f, + REG_IACOFF = 0x10, + REG_VACOFF = 0x11, + REG_MASK = 0x1a, + REG_CONTROL = 0x1c, +}; + +/** Enum listing the current channel aplifiergain settings for the CS5460A. + */ +enum CS5460APGAGain { + CS5460A_PGA_GAIN_10X = 0b0, + CS5460A_PGA_GAIN_50X = 0b1, +}; + +class CS5460AComponent : public Component, + public spi::SPIDevice { + public: + void set_samples(uint32_t samples) { samples_ = samples; } + void set_phase_offset(int8_t phase_offset) { phase_offset_ = phase_offset; } + void set_pga_gain(CS5460APGAGain pga_gain) { pga_gain_ = pga_gain; } + void set_gains(float current_gain, float voltage_gain) { + current_gain_ = current_gain; + voltage_gain_ = voltage_gain; + } + void set_hpf_enable(bool current_hpf, bool voltage_hpf) { + current_hpf_ = current_hpf; + voltage_hpf_ = voltage_hpf; + } + void set_pulse_energy_wh(float pulse_energy_wh) { pulse_energy_wh_ = pulse_energy_wh; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void restart() { restart_(); } + + void setup() override; + void loop() override {} + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + protected: + uint32_t samples_; + int8_t phase_offset_; + CS5460APGAGain pga_gain_; + float current_gain_; + float voltage_gain_; + bool current_hpf_; + bool voltage_hpf_; + float pulse_energy_wh_; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + + void write_register_(enum CS5460ARegister addr, uint32_t value); + uint32_t read_register_(uint8_t addr); + bool softreset_(); + void hw_init_(); + void restart_(); + void started_(); + void schedule_next_check_(); + bool check_status_(); + + float current_multiplier_; + float voltage_multiplier_; + float power_multiplier_; + float pulse_freq_; + uint32_t expect_data_ts_; + uint32_t prev_raw_current_{0}; + uint32_t prev_raw_energy_{0}; +}; + +template class CS5460ARestartAction : public Action { + public: + CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} + + void play(Ts... x) override { cs5460a_->restart(); } + + protected: + CS5460AComponent *cs5460a_; +}; + +} // namespace cs5460a +} // namespace esphome diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py new file mode 100644 index 0000000000..82df881bfc --- /dev/null +++ b/esphome/components/cs5460a/sensor.py @@ -0,0 +1,141 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, sensor +from esphome.const import ( + CONF_CURRENT, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_VOLTAGE, +) +from esphome import automation +from esphome.automation import maybe_simple_id + +CODEOWNERS = ["@balrog-kun"] +DEPENDENCIES = ["spi"] + +cs5460a_ns = cg.esphome_ns.namespace("cs5460a") +CS5460APGAGain = cs5460a_ns.enum("CS5460APGAGain") +PGA_GAIN_OPTIONS = { + "10X": CS5460APGAGain.CS5460A_PGA_GAIN_10X, + "50X": CS5460APGAGain.CS5460A_PGA_GAIN_50X, +} + +CS5460AComponent = cs5460a_ns.class_("CS5460AComponent", spi.SPIDevice, cg.Component) +CS5460ARestartAction = cs5460a_ns.class_("CS5460ARestartAction", automation.Action) + +CONF_SAMPLES = "samples" +CONF_PHASE_OFFSET = "phase_offset" +CONF_PGA_GAIN = "pga_gain" +CONF_CURRENT_GAIN = "current_gain" +CONF_VOLTAGE_GAIN = "voltage_gain" +CONF_CURRENT_HPF = "current_hpf" +CONF_VOLTAGE_HPF = "voltage_hpf" +CONF_PULSE_ENERGY = "pulse_energy" + + +def validate_config(config): + current_gain = abs(config[CONF_CURRENT_GAIN]) * ( + 1.0 if config[CONF_PGA_GAIN] == "10X" else 5.0 + ) + voltage_gain = config[CONF_VOLTAGE_GAIN] + pulse_energy = config[CONF_PULSE_ENERGY] + + if current_gain == 0.0 or voltage_gain == 0.0: + raise cv.Invalid("The gains can't be zero") + + max_energy = (0.25 * 0.25 / 3600 / (2 ** -4)) / (voltage_gain * current_gain) + min_energy = (0.25 * 0.25 / 3600 / (2 ** 18)) / (voltage_gain * current_gain) + mech_min_energy = (0.25 * 0.25 / 3600 / 7.8) / (voltage_gain * current_gain) + if pulse_energy < min_energy or pulse_energy > max_energy: + raise cv.Invalid( + "For given current&voltage gains, the pulse energy must be between " + f"{min_energy} Wh and {max_energy} Wh and in mechanical counter mode " + f"between {mech_min_energy} Wh and {max_energy} Wh" + ) + + return config + + +validate_energy = cv.float_with_unit("energy", "(Wh|WH|wh)?", optional_unit=True) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CS5460AComponent), + cv.Optional(CONF_SAMPLES, default=4000): cv.int_range(min=1, max=0xFFFFFF), + cv.Optional(CONF_PHASE_OFFSET, default=0): cv.int_range(min=-64, max=63), + cv.Optional(CONF_PGA_GAIN, default="10X"): cv.enum( + PGA_GAIN_OPTIONS, upper=True + ), + cv.Optional(CONF_CURRENT_GAIN, default=0.001): cv.negative_one_to_one_float, + cv.Optional(CONF_VOLTAGE_GAIN, default=0.001): cv.zero_to_one_float, + cv.Optional(CONF_CURRENT_HPF, default=True): cv.boolean, + 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_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=False)), + validate_config, +) + + +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) + + cg.add(var.set_samples(config[CONF_SAMPLES])) + cg.add(var.set_phase_offset(config[CONF_PHASE_OFFSET])) + cg.add(var.set_pga_gain(config[CONF_PGA_GAIN])) + cg.add(var.set_gains(config[CONF_CURRENT_GAIN], config[CONF_VOLTAGE_GAIN])) + cg.add(var.set_hpf_enable(config[CONF_CURRENT_HPF], config[CONF_VOLTAGE_HPF])) + cg.add(var.set_pulse_energy_wh(config[CONF_PULSE_ENERGY])) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + + +@automation.register_action( + "cs5460a.restart", + CS5460ARestartAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(CS5460AComponent), + } + ), +) +async def restart_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/cse7761/__init__.py b/esphome/components/cse7761/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp new file mode 100644 index 0000000000..3b8364f0bc --- /dev/null +++ b/esphome/components/cse7761/cse7761.cpp @@ -0,0 +1,244 @@ +#include "cse7761.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace cse7761 { + +static const char *const TAG = "cse7761"; + +/*********************************************************************************************\ + * CSE7761 - Energy (Sonoff Dual R3 Pow v1.x) + * + * Based on Tasmota source code + * See https://github.com/arendst/Tasmota/discussions/10793 + * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino +\*********************************************************************************************/ + +static const int CSE7761_UREF = 42563; // RmsUc +static const int CSE7761_IREF = 52241; // RmsIAC +static const int CSE7761_PREF = 44513; // PowerPAC + +static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) + +static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register + +static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient + +static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation + +enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; + +void CSE7761Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up CSE7761..."); + this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_RESET); + uint16_t syscon = this->read_(0x00, 2); // Default 0x0A04 + if ((0x0A04 == syscon) && this->chip_init_()) { + this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_CLOSE_WRITE); + ESP_LOGD(TAG, "CSE7761 found"); + this->data_.ready = true; + } else { + this->mark_failed(); + } +} + +void CSE7761Component::dump_config() { + ESP_LOGCONFIG(TAG, "CSE7761:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with CSE7761 failed!"); + } + LOG_UPDATE_INTERVAL(this); + this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); +} + +float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } + +void CSE7761Component::update() { + if (this->data_.ready) { + this->get_data_(); + } +} + +void CSE7761Component::write_(uint8_t reg, uint16_t data) { + uint8_t buffer[5]; + + buffer[0] = 0xA5; + buffer[1] = reg; + uint32_t len = 2; + if (data) { + if (data < 0xFF) { + buffer[2] = data & 0xFF; + len = 3; + } else { + buffer[2] = (data >> 8) & 0xFF; + buffer[3] = data & 0xFF; + len = 4; + } + uint8_t crc = 0; + for (uint32_t i = 0; i < len; i++) { + crc += buffer[i]; + } + buffer[len] = ~crc; + len++; + } + + this->write_array(buffer, len); +} + +bool CSE7761Component::read_once_(uint8_t reg, uint8_t size, uint32_t *value) { + while (this->available()) { + this->read(); + } + + this->write_(reg, 0); + + uint8_t buffer[8] = {0}; + uint32_t rcvd = 0; + + for (uint32_t i = 0; i <= size; i++) { + int value = this->read(); + if (value > -1 && rcvd < sizeof(buffer) - 1) { + buffer[rcvd++] = value; + } + } + + if (!rcvd) { + ESP_LOGD(TAG, "Received 0 bytes for register %hhu", reg); + return false; + } + + rcvd--; + uint32_t result = 0; + // CRC check + uint8_t crc = 0xA5 + reg; + for (uint32_t i = 0; i < rcvd; i++) { + result = (result << 8) | buffer[i]; + crc += buffer[i]; + } + crc = ~crc; + if (crc != buffer[rcvd]) { + return false; + } + + *value = result; + return true; +} + +uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) { + bool result = false; // Start loop + uint8_t retry = 3; // Retry up to three times + uint32_t value = 0; // Default no value + while (!result && retry > 0) { + retry--; + if (this->read_once_(reg, size, &value)) + return value; + } + ESP_LOGE(TAG, "Reading register %hhu failed!", reg); + return value; +} + +uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) { + switch (unit) { + case RMS_UC: + return 0x400000 * 100 / this->data_.coefficient[RMS_UC]; + case RMS_IAC: + return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10; // Stay within 32 bits + case POWER_PAC: + return 0x80000000 / this->data_.coefficient[POWER_PAC]; + } + return 0; +} + +bool CSE7761Component::chip_init_() { + uint16_t calc_chksum = 0xFFFF; + for (uint32_t i = 0; i < 8; i++) { + this->data_.coefficient[i] = this->read_(CSE7761_REG_RMSIAC + i, 2); + calc_chksum += this->data_.coefficient[i]; + } + calc_chksum = ~calc_chksum; + uint16_t coeff_chksum = this->read_(CSE7761_REG_COEFFCHKSUM, 2); + if ((calc_chksum != coeff_chksum) || (!calc_chksum)) { + ESP_LOGD(TAG, "Default calibration"); + this->data_.coefficient[RMS_IAC] = CSE7761_IREF; + this->data_.coefficient[RMS_UC] = CSE7761_UREF; + this->data_.coefficient[POWER_PAC] = CSE7761_PREF; + } + + this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_ENABLE_WRITE); + + uint8_t sys_status = this->read_(CSE7761_REG_SYSSTATUS, 1); + if (sys_status & 0x10) { // Write enable to protected registers (WREN) + this->write_(CSE7761_REG_SYSCON | 0x80, 0xFF04); + this->write_(CSE7761_REG_EMUCON | 0x80, 0x1183); + this->write_(CSE7761_REG_EMUCON2 | 0x80, 0x0FC1); + this->write_(CSE7761_REG_PULSE1SEL | 0x80, 0x3290); + } else { + ESP_LOGD(TAG, "Write failed at chip_init"); + return false; + } + return true; +} + +void CSE7761Component::get_data_() { + // The effective value of current and voltage Rms is a 24-bit signed number, + // the highest bit is 0 for valid data, + // and when the highest bit is 1, the reading will be processed as zero + // The active power parameter PowerA/B is in two’s complement format, 32-bit + // data, the highest bit is Sign bit. + uint32_t value = this->read_(CSE7761_REG_RMSU, 3); + this->data_.voltage_rms = (value >= 0x800000) ? 0 : value; + + value = this->read_(CSE7761_REG_RMSIA, 3); + this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA + value = this->read_(CSE7761_REG_POWERPA, 4); + this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : ((uint32_t) abs((int) value)); + + value = this->read_(CSE7761_REG_RMSIB, 3); + this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA + value = this->read_(CSE7761_REG_POWERPB, 4); + this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : ((uint32_t) abs((int) value)); + + // convert values and publish to sensors + + float voltage = (float) this->data_.voltage_rms / this->coefficient_by_unit_(RMS_UC); + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + + for (uint32_t channel = 0; channel < 2; channel++) { + // Active power = PowerPA * PowerPAC * 1000 / 0x80000000 + float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC); // W + float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC); // A + ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps); + if (channel == 0) { + if (this->power_sensor_1_ != nullptr) { + this->power_sensor_1_->publish_state(active_power); + } + if (this->current_sensor_1_ != nullptr) { + this->current_sensor_1_->publish_state(amps); + } + } else if (channel == 1) { + if (this->power_sensor_2_ != nullptr) { + this->power_sensor_2_->publish_state(active_power); + } + if (this->current_sensor_2_ != nullptr) { + this->current_sensor_2_->publish_state(amps); + } + } + } +} + +} // namespace cse7761 +} // namespace esphome diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h new file mode 100644 index 0000000000..71846cdcab --- /dev/null +++ b/esphome/components/cse7761/cse7761.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace cse7761 { + +struct CSE7761DataStruct { + uint32_t frequency = 0; + uint32_t voltage_rms = 0; + uint32_t current_rms[2] = {0}; + uint32_t energy[2] = {0}; + uint32_t active_power[2] = {0}; + uint16_t coefficient[8] = {0}; + uint8_t energy_update = 0; + bool ready = false; +}; + +/// This class implements support for the CSE7761 UART power sensor. +class CSE7761Component : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_active_power_1_sensor(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } + void set_current_1_sensor(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } + void set_active_power_2_sensor(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } + void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + // Sensors + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *power_sensor_1_{nullptr}; + sensor::Sensor *current_sensor_1_{nullptr}; + sensor::Sensor *power_sensor_2_{nullptr}; + sensor::Sensor *current_sensor_2_{nullptr}; + CSE7761DataStruct data_; + + void write_(uint8_t reg, uint16_t data); + bool read_once_(uint8_t reg, uint8_t size, uint32_t *value); + uint32_t read_(uint8_t reg, uint8_t size); + uint32_t coefficient_by_unit_(uint32_t unit); + bool chip_init_(); + void get_data_(); +}; + +} // namespace cse7761 +} // namespace esphome diff --git a/esphome/components/cse7761/sensor.py b/esphome/components/cse7761/sensor.py new file mode 100644 index 0000000000..c5ec3e5b71 --- /dev/null +++ b/esphome/components/cse7761/sensor.py @@ -0,0 +1,90 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) + +CODEOWNERS = ["@berfenger"] +DEPENDENCIES = ["uart"] + +cse7761_ns = cg.esphome_ns.namespace("cse7761") +CSE7761Component = cse7761_ns.class_( + "CSE7761Component", cg.PollingComponent, uart.UARTDevice +) + +CONF_CURRENT_1 = "current_1" +CONF_CURRENT_2 = "current_2" +CONF_ACTIVE_POWER_1 = "active_power_1" +CONF_ACTIVE_POWER_2 = "active_power_2" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7761Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_1): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_2): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "cse7761", baud_rate=38400, 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) + + for key in [ + CONF_VOLTAGE, + CONF_CURRENT_1, + CONF_CURRENT_2, + CONF_ACTIVE_POWER_1, + CONF_ACTIVE_POWER_2, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 6c014138fd..25d75da3e6 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace cse7766 { -static const char *TAG = "cse7766"; +static const char *const TAG = "cse7766"; void CSE7766Component::loop() { const uint32_t now = millis(); @@ -90,6 +90,7 @@ void CSE7766Component::parse_data_() { uint32_t power_cycle = this->get_24_bit_uint_(17); uint8_t adj = this->raw_data_[20]; + uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; bool power_ok = true; bool voltage_ok = true; @@ -127,6 +128,18 @@ void CSE7766Component::parse_data_() { power = power_calib / float(power_cycle); this->power_acc_ += power; this->power_counts_ += 1; + + uint32_t difference; + if (this->cf_pulses_last_ == 0) + this->cf_pulses_last_ = cf_pulses; + + if (cf_pulses < this->cf_pulses_last_) { + difference = cf_pulses + (0x10000 - this->cf_pulses_last_); + } else { + difference = cf_pulses - this->cf_pulses_last_; + } + this->cf_pulses_last_ = cf_pulses; + this->energy_total_ += difference * float(power_calib) / 1000000.0 / 3600.0; } if ((adj & 0x20) == 0x20 && current_ok && voltage_ok && power != 0.0) { @@ -136,9 +149,9 @@ void CSE7766Component::parse_data_() { } } void CSE7766Component::update() { - float voltage = this->voltage_counts_ > 0 ? this->voltage_acc_ / this->voltage_counts_ : 0.0; - float current = this->current_counts_ > 0 ? this->current_acc_ / this->current_counts_ : 0.0; - float power = this->power_counts_ > 0 ? this->power_acc_ / this->power_counts_ : 0.0; + float voltage = this->voltage_counts_ > 0 ? this->voltage_acc_ / this->voltage_counts_ : 0.0f; + float current = this->current_counts_ > 0 ? this->current_acc_ / this->current_counts_ : 0.0f; + float power = this->power_counts_ > 0 ? this->power_acc_ / this->power_counts_ : 0.0f; ESP_LOGV(TAG, "Got voltage_acc=%.2f current_acc=%.2f power_acc=%.2f", this->voltage_acc_, this->current_acc_, this->power_acc_); @@ -152,6 +165,8 @@ void CSE7766Component::update() { this->current_sensor_->publish_state(current); if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(power); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(this->energy_total_); this->voltage_acc_ = 0.0f; this->current_acc_ = 0.0f; @@ -172,6 +187,7 @@ void CSE7766Component::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_); this->check_uart_settings(4800); } diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 6cacfee072..d6062c251c 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -12,6 +12,7 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void loop() override; float get_setup_priority() const override; @@ -29,9 +30,12 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *energy_sensor_{nullptr}; float voltage_acc_{0.0f}; float current_acc_{0.0f}; float power_acc_{0.0f}; + float energy_total_{0.0f}; + uint32_t cf_pulses_last_{0}; uint32_t voltage_counts_{0}; uint32_t current_counts_{0}; uint32_t power_counts_{0}; diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index a415d67688..2f48aff0aa 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -1,37 +1,87 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart -from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT +from esphome.const import ( + CONF_CURRENT, + CONF_ENERGY, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_WATT_HOURS, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -cse7766_ns = cg.esphome_ns.namespace('cse7766') -CSE7766Component = cse7766_ns.class_('CSE7766Component', cg.PollingComponent, uart.UARTDevice) +cse7766_ns = cg.esphome_ns.namespace("cse7766") +CSE7766Component = cse7766_ns.class_( + "CSE7766Component", cg.PollingComponent, uart.UARTDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CSE7766Component), - - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), -}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7766Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + } + ) + .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 +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) if CONF_VOLTAGE in config: conf = config[CONF_VOLTAGE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_voltage_sensor(sens)) if CONF_CURRENT in config: conf = config[CONF_CURRENT] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: conf = config[CONF_POWER] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index c1e3bec486..51b0f1318c 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -6,19 +6,7 @@ namespace esphome { namespace ct_clamp { -static const char *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_; - } - }); -} +static const char *const TAG = "ct_clamp"; void CTClampSensor::dump_config() { LOG_SENSOR("", "CT Clamp Sensor", this); @@ -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 9f41f8c614..049905d0a7 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -1,27 +1,47 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler -from esphome.const import CONF_SENSOR, CONF_ID, ICON_FLASH, UNIT_AMPERE +from esphome.const import ( + CONF_SENSOR, + CONF_ID, + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, +) -AUTO_LOAD = ['voltage_sampler'] +AUTO_LOAD = ["voltage_sampler"] +CODEOWNERS = ["@jesserockz"] -CONF_SAMPLE_DURATION = 'sample_duration' +CONF_SAMPLE_DURATION = "sample_duration" -ct_clamp_ns = cg.esphome_ns.namespace('ct_clamp') -CTClampSensor = ct_clamp_ns.class_('CTClampSensor', sensor.Sensor, cg.PollingComponent) +ct_clamp_ns = cg.esphome_ns.namespace("ct_clamp") +CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingComponent) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2).extend({ - cv.GenerateID(): cv.declare_id(CTClampSensor), - cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), - cv.Optional(CONF_SAMPLE_DURATION, default='200ms'): cv.positive_time_period_milliseconds, -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(CTClampSensor), + cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), + cv.Optional( + CONF_SAMPLE_DURATION, default="200ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_source(sens)) cg.add(var.set_sample_duration(config[CONF_SAMPLE_DURATION])) 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/custom/__init__.py b/esphome/components/custom/__init__.py index 0ef87cc6e1..74450300f3 100644 --- a/esphome/components/custom/__init__.py +++ b/esphome/components/custom/__init__.py @@ -1,3 +1,3 @@ import esphome.codegen as cg -custom_ns = cg.esphome_ns.namespace('custom') +custom_ns = cg.esphome_ns.namespace("custom") diff --git a/esphome/components/custom/binary_sensor/__init__.py b/esphome/components/custom/binary_sensor/__init__.py index 83b4a3dad8..18d613d4c1 100644 --- a/esphome/components/custom/binary_sensor/__init__.py +++ b/esphome/components/custom/binary_sensor/__init__.py @@ -4,21 +4,28 @@ from esphome.components import binary_sensor from esphome.const import CONF_BINARY_SENSORS, CONF_ID, CONF_LAMBDA from .. import custom_ns -CustomBinarySensorConstructor = custom_ns.class_('CustomBinarySensorConstructor') +CustomBinarySensorConstructor = custom_ns.class_("CustomBinarySensorConstructor") -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomBinarySensorConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_BINARY_SENSORS): cv.ensure_list(binary_sensor.BINARY_SENSOR_SCHEMA), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomBinarySensorConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_BINARY_SENSORS): cv.ensure_list( + binary_sensor.BINARY_SENSOR_SCHEMA + ), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(binary_sensor.BinarySensorPtr)) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [], + return_type=cg.std_vector.template(binary_sensor.BinarySensorPtr), + ) rhs = CustomBinarySensorConstructor(template_) custom = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_BINARY_SENSORS]): rhs = custom.Pget_binary_sensor(i) - yield binary_sensor.register_binary_sensor(rhs, conf) + await binary_sensor.register_binary_sensor(rhs, conf) diff --git a/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp b/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp index cfd85e6f2a..ea83198568 100644 --- a/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp +++ b/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace custom { -static const char *TAG = "custom.binary_sensor"; +static const char *const TAG = "custom.binary_sensor"; void CustomBinarySensorConstructor::dump_config() { for (auto *child : this->binary_sensors_) { diff --git a/esphome/components/custom/climate/__init__.py b/esphome/components/custom/climate/__init__.py index ed19452dee..a95456133a 100644 --- a/esphome/components/custom/climate/__init__.py +++ b/esphome/components/custom/climate/__init__.py @@ -4,23 +4,27 @@ from esphome.components import climate from esphome.const import CONF_ID, CONF_LAMBDA from .. import custom_ns -CustomClimateConstructor = custom_ns.class_('CustomClimateConstructor') -CONF_CLIMATES = 'climates' +CustomClimateConstructor = custom_ns.class_("CustomClimateConstructor") +CONF_CLIMATES = "climates" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomClimateConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_CLIMATES): cv.ensure_list(climate.CLIMATE_SCHEMA), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomClimateConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_CLIMATES): cv.ensure_list(climate.CLIMATE_SCHEMA), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], - return_type=cg.std_vector.template(climate.Climate.operator('ptr'))) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [], + return_type=cg.std_vector.template(climate.Climate.operator("ptr")), + ) rhs = CustomClimateConstructor(template_) custom = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_CLIMATES]): rhs = custom.Pget_climate(i) - yield climate.register_climate(rhs, conf) + await climate.register_climate(rhs, conf) diff --git a/esphome/components/custom/cover/__init__.py b/esphome/components/custom/cover/__init__.py index 3ab3ec1d04..37fd4cdbbc 100644 --- a/esphome/components/custom/cover/__init__.py +++ b/esphome/components/custom/cover/__init__.py @@ -4,23 +4,27 @@ from esphome.components import cover from esphome.const import CONF_ID, CONF_LAMBDA from .. import custom_ns -CustomCoverConstructor = custom_ns.class_('CustomCoverConstructor') -CONF_COVERS = 'covers' +CustomCoverConstructor = custom_ns.class_("CustomCoverConstructor") +CONF_COVERS = "covers" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomCoverConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_COVERS): cv.ensure_list(cover.COVER_SCHEMA), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomCoverConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_COVERS): cv.ensure_list(cover.COVER_SCHEMA), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], - return_type=cg.std_vector.template(cover.Cover.operator('ptr'))) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [], + return_type=cg.std_vector.template(cover.Cover.operator("ptr")), + ) rhs = CustomCoverConstructor(template_) custom = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_COVERS]): rhs = custom.Pget_cover(i) - yield cover.register_cover(rhs, conf) + await cover.register_cover(rhs, conf) diff --git a/esphome/components/custom/light/__init__.py b/esphome/components/custom/light/__init__.py index 61dd74e661..b6ebe13ab2 100644 --- a/esphome/components/custom/light/__init__.py +++ b/esphome/components/custom/light/__init__.py @@ -4,23 +4,27 @@ from esphome.components import light from esphome.const import CONF_ID, CONF_LAMBDA from .. import custom_ns -CustomLightOutputConstructor = custom_ns.class_('CustomLightOutputConstructor') -CONF_LIGHTS = 'lights' +CustomLightOutputConstructor = custom_ns.class_("CustomLightOutputConstructor") +CONF_LIGHTS = "lights" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomLightOutputConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_LIGHTS): cv.ensure_list(light.ADDRESSABLE_LIGHT_SCHEMA), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomLightOutputConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_LIGHTS): cv.ensure_list(light.ADDRESSABLE_LIGHT_SCHEMA), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], - return_type=cg.std_vector.template(light.LightOutput.operator('ptr'))) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [], + return_type=cg.std_vector.template(light.LightOutput.operator("ptr")), + ) rhs = CustomLightOutputConstructor(template_) custom = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_LIGHTS]): rhs = custom.Pget_light(i) - yield light.register_light(rhs, conf) + await light.register_light(rhs, conf) diff --git a/esphome/components/custom/output/__init__.py b/esphome/components/custom/output/__init__.py index efe6f19dab..97ef070fc3 100644 --- a/esphome/components/custom/output/__init__.py +++ b/esphome/components/custom/output/__init__.py @@ -4,44 +4,58 @@ from esphome.components import output from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE, CONF_BINARY from .. import custom_ns -CustomBinaryOutputConstructor = custom_ns.class_('CustomBinaryOutputConstructor') -CustomFloatOutputConstructor = custom_ns.class_('CustomFloatOutputConstructor') +CustomBinaryOutputConstructor = custom_ns.class_("CustomBinaryOutputConstructor") +CustomFloatOutputConstructor = custom_ns.class_("CustomFloatOutputConstructor") -CONF_FLOAT = 'float' +CONF_FLOAT = "float" -CONFIG_SCHEMA = cv.typed_schema({ - CONF_BINARY: cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomBinaryOutputConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_OUTPUTS): - cv.ensure_list(output.BINARY_OUTPUT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(output.BinaryOutput), - })), - }), - CONF_FLOAT: cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomFloatOutputConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_OUTPUTS): - cv.ensure_list(output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(output.FloatOutput), - })), - }) -}, lower=True) +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_BINARY: cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomBinaryOutputConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_OUTPUTS): cv.ensure_list( + output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(output.BinaryOutput), + } + ) + ), + } + ), + CONF_FLOAT: cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomFloatOutputConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_OUTPUTS): cv.ensure_list( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(output.FloatOutput), + } + ) + ), + } + ), + }, + lower=True, +) -def to_code(config): +async def to_code(config): type = config[CONF_TYPE] - if type == 'binary': + if type == "binary": ret_type = output.BinaryOutputPtr klass = CustomBinaryOutputConstructor else: ret_type = output.FloatOutputPtr klass = CustomFloatOutputConstructor - template_ = yield cg.process_lambda(config[CONF_LAMBDA], [], - return_type=cg.std_vector.template(ret_type)) + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.std_vector.template(ret_type) + ) rhs = klass(template_) custom = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_OUTPUTS]): out = cg.Pvariable(conf[CONF_ID], custom.get_output(i)) - yield output.register_output(out, conf) + await output.register_output(out, conf) diff --git a/esphome/components/custom/output/custom_output.h b/esphome/components/custom/output/custom_output.h index abebe16ee9..1b55d51e29 100644 --- a/esphome/components/custom/output/custom_output.h +++ b/esphome/components/custom/output/custom_output.h @@ -9,7 +9,9 @@ namespace custom { class CustomBinaryOutputConstructor { public: - CustomBinaryOutputConstructor(std::function()> init) { this->outputs_ = init(); } + CustomBinaryOutputConstructor(const std::function()> &init) { + this->outputs_ = init(); + } output::BinaryOutput *get_output(int i) { return this->outputs_[i]; } @@ -19,7 +21,9 @@ class CustomBinaryOutputConstructor { class CustomFloatOutputConstructor { public: - CustomFloatOutputConstructor(std::function()> init) { this->outputs_ = init(); } + CustomFloatOutputConstructor(const std::function()> &init) { + this->outputs_ = init(); + } output::FloatOutput *get_output(int i) { return this->outputs_[i]; } diff --git a/esphome/components/custom/sensor/__init__.py b/esphome/components/custom/sensor/__init__.py index e6da4a733c..bf9421e43e 100644 --- a/esphome/components/custom/sensor/__init__.py +++ b/esphome/components/custom/sensor/__init__.py @@ -4,21 +4,24 @@ from esphome.components import sensor from esphome.const import CONF_ID, CONF_LAMBDA, CONF_SENSORS from .. import custom_ns -CustomSensorConstructor = custom_ns.class_('CustomSensorConstructor') +CustomSensorConstructor = custom_ns.class_("CustomSensorConstructor") -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomSensorConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomSensorConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(sensor.SensorPtr)) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.std_vector.template(sensor.SensorPtr) + ) rhs = CustomSensorConstructor(template_) var = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_SENSORS]): sens = cg.Pvariable(conf[CONF_ID], var.get_sensor(i)) - yield sensor.register_sensor(sens, conf) + await sensor.register_sensor(sens, conf) diff --git a/esphome/components/custom/sensor/custom_sensor.cpp b/esphome/components/custom/sensor/custom_sensor.cpp index 736a9110dd..e670f09530 100644 --- a/esphome/components/custom/sensor/custom_sensor.cpp +++ b/esphome/components/custom/sensor/custom_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace custom { -static const char *TAG = "custom.sensor"; +static const char *const TAG = "custom.sensor"; void CustomSensorConstructor::dump_config() { for (auto *child : this->sensors_) { diff --git a/esphome/components/custom/switch/__init__.py b/esphome/components/custom/switch/__init__.py index fd619e6769..e0b9d7751a 100644 --- a/esphome/components/custom/switch/__init__.py +++ b/esphome/components/custom/switch/__init__.py @@ -4,24 +4,30 @@ from esphome.components import switch from esphome.const import CONF_ID, CONF_LAMBDA, CONF_SWITCHES from .. import custom_ns -CustomSwitchConstructor = custom_ns.class_('CustomSwitchConstructor') +CustomSwitchConstructor = custom_ns.class_("CustomSwitchConstructor") -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomSwitchConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SWITCHES): - cv.ensure_list(switch.SWITCH_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(switch.Switch), - })), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomSwitchConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_SWITCHES): cv.ensure_list( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(switch.Switch), + } + ) + ), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(switch.SwitchPtr)) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.std_vector.template(switch.SwitchPtr) + ) rhs = CustomSwitchConstructor(template_) var = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_SWITCHES]): switch_ = cg.Pvariable(conf[CONF_ID], var.get_switch(i)) - yield switch.register_switch(switch_, conf) + await switch.register_switch(switch_, conf) diff --git a/esphome/components/custom/switch/custom_switch.cpp b/esphome/components/custom/switch/custom_switch.cpp index cc7f6b3640..6d0a8fa621 100644 --- a/esphome/components/custom/switch/custom_switch.cpp +++ b/esphome/components/custom/switch/custom_switch.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace custom { -static const char *TAG = "custom.switch"; +static const char *const TAG = "custom.switch"; void CustomSwitchConstructor::dump_config() { for (auto *child : this->switches_) { diff --git a/esphome/components/custom/switch/custom_switch.h b/esphome/components/custom/switch/custom_switch.h index b96074424d..186e7473fe 100644 --- a/esphome/components/custom/switch/custom_switch.h +++ b/esphome/components/custom/switch/custom_switch.h @@ -8,7 +8,7 @@ namespace custom { class CustomSwitchConstructor : public Component { public: - CustomSwitchConstructor(std::function()> init) { this->switches_ = init(); } + CustomSwitchConstructor(const std::function()> &init) { this->switches_ = init(); } switch_::Switch *get_switch(int i) { return this->switches_[i]; } diff --git a/esphome/components/custom/text_sensor/__init__.py b/esphome/components/custom/text_sensor/__init__.py index 58db2a3f9e..5b6d416436 100644 --- a/esphome/components/custom/text_sensor/__init__.py +++ b/esphome/components/custom/text_sensor/__init__.py @@ -4,25 +4,33 @@ from esphome.components import text_sensor from esphome.const import CONF_ID, CONF_LAMBDA, CONF_TEXT_SENSORS from .. import custom_ns -CustomTextSensorConstructor = custom_ns.class_('CustomTextSensorConstructor') +CustomTextSensorConstructor = custom_ns.class_("CustomTextSensorConstructor") -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomTextSensorConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_TEXT_SENSORS): - cv.ensure_list(text_sensor.TEXT_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - })), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomTextSensorConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Required(CONF_TEXT_SENSORS): cv.ensure_list( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ) + ), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(text_sensor.TextSensorPtr)) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [], + return_type=cg.std_vector.template(text_sensor.TextSensorPtr), + ) rhs = CustomTextSensorConstructor(template_) var = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config[CONF_TEXT_SENSORS]): text = cg.Pvariable(conf[CONF_ID], var.get_text_sensor(i)) - yield text_sensor.register_text_sensor(text, conf) + await text_sensor.register_text_sensor(text, conf) diff --git a/esphome/components/custom/text_sensor/custom_text_sensor.cpp b/esphome/components/custom/text_sensor/custom_text_sensor.cpp index dec96387e7..618ba832a5 100644 --- a/esphome/components/custom/text_sensor/custom_text_sensor.cpp +++ b/esphome/components/custom/text_sensor/custom_text_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace custom { -static const char *TAG = "custom.text_sensor"; +static const char *const TAG = "custom.text_sensor"; void CustomTextSensorConstructor::dump_config() { for (auto *child : this->text_sensors_) { diff --git a/esphome/components/custom/text_sensor/custom_text_sensor.h b/esphome/components/custom/text_sensor/custom_text_sensor.h index aa8c7ddb03..f1e9c7665e 100644 --- a/esphome/components/custom/text_sensor/custom_text_sensor.h +++ b/esphome/components/custom/text_sensor/custom_text_sensor.h @@ -8,7 +8,7 @@ namespace custom { class CustomTextSensorConstructor : public Component { public: - CustomTextSensorConstructor(std::function()> init) { + CustomTextSensorConstructor(const std::function()> &init) { this->text_sensors_ = init(); } diff --git a/esphome/components/custom_component/__init__.py b/esphome/components/custom_component/__init__.py index 198aa6a9ec..d41dd7ea59 100644 --- a/esphome/components/custom_component/__init__.py +++ b/esphome/components/custom_component/__init__.py @@ -2,25 +2,30 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_LAMBDA -custom_component_ns = cg.esphome_ns.namespace('custom_component') -CustomComponentConstructor = custom_component_ns.class_('CustomComponentConstructor') +custom_component_ns = cg.esphome_ns.namespace("custom_component") +CustomComponentConstructor = custom_component_ns.class_("CustomComponentConstructor") MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(CustomComponentConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_COMPONENTS): cv.ensure_list(cv.Schema({ - cv.GenerateID(): cv.declare_id(cg.Component) - }).extend(cv.COMPONENT_SCHEMA)), -}) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomComponentConstructor), + cv.Required(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_COMPONENTS): cv.ensure_list( + cv.Schema({cv.GenerateID(): cv.declare_id(cg.Component)}).extend( + cv.COMPONENT_SCHEMA + ) + ), + } +) -def to_code(config): - template_ = yield cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(cg.ComponentPtr)) +async def to_code(config): + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.std_vector.template(cg.ComponentPtr) + ) rhs = CustomComponentConstructor(template_) var = cg.variable(config[CONF_ID], rhs) for i, conf in enumerate(config.get(CONF_COMPONENTS, [])): comp = cg.Pvariable(conf[CONF_ID], var.get_component(i)) - yield cg.register_component(comp, conf) + await cg.register_component(comp, conf) diff --git a/esphome/components/custom_component/custom_component.h b/esphome/components/custom_component/custom_component.h index 6b009ba549..3f5760e4cf 100644 --- a/esphome/components/custom_component/custom_component.h +++ b/esphome/components/custom_component/custom_component.h @@ -16,7 +16,7 @@ class CustomComponentConstructor { } } - Component *get_component(int i) { return this->components_[i]; } + Component *get_component(int i) const { return this->components_[i]; } protected: std::vector components_; 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 5cc4262105..fc204b2f3b 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -1,32 +1,52 @@ 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_COLD_WHITE, CONF_WARM_WHITE, \ - CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE +from esphome.const import ( + CONF_CONSTANT_BRIGHTNESS, + CONF_OUTPUT_ID, + CONF_COLD_WHITE, + CONF_WARM_WHITE, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, +) -cwww_ns = cg.esphome_ns.namespace('cwww') -CWWWLightOutput = cwww_ns.class_('CWWWLightOutput', light.LightOutput) +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, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield light.register_light(var, config) - cwhite = yield 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])) + await light.register_light(var, config) - wwhite = yield cg.get_variable(config[CONF_WARM_WHITE]) + cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) + cg.add(var.set_cold_white(cwhite)) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) + + 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/climate.py b/esphome/components/daikin/climate.py index ff3f506fb2..af60b17448 100644 --- a/esphome/components/daikin/climate.py +++ b/esphome/components/daikin/climate.py @@ -3,16 +3,18 @@ import esphome.config_validation as cv from esphome.components import climate_ir from esphome.const import CONF_ID -AUTO_LOAD = ['climate_ir'] +AUTO_LOAD = ["climate_ir"] -daikin_ns = cg.esphome_ns.namespace('daikin') -DaikinClimate = daikin_ns.class_('DaikinClimate', climate_ir.ClimateIR) +daikin_ns = cg.esphome_ns.namespace("daikin") +DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(DaikinClimate), -}) +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DaikinClimate), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield climate_ir.register_climate_ir(var, config) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index b6e80d62a7..83d0253691 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace daikin { -static const char *TAG = "daikin.climate"; +static const char *const TAG = "daikin.climate"; void DaikinClimate::transmit_state() { uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00, @@ -12,8 +12,10 @@ void DaikinClimate::transmit_state() { 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00}; remote_state[21] = this->operation_mode_(); - remote_state[24] = this->fan_speed_(); remote_state[22] = this->temperature_(); + uint16_t fan_speed = this->fan_speed_(); + remote_state[24] = fan_speed >> 8; + remote_state[25] = fan_speed & 0xff; // Calculate checksum for (int i = 16; i < 34; i++) { @@ -75,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: @@ -90,25 +92,38 @@ uint8_t DaikinClimate::operation_mode_() { return operating_mode; } -uint8_t DaikinClimate::fan_speed_() { - uint8_t fan_speed; - switch (this->fan_mode) { +uint16_t DaikinClimate::fan_speed_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { case climate::CLIMATE_FAN_LOW: - fan_speed = DAIKIN_FAN_1; + fan_speed = DAIKIN_FAN_1 << 8; break; case climate::CLIMATE_FAN_MEDIUM: - fan_speed = DAIKIN_FAN_3; + fan_speed = DAIKIN_FAN_3 << 8; break; case climate::CLIMATE_FAN_HIGH: - fan_speed = DAIKIN_FAN_5; + fan_speed = DAIKIN_FAN_5 << 8; break; case climate::CLIMATE_FAN_AUTO: default: - fan_speed = DAIKIN_FAN_AUTO; + fan_speed = DAIKIN_FAN_AUTO << 8; } // If swing is enabled switch first 4 bits to 1111 - return this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? fan_speed | 0xF : fan_speed; + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + fan_speed |= 0x0F00; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + fan_speed |= 0x000F; + break; + case climate::CLIMATE_SWING_BOTH: + fan_speed |= 0x0F0F; + break; + default: + break; + } + return fan_speed; } uint8_t DaikinClimate::temperature_() { @@ -116,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; } } @@ -145,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; @@ -159,13 +174,19 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) { this->target_temperature = temperature >> 1; } uint8_t fan_mode = frame[8]; - if (fan_mode & 0xF) + uint8_t swing_mode = frame[9]; + if (fan_mode & 0xF && swing_mode & 0xF) + this->swing_mode = climate::CLIMATE_SWING_BOTH; + else if (fan_mode & 0xF) this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + else if (swing_mode & 0xF) + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; else this->swing_mode = climate::CLIMATE_SWING_OFF; switch (fan_mode & 0xF0) { case DAIKIN_FAN_1: case DAIKIN_FAN_2: + case DAIKIN_FAN_SILENT: this->fan_mode = climate::CLIMATE_FAN_LOW; break; case DAIKIN_FAN_3: @@ -210,7 +231,7 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) { // frame header if (byte != 0x27) return false; - } else if (pos == 3) { + } else if (pos == 3) { // NOLINT(bugprone-branch-clone) // frame header if (byte != 0x00) return false; diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h index 4671d57570..b4ac309de9 100644 --- a/esphome/components/daikin/daikin.h +++ b/esphome/components/daikin/daikin.h @@ -21,6 +21,7 @@ const uint8_t DAIKIN_MODE_ON = 0x01; // Fan Speed const uint8_t DAIKIN_FAN_AUTO = 0xA0; +const uint8_t DAIKIN_FAN_SILENT = 0xB0; const uint8_t DAIKIN_FAN_1 = 0x30; const uint8_t DAIKIN_FAN_2 = 0x40; const uint8_t DAIKIN_FAN_3 = 0x50; @@ -42,17 +43,17 @@ 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_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. void transmit_state() override; uint8_t operation_mode_(); - uint8_t fan_speed_(); + uint16_t fan_speed_(); uint8_t temperature_(); // Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 85ab4300ee..0f71399a7c 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -4,22 +4,22 @@ from esphome import pins from esphome.const import CONF_ID, CONF_PIN MULTI_CONF = True -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ["sensor"] -CONF_ONE_WIRE_ID = 'one_wire_id' -dallas_ns = cg.esphome_ns.namespace('dallas') -DallasComponent = dallas_ns.class_('DallasComponent', cg.PollingComponent) -ESPOneWire = dallas_ns.class_('ESPOneWire') +dallas_ns = cg.esphome_ns.namespace("dallas") +DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(DallasComponent), - cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), - cv.Required(CONF_PIN): pins.gpio_input_pin_schema, -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DallasComponent), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("60s")) -def to_code(config): - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) - one_wire = cg.new_Pvariable(config[CONF_ONE_WIRE_ID], pin) - var = cg.new_Pvariable(config[CONF_ID], one_wire) - yield cg.register_component(var, config) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index aa839e7331..8d7f2e4deb 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace dallas { -static const char *TAG = "dallas.sensor"; +static const char *const TAG = "dallas.sensor"; static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; static const uint8_t DALLAS_MODEL_DS1822 = 0x22; @@ -31,12 +31,11 @@ uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion() const { void DallasComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); - yield(); + pin_->setup(); + one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory) + std::vector raw_sensors; - { - InterruptLock lock; - raw_sensors = this->one_wire_->search_vec(); - } + raw_sensors = this->one_wire_->search_vec(); for (auto &address : raw_sensors) { std::string s = uint64_to_string(address); @@ -70,7 +69,7 @@ void DallasComponent::setup() { } void DallasComponent::dump_config() { ESP_LOGCONFIG(TAG, "DallasComponent:"); - LOG_PIN(" Pin: ", this->one_wire_->get_pin()); + LOG_PIN(" Pin: ", this->pin_); LOG_UPDATE_INTERVAL(this); if (this->found_sensors_.empty()) { @@ -97,29 +96,17 @@ 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(); bool result; - { - InterruptLock lock; - if (!this->one_wire_->reset()) { - result = false; - } else { - result = true; - this->one_wire_->skip(); - this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); - } + if (!this->one_wire_->reset()) { + result = false; + } else { + result = true; + this->one_wire_->skip(); + this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); } if (!result) { @@ -130,14 +117,10 @@ void DallasComponent::update() { for (auto *sensor : this->sensors_) { this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { - bool res; - { - InterruptLock lock; - res = sensor->read_scratch_pad(); - } + bool res = sensor->read_scratch_pad(); 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; @@ -155,13 +138,7 @@ 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,8 +152,8 @@ const std::string &DallasTemperatureSensor::get_address_name() { return this->address_name_; } -bool ICACHE_RAM_ATTR DallasTemperatureSensor::read_scratch_pad() { - ESPOneWire *wire = this->parent_->one_wire_; +bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { + auto *wire = this->parent_->one_wire_; if (!wire->reset()) { return false; } @@ -190,11 +167,7 @@ bool ICACHE_RAM_ATTR DallasTemperatureSensor::read_scratch_pad() { return true; } bool DallasTemperatureSensor::setup_sensor() { - bool r; - { - InterruptLock lock; - r = this->read_scratch_pad(); - } + bool r = this->read_scratch_pad(); if (!r) { ESP_LOGE(TAG, "Reading scratchpad failed: reset"); @@ -228,21 +201,18 @@ bool DallasTemperatureSensor::setup_sensor() { break; } - ESPOneWire *wire = this->parent_->one_wire_; - { - InterruptLock lock; - if (wire->reset()) { - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); - wire->write8(this->scratch_pad_[2]); // high alarm temp - wire->write8(this->scratch_pad_[3]); // low alarm temp - wire->write8(this->scratch_pad_[4]); // resolution - wire->reset(); + auto *wire = this->parent_->one_wire_; + if (wire->reset()) { + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); + wire->write8(this->scratch_pad_[2]); // high alarm temp + wire->write8(this->scratch_pad_[3]); // low alarm temp + wire->write8(this->scratch_pad_[4]); // resolution + wire->reset(); - // write value to EEPROM - wire->select(this->address_); - wire->write8(0x48); - } + // write value to EEPROM + wire->select(this->address_); + wire->write8(0x48); } delay(20); // allow it to finish operation diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index d32aec1758..37c098283a 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -11,10 +11,8 @@ class DallasTemperatureSensor; 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 set_pin(InternalGPIOPin *pin) { pin_ = pin; } + void register_sensor(DallasTemperatureSensor *sensor); void setup() override; void dump_config() override; @@ -25,6 +23,7 @@ class DallasComponent : public PollingComponent { protected: friend DallasTemperatureSensor; + InternalGPIOPin *pin_; ESPOneWire *one_wire_; std::vector sensors_; std::vector found_sensors_; @@ -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 d90b10894d..a0ab10f8a4 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -5,120 +5,128 @@ namespace esphome { namespace dallas { -static const char *TAG = "dallas.one_wire"; +static const char *const TAG = "dallas.one_wire"; const uint8_t ONE_WIRE_ROM_SELECT = 0x55; const int ONE_WIRE_ROM_SEARCH = 0xF0; -ESPOneWire::ESPOneWire(GPIOPin *pin) : pin_(pin) {} +ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); } -bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { +bool HOT IRAM_ATTR ESPOneWire::reset() { + // See reset here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; + + // Wait for communication to clear (delay G) + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); uint8_t retries = 125; - - // Wait for communication to clear - this->pin_->pin_mode(INPUT_PULLUP); do { if (--retries == 0) return false; delayMicroseconds(2); - } while (!this->pin_->digital_read()); + } while (!pin_.digital_read()); - // Send 480µs LOW TX reset pulse - this->pin_->pin_mode(OUTPUT); - this->pin_->digital_write(false); + // Send 480µs LOW TX reset pulse (drive bus low, delay H) + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); delayMicroseconds(480); - // Switch into RX mode, letting the pin float - this->pin_->pin_mode(INPUT_PULLUP); - // after 15µs-60µs wait time, slave pulls low for 60µs-240µs - // let's have 70µs just in case + // Release the bus, delay I + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); delayMicroseconds(70); - bool r = !this->pin_->digital_read(); + // sample bus, 0=device(s) present, 1=no device present + bool r = !pin_.digital_read(); + // delay J delayMicroseconds(410); return r; } -void HOT ICACHE_RAM_ATTR ESPOneWire::write_bit(bool bit) { - // Initiate write/read by pulling low. - this->pin_->pin_mode(OUTPUT); - this->pin_->digital_write(false); +void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { + // See write 1/0 bit here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; - // bus sampled within 15µs and 60µs after pulling LOW. - if (bit) { - // pull high/release within 15µs - delayMicroseconds(10); - this->pin_->digital_write(true); - // in total minimum of 60µs long - delayMicroseconds(55); - } else { - // continue pulling LOW for at least 60µs - delayMicroseconds(65); - this->pin_->digital_write(true); - // grace period, 1µs recovery time - delayMicroseconds(5); - } + // drive bus low + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); + + uint32_t delay0 = bit ? 10 : 65; + uint32_t delay1 = bit ? 55 : 5; + + // delay A/C + delayMicroseconds(delay0); + // release bus + pin_.digital_write(true); + // delay B/D + delayMicroseconds(delay1); } -bool HOT ICACHE_RAM_ATTR ESPOneWire::read_bit() { - // Initiate read slot by pulling LOW for at least 1µs - this->pin_->pin_mode(OUTPUT); - this->pin_->digital_write(false); +bool HOT IRAM_ATTR ESPOneWire::read_bit() { + // See read bit here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; + + // drive bus low, delay A + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); delayMicroseconds(3); - // release bus, we have to sample within 15µs of pulling low - this->pin_->pin_mode(INPUT_PULLUP); + // release bus, delay E + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); delayMicroseconds(10); - bool r = this->pin_->digital_read(); - // read time slot at least 60µs long + 1µs recovery time between slots + // sample bus to read bit from peer + bool r = pin_.digital_read(); + + // delay F delayMicroseconds(53); return r; } -void ICACHE_RAM_ATTR ESPOneWire::write8(uint8_t val) { +void 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 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 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 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 ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void ICACHE_RAM_ATTR ESPOneWire::reset_search() { +void 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 ESPOneWire::search() { if (this->last_device_flag_) { return 0u; } if (!this->reset()) { - // Reset failed + // Reset failed or no devices present this->reset_search(); return 0u; } @@ -196,7 +204,7 @@ uint64_t HOT ICACHE_RAM_ATTR ESPOneWire::search() { return this->rom_number_; } -std::vector ICACHE_RAM_ATTR ESPOneWire::search_vec() { +std::vector ESPOneWire::search_vec() { std::vector res; this->reset_search(); @@ -206,12 +214,11 @@ std::vector ICACHE_RAM_ATTR ESPOneWire::search_vec() { return res; } -void ICACHE_RAM_ATTR ESPOneWire::skip() { +void 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..ef6f079f02 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" +#include namespace esphome { namespace dallas { @@ -11,7 +11,7 @@ extern const int ONE_WIRE_ROM_SEARCH; class ESPOneWire { public: - explicit ESPOneWire(GPIOPin *pin); + explicit ESPOneWire(InternalGPIOPin *pin); /** Reset the bus, should be done before all write operations. * @@ -54,13 +54,11 @@ class ESPOneWire { /// Helper that wraps search in a std::vector. std::vector search_vec(); - GPIOPin *get_pin(); - protected: /// Helper to get the internal 64-bit unsigned rom number as a 8-bit integer pointer. inline uint8_t *rom_number8_(); - GPIOPin *pin_; + ISRInternalGPIOPin pin_; uint8_t last_discrepancy_{0}; uint8_t last_family_discrepancy_{0}; bool last_device_flag_{false}; diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index df9d561995..14ad0efa7b 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -1,28 +1,52 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_ADDRESS, CONF_DALLAS_ID, CONF_INDEX, CONF_RESOLUTION, UNIT_CELSIUS, \ - ICON_THERMOMETER, CONF_ID +from esphome.const import ( + CONF_ADDRESS, + CONF_DALLAS_ID, + CONF_INDEX, + CONF_RESOLUTION, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + CONF_ID, +) from . import DallasComponent, dallas_ns -DallasTemperatureSensor = dallas_ns.class_('DallasTemperatureSensor', sensor.Sensor) +DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sensor) -CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.GenerateID(): cv.declare_id(DallasTemperatureSensor), - cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent), - - cv.Optional(CONF_ADDRESS): cv.hex_int, - cv.Optional(CONF_INDEX): cv.positive_int, - cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), -}), cv.has_exactly_one_key(CONF_ADDRESS, CONF_INDEX)) +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + 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), + cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent), + cv.Optional(CONF_ADDRESS): cv.hex_int, + cv.Optional(CONF_INDEX): cv.positive_int, + cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), + } + ), + cv.has_exactly_one_key(CONF_ADDRESS, CONF_INDEX), +) -def to_code(config): - hub = yield cg.get_variable(config[CONF_DALLAS_ID]) +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) - yield sensor.register_sensor(var, config) + 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..4c47c32ccc --- /dev/null +++ b/esphome/components/dashboard_import/__init__.py @@ -0,0 +1,59 @@ +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, + } +) + +WIFI_CONFIG = """ + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password +""" + + +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}, + "esphome": {"name_add_mac_suffix": False}, + } + p.write_text( + dump(config) + WIFI_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/__init__.py b/esphome/components/debug/__init__.py index a40dadb5c2..32c4339530 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -2,15 +2,18 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_ID -DEPENDENCIES = ['logger'] +CODEOWNERS = ["@OttoWinter"] +DEPENDENCIES = ["logger"] -debug_ns = cg.esphome_ns.namespace('debug') -DebugComponent = debug_ns.class_('DebugComponent', cg.Component) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(DebugComponent), -}).extend(cv.COMPONENT_SCHEMA) +debug_ns = cg.esphome_ns.namespace("debug") +DebugComponent = debug_ns.class_("DebugComponent", cg.Component) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DebugComponent), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 4ffc034d50..40eb20fa6e 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -4,14 +4,27 @@ #include "esphome/core/defines.h" #include "esphome/core/version.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP_IDF +#include +#include +#endif + +#ifdef USE_ESP32 +#if ESP_IDF_VERSION_MAJOR >= 4 +#include +#else #include #endif +#endif + +#ifdef USE_ARDUINO +#include +#endif namespace esphome { namespace debug { -static const char *TAG = "debug"; +static const char *const TAG = "debug"; void DebugComponent::dump_config() { #ifndef ESPHOME_LOG_HAS_DEBUG @@ -21,11 +34,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 +56,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 +67,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 +107,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 +205,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 +217,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 1b766c9928..ba4c2c0d7e 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,73 +1,127 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_CYCLES, \ - CONF_RUN_DURATION, CONF_SLEEP_DURATION, CONF_WAKEUP_PIN +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_NUMBER, + CONF_PINS, + CONF_RUN_DURATION, + CONF_SLEEP_DURATION, + CONF_WAKEUP_PIN, +) 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))) + raise cv.Invalid( + f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup" + ) return value -deep_sleep_ns = cg.esphome_ns.namespace('deep_sleep') -DeepSleepComponent = deep_sleep_ns.class_('DeepSleepComponent', cg.Component) -EnterDeepSleepAction = deep_sleep_ns.class_('EnterDeepSleepAction', automation.Action) -PreventDeepSleepAction = deep_sleep_ns.class_('PreventDeepSleepAction', automation.Action) +deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") +DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) +EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) +PreventDeepSleepAction = deep_sleep_ns.class_( + "PreventDeepSleepAction", automation.Action +) -WakeupPinMode = deep_sleep_ns.enum('WakeupPinMode') +WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") WAKEUP_PIN_MODES = { - 'IGNORE': WakeupPinMode.WAKEUP_PIN_MODE_IGNORE, - 'KEEP_AWAKE': WakeupPinMode.WAKEUP_PIN_MODE_KEEP_AWAKE, - 'INVERT_WAKEUP': WakeupPinMode.WAKEUP_PIN_MODE_INVERT_WAKEUP, + "IGNORE": WakeupPinMode.WAKEUP_PIN_MODE_IGNORE, + "KEEP_AWAKE": WakeupPinMode.WAKEUP_PIN_MODE_KEEP_AWAKE, + "INVERT_WAKEUP": WakeupPinMode.WAKEUP_PIN_MODE_INVERT_WAKEUP, } -esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum('esp_sleep_ext1_wakeup_mode_t') -Ext1Wakeup = deep_sleep_ns.struct('Ext1Wakeup') +esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") +Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") EXT1_WAKEUP_MODES = { - 'ALL_LOW': esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, - 'ANY_HIGH': esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, + "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, + "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, } +WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration") -CONF_WAKEUP_PIN_MODE = 'wakeup_pin_mode' -CONF_ESP32_EXT1_WAKEUP = 'esp32_ext1_wakeup' +CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode" +CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup" +CONF_TOUCH_WAKEUP = "touch_wakeup" +CONF_DEFAULT = "default" +CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason" +CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(DeepSleepComponent), - cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_milliseconds, +WAKEUP_CAUSES_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEFAULT): cv.positive_time_period_milliseconds, + cv.Optional(CONF_TOUCH_WAKEUP_REASON): cv.positive_time_period_milliseconds, + cv.Optional(CONF_GPIO_WAKEUP_REASON): cv.positive_time_period_milliseconds, + } +) - cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_WAKEUP_PIN): cv.All(cv.only_on_esp32, pins.internal_gpio_input_pin_schema, - validate_pin_number), - cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(cv.only_on_esp32, - cv.enum(WAKEUP_PIN_MODES), upper=True), - cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(cv.only_on_esp32, cv.Schema({ - cv.Required(CONF_PINS): cv.ensure_list(pins.shorthand_input_pin, 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.") -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DeepSleepComponent), + cv.Optional(CONF_RUN_DURATION): cv.Any( + cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA), + cv.positive_time_period_milliseconds, + ), + cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_WAKEUP_PIN): cv.All( + cv.only_on_esp32, pins.internal_gpio_input_pin_schema, validate_pin_number + ), + cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All( + cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True + ), + cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( + cv.only_on_esp32, + cv.Schema( + { + cv.Required(CONF_PINS): cv.ensure_list( + pins.internal_gpio_input_pin_schema, validate_pin_number + ), + cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), + } + ), + ), + cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) if CONF_SLEEP_DURATION in config: cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION])) if CONF_WAKEUP_PIN in config: - pin = yield cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) cg.add(var.set_wakeup_pin(pin)) if CONF_WAKEUP_PIN_MODE in config: cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE])) if CONF_RUN_DURATION in config: - cg.add(var.set_run_duration(config[CONF_RUN_DURATION])) + run_duration_config = config[CONF_RUN_DURATION] + if not isinstance(run_duration_config, dict): + cg.add(var.set_run_duration(config[CONF_RUN_DURATION])) + else: + default_run_duration = run_duration_config[CONF_DEFAULT] + wakeup_cause_to_run_duration = cg.StructInitializer( + WakeupCauseToRunDuration, + ("default_cause", default_run_duration), + ( + "touch_cause", + run_duration_config.get( + CONF_TOUCH_WAKEUP_REASON, default_run_duration + ), + ), + ( + "gpio_cause", + run_duration_config.get( + CONF_GPIO_WAKEUP_REASON, default_run_duration + ), + ), + ) + cg.add(var.set_run_duration(wakeup_cause_to_run_duration)) if CONF_ESP32_EXT1_WAKEUP in config: conf = config[CONF_ESP32_EXT1_WAKEUP] @@ -75,27 +129,48 @@ def to_code(config): for pin in conf[CONF_PINS]: mask |= 1 << pin[CONF_NUMBER] struct = cg.StructInitializer( - Ext1Wakeup, - ('mask', mask), - ('wakeup_mode', conf[CONF_MODE]) + Ext1Wakeup, ("mask", mask), ("wakeup_mode", conf[CONF_MODE]) ) cg.add(var.set_ext1_wakeup(struct)) - cg.add_define('USE_DEEP_SLEEP') + if CONF_TOUCH_WAKEUP in config: + cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP])) + + cg.add_define("USE_DEEP_SLEEP") -DEEP_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id({ - cv.GenerateID(): cv.use_id(DeepSleepComponent), -}) +DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DeepSleepComponent), + cv.Optional(CONF_SLEEP_DURATION): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) -@automation.register_action('deep_sleep.enter', EnterDeepSleepAction, DEEP_SLEEP_ACTION_SCHEMA) -def deep_sleep_enter_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) +DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DeepSleepComponent), + } +) -@automation.register_action('deep_sleep.prevent', PreventDeepSleepAction, DEEP_SLEEP_ACTION_SCHEMA) -def deep_sleep_prevent_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) +@automation.register_action( + "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA +) +async def deep_sleep_enter_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_SLEEP_DURATION in config: + template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) + cg.add(var.set_sleep_duration(template_)) + return var + + +@automation.register_action( + "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA +) +async def deep_sleep_prevent_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 684d7e12bf..7774014d3d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -2,19 +2,46 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#ifdef USE_ESP8266 +#include +#endif + namespace esphome { namespace deep_sleep { -static const char *TAG = "deep_sleep"; +static const char *const TAG = "deep_sleep"; -bool global_has_deep_sleep = false; +bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +optional DeepSleepComponent::get_run_duration_() const { +#ifdef USE_ESP32 + if (this->wakeup_cause_to_run_duration_.has_value()) { + esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); + switch (wakeup_cause) { + case ESP_SLEEP_WAKEUP_EXT0: + case ESP_SLEEP_WAKEUP_EXT1: + return this->wakeup_cause_to_run_duration_->gpio_cause; + case ESP_SLEEP_WAKEUP_TOUCHPAD: + return this->wakeup_cause_to_run_duration_->touch_cause; + default: + return this->wakeup_cause_to_run_duration_->default_cause; + } + } +#endif + return this->run_duration_; +} void DeepSleepComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Deep Sleep..."); global_has_deep_sleep = true; - if (this->run_duration_.has_value()) - this->set_timeout(*this->run_duration_, [this]() { this->begin_sleep(); }); + const optional run_duration = get_run_duration_(); + if (run_duration.has_value()) { + ESP_LOGI(TAG, "Scheduling Deep Sleep to start in %u ms", *run_duration); + this->set_timeout(*run_duration, [this]() { this->begin_sleep(); }); + } else { + ESP_LOGD(TAG, "Not scheduling Deep Sleep, as no run duration is configured."); + } } void DeepSleepComponent::dump_config() { ESP_LOGCONFIG(TAG, "Setting up Deep Sleep..."); @@ -25,9 +52,14 @@ 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_); + } + if (this->wakeup_cause_to_run_duration_.has_value()) { + ESP_LOGCONFIG(TAG, " Default Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->default_cause); + ESP_LOGCONFIG(TAG, " Touch Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->touch_cause); + ESP_LOGCONFIG(TAG, " GPIO Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->gpio_cause); } #endif } @@ -39,11 +71,15 @@ 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; } +void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { + wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; +} #endif void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { @@ -51,9 +87,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 +104,30 @@ 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 4372a3f66c..59df199a9f 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. @@ -27,6 +32,15 @@ struct Ext1Wakeup { esp_sleep_ext1_wakeup_mode_t wakeup_mode; }; +struct WakeupCauseToRunDuration { + // Run duration if woken up by timer or any other reason besides those below. + uint32_t default_cause; + // Run duration if woken up by touch pads. + uint32_t touch_cause; + // Run duration if woken up by GPIO pins. + uint32_t gpio_cause; +}; + #endif template class EnterDeepSleepAction; @@ -43,15 +57,22 @@ 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); + + // Set the duration in ms for how long the code should run before entering + // deep sleep mode, according to the cause the ESP32 has woken. + void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); + #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); @@ -68,24 +89,36 @@ class DeepSleepComponent : public Component { void prevent_deep_sleep(); protected: + // Returns nullopt if no run duration is set. Otherwise, returns the run + // duration before entering deep sleep. + optional get_run_duration_() const; + 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_; + optional wakeup_cause_to_run_duration_; #endif optional run_duration_; bool next_enter_deep_sleep_{false}; bool prevent_{false}; }; -extern bool global_has_deep_sleep; +extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class EnterDeepSleepAction : public Action { public: EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} + TEMPLATABLE_VALUE(uint32_t, sleep_duration); - void play(Ts... x) override { this->deep_sleep_->begin_sleep(true); } + void play(Ts... x) override { + if (this->sleep_duration_.has_value()) { + this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); + } + this->deep_sleep_->begin_sleep(true); + } protected: DeepSleepComponent *deep_sleep_; 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 890c2bede4..3cdfc8ab85 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -4,216 +4,351 @@ from esphome import automation from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE from esphome.components import uart -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@glmnet"] -dfplayer_ns = cg.esphome_ns.namespace('dfplayer') -DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) -DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_('DFPlayerFinishedPlaybackTrigger', - automation.Trigger.template()) -DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', automation.Condition) +dfplayer_ns = cg.esphome_ns.namespace("dfplayer") +DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component) +DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_( + "DFPlayerFinishedPlaybackTrigger", automation.Trigger.template() +) +DFPlayerIsPlayingCondition = dfplayer_ns.class_( + "DFPlayerIsPlayingCondition", automation.Condition +) MULTI_CONF = True -CONF_FOLDER = 'folder' -CONF_LOOP = 'loop' -CONF_VOLUME = 'volume' -CONF_EQ_PRESET = 'eq_preset' -CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' +CONF_FOLDER = "folder" +CONF_LOOP = "loop" +CONF_VOLUME = "volume" +CONF_EQ_PRESET = "eq_preset" +CONF_ON_FINISHED_PLAYBACK = "on_finished_playback" EqPreset = dfplayer_ns.enum("EqPreset") EQ_PRESET = { - 'NORMAL': EqPreset.NORMAL, - 'POP': EqPreset.POP, - 'ROCK': EqPreset.ROCK, - 'JAZZ': EqPreset.JAZZ, - 'CLASSIC': EqPreset.CLASSIC, - 'BASS': EqPreset.BASS, + "NORMAL": EqPreset.NORMAL, + "POP": EqPreset.POP, + "ROCK": EqPreset.ROCK, + "JAZZ": EqPreset.JAZZ, + "CLASSIC": EqPreset.CLASSIC, + "BASS": EqPreset.BASS, } Device = dfplayer_ns.enum("Device") DEVICE = { - 'USB': Device.USB, - 'TF_CARD': Device.TF_CARD, + "USB": Device.USB, + "TF_CARD": Device.TF_CARD, } -NextAction = dfplayer_ns.class_('NextAction', automation.Action) -PreviousAction = dfplayer_ns.class_('PreviousAction', automation.Action) -PlayFileAction = dfplayer_ns.class_('PlayFileAction', automation.Action) -PlayFolderAction = dfplayer_ns.class_('PlayFolderAction', automation.Action) -SetVolumeAction = dfplayer_ns.class_('SetVolumeAction', automation.Action) -SetEqAction = dfplayer_ns.class_('SetEqAction', automation.Action) -SleepAction = dfplayer_ns.class_('SleepAction', automation.Action) -ResetAction = dfplayer_ns.class_('ResetAction', automation.Action) -StartAction = dfplayer_ns.class_('StartAction', automation.Action) -PauseAction = dfplayer_ns.class_('PauseAction', automation.Action) -StopAction = dfplayer_ns.class_('StopAction', automation.Action) -RandomAction = dfplayer_ns.class_('RandomAction', automation.Action) -SetDeviceAction = dfplayer_ns.class_('SetDeviceAction', automation.Action) +NextAction = dfplayer_ns.class_("NextAction", automation.Action) +PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action) +PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action) +PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action) +SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action) +VolumeUpAction = dfplayer_ns.class_("VolumeUpAction", automation.Action) +VolumeDownAction = dfplayer_ns.class_("VolumeDownAction", automation.Action) +SetEqAction = dfplayer_ns.class_("SetEqAction", automation.Action) +SleepAction = dfplayer_ns.class_("SleepAction", automation.Action) +ResetAction = dfplayer_ns.class_("ResetAction", automation.Action) +StartAction = dfplayer_ns.class_("StartAction", automation.Action) +PauseAction = dfplayer_ns.class_("PauseAction", automation.Action) +StopAction = dfplayer_ns.class_("StopAction", automation.Action) +RandomAction = dfplayer_ns.class_("RandomAction", automation.Action) +SetDeviceAction = dfplayer_ns.class_("SetDeviceAction", automation.Action) -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(DFPlayer), - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DFPlayerFinishedPlaybackTrigger), - }), -}).extend(uart.UART_DEVICE_SCHEMA)) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DFPlayer), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DFPlayerFinishedPlaybackTrigger + ), + } + ), + } + ).extend(uart.UART_DEVICE_SCHEMA) +) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "dfplayer", baud_rate=9600, require_tx=True +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) -@automation.register_action('dfplayer.play_next', NextAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_next_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.play_next", + NextAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_next_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.play_previous', PreviousAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_previous_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.play_previous", + PreviousAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_previous_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.play', PlayFileAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(DFPlayer), - cv.Required(CONF_FILE): cv.templatable(cv.int_), - cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), -}, key=CONF_FILE)) -def dfplayer_play_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.play", + PlayFileAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), + }, + key=CONF_FILE, + ), +) +async def dfplayer_play_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_FILE], args, float) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_FILE], args, float) cg.add(var.set_file(template_)) if CONF_LOOP in config: - template_ = yield cg.templatable(config[CONF_LOOP], args, float) + template_ = await cg.templatable(config[CONF_LOOP], args, float) cg.add(var.set_loop(template_)) - yield var + return var -@automation.register_action('dfplayer.play_folder', PlayFolderAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), - cv.Required(CONF_FOLDER): cv.templatable(cv.int_), - cv.Optional(CONF_FILE): cv.templatable(cv.int_), - cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), -})) -def dfplayer_play_folder_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.play_folder", + PlayFolderAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FOLDER): cv.templatable(cv.int_), + cv.Optional(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), + } + ), +) +async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_FOLDER], args, float) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_FOLDER], args, float) cg.add(var.set_folder(template_)) if CONF_FILE in config: - template_ = yield cg.templatable(config[CONF_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, float) cg.add(var.set_file(template_)) if CONF_LOOP in config: - template_ = yield cg.templatable(config[CONF_LOOP], args, float) + template_ = await cg.templatable(config[CONF_LOOP], args, float) cg.add(var.set_loop(template_)) - yield var + return var -@automation.register_action('dfplayer.set_device', SetDeviceAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(DFPlayer), - cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True), -}, key=CONF_DEVICE)) -def dfplayer_set_device_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.set_device", + SetDeviceAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True), + }, + key=CONF_DEVICE, + ), +) +async def dfplayer_set_device_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_DEVICE], args, Device) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_DEVICE], args, Device) cg.add(var.set_device(template_)) - yield var + return var -@automation.register_action('dfplayer.set_volume', SetVolumeAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(DFPlayer), - cv.Required(CONF_VOLUME): cv.templatable(cv.int_), -}, key=CONF_VOLUME)) -def dfplayer_set_volume_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.set_volume", + SetVolumeAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_VOLUME): cv.templatable(cv.int_), + }, + key=CONF_VOLUME, + ), +) +async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_VOLUME], args, float) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VOLUME], args, float) cg.add(var.set_volume(template_)) - yield var + return var -@automation.register_action('dfplayer.set_eq', SetEqAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(DFPlayer), - cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)), -}, key=CONF_EQ_PRESET)) -def dfplayer_set_eq_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.volume_up", + VolumeUpAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "dfplayer.volume_down", + VolumeDownAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "dfplayer.set_eq", + SetEqAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)), + }, + key=CONF_EQ_PRESET, + ), +) +async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_EQ_PRESET], args, EqPreset) cg.add(var.set_eq(template_)) - yield var + return var -@automation.register_action('dfplayer.sleep', SleepAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_sleep_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.sleep", + SleepAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_sleep_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.reset', ResetAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_reset_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.reset", + ResetAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.start', StartAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_start_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.start", + StartAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.pause', PauseAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_pause_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.pause", + PauseAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_pause_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.stop', StopAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_stop_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.stop", + StopAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_action('dfplayer.random', RandomAction, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplayer_random_to_code(config, action_id, template_arg, args): +@automation.register_action( + "dfplayer.random", + RandomAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplayer_random_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var -@automation.register_condition('dfplayer.is_playing', DFPlayerIsPlayingCondition, cv.Schema({ - cv.GenerateID(): cv.use_id(DFPlayer), -})) -def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): +@automation.register_condition( + "dfplayer.is_playing", + DFPlayerIsPlayingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DFPlayer), + } + ), +) +async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 6fed433dac..7df551f5d2 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace dfplayer { -static const char* TAG = "dfplayer"; +static const char *const TAG = "dfplayer"; void DFPlayer::play_folder(uint16_t folder, uint16_t file) { if (folder < 100 && file < 256) { diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 22ca11c3be..ae47cb33f1 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -103,8 +103,10 @@ class DFPlayer : public uart::UARTDevice, public Component { }; #define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ - template class ACTION_CLASS : public Action, public Parented { \ - public: \ + template \ + class ACTION_CLASS : /* NOLINT */ \ + public Action, \ + public Parented { \ void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ }; @@ -114,7 +116,8 @@ 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...); auto loop = this->loop_.value(x...); @@ -130,7 +133,8 @@ 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...); auto file = this->file_.value(x...); @@ -146,6 +150,7 @@ template class PlayFolderAction : public Action, public P template class SetDeviceAction : public Action, public Parented { public: TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); @@ -155,6 +160,7 @@ template class SetDeviceAction : public Action, public Pa template class SetVolumeAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); @@ -164,6 +170,7 @@ template class SetVolumeAction : public Action, public Pa template class SetEqAction : public Action, public Parented { public: TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); @@ -176,6 +183,8 @@ DFPLAYER_SIMPLE_ACTION(StartAction, start) DFPLAYER_SIMPLE_ACTION(PauseAction, pause) DFPLAYER_SIMPLE_ACTION(StopAction, stop) DFPLAYER_SIMPLE_ACTION(RandomAction, random) +DFPLAYER_SIMPLE_ACTION(VolumeUpAction, volume_up) +DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down) template class DFPlayerIsPlayingCondition : public Condition, public Parented { public: diff --git a/esphome/components/dht/__init__.py b/esphome/components/dht/__init__.py index e69de29bb2..71a87b6ae5 100644 --- a/esphome/components/dht/__init__.py +++ b/esphome/components/dht/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index c311aa43ec..2a4ccf1529 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace dht { -static const char *TAG = "dht"; +static const char *const TAG = "dht"; void DHT::setup() { ESP_LOGCONFIG(TAG, "Setting up DHT..."); @@ -32,19 +32,19 @@ void DHT::dump_config() { void DHT::update() { float temperature, humidity; - bool error; + bool success; if (this->model_ == DHT_MODEL_AUTO_DETECT) { this->model_ = DHT_MODEL_DHT22; - error = this->read_sensor_(&temperature, &humidity, false); - if (error) { + success = this->read_sensor_(&temperature, &humidity, false); + if (!success) { this->model_ = DHT_MODEL_DHT11; return; } } else { - error = this->read_sensor_(&temperature, &humidity, true); + success = this->read_sensor_(&temperature, &humidity, true); } - if (error) { + if (success) { ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); if (this->temperature_sensor_ != nullptr) @@ -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,24 +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 { - 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; @@ -199,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]; @@ -208,7 +215,7 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); - if ((raw_temperature & 0x8000) != 0) + if (this->model_ != DHT_MODEL_DHT22_TYPE2 && (raw_temperature & 0x8000) != 0) raw_temperature = ~(raw_temperature & 0x7FFF); if (raw_temperature == 1 && raw_humidity == 10) { diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 4ed5d4e022..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 { @@ -12,7 +13,8 @@ enum DHTModel { DHT_MODEL_DHT22, DHT_MODEL_AM2302, DHT_MODEL_RHT03, - DHT_MODEL_SI7021 + DHT_MODEL_SI7021, + DHT_MODEL_DHT22_TYPE2 }; /// Component for reading temperature/humidity measurements from DHT11/DHT22 sensors. @@ -28,12 +30,13 @@ class DHT : public PollingComponent { * - DHT_MODEL_AM2302 * - DHT_MODEL_RHT03 * - DHT_MODEL_SI7021 + * - DHT_MODEL_DHT22_TYPE2 * * @param model The DHT model. */ 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; } @@ -49,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 8455f74fb4..1334f0270c 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -2,43 +2,69 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, \ - ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_MODEL, + CONF_PIN, + CONF_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + from esphome.cpp_helpers import gpio_pin_expression -dht_ns = cg.esphome_ns.namespace('dht') -DHTModel = dht_ns.enum('DHTModel') +dht_ns = cg.esphome_ns.namespace("dht") +DHTModel = dht_ns.enum("DHTModel") DHT_MODELS = { - 'AUTO_DETECT': DHTModel.DHT_MODEL_AUTO_DETECT, - 'DHT11': DHTModel.DHT_MODEL_DHT11, - 'DHT22': DHTModel.DHT_MODEL_DHT22, - 'AM2302': DHTModel.DHT_MODEL_AM2302, - 'RHT03': DHTModel.DHT_MODEL_RHT03, - 'SI7021': DHTModel.DHT_MODEL_SI7021, + "AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT, + "DHT11": DHTModel.DHT_MODEL_DHT11, + "DHT22": DHTModel.DHT_MODEL_DHT22, + "AM2302": DHTModel.DHT_MODEL_AM2302, + "RHT03": DHTModel.DHT_MODEL_RHT03, + "SI7021": DHTModel.DHT_MODEL_SI7021, + "DHT22_TYPE2": DHTModel.DHT_MODEL_DHT22_TYPE2, } -DHT = dht_ns.class_('DHT', cg.PollingComponent) +DHT = dht_ns.class_("DHT", cg.PollingComponent) -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_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), - cv.Optional(CONF_MODEL, default='auto detect'): cv.enum(DHT_MODELS, upper=True, space='_'), -}).extend(cv.polling_component_schema('60s')) +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_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MODEL, default="auto detect"): cv.enum( + DHT_MODELS, upper=True, space="_" + ), + } +).extend(cv.polling_component_schema("60s")) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - pin = yield gpio_pin_expression(config[CONF_PIN]) + pin = await gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) cg.add(var.set_dht_model(config[CONF_MODEL])) diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 3c18f8055d..a5e1886918 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -8,7 +8,7 @@ namespace esphome { namespace dht12 { -static const char *TAG = "dht12"; +static const char *const TAG = "dht12"; void DHT12Component::update() { uint8_t data[5]; diff --git a/esphome/components/dht12/sensor.py b/esphome/components/dht12/sensor.py index 7d86e8c836..ae2173ef22 100644 --- a/esphome/components/dht12/sensor.py +++ b/esphome/components/dht12/sensor.py @@ -1,30 +1,54 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -dht12_ns = cg.esphome_ns.namespace('dht12') -DHT12Component = dht12_ns.class_('DHT12Component', cg.PollingComponent, i2c.I2CDevice) +dht12_ns = cg.esphome_ns.namespace("dht12") +DHT12Component = dht12_ns.class_("DHT12Component", cg.PollingComponent, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(DHT12Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x5C)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DHT12Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5C)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 951d561caa..0d403f99f0 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -2,19 +2,41 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION -from esphome.core import coroutine, coroutine_with_priority +from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, + CONF_ID, + CONF_LAMBDA, + CONF_PAGES, + CONF_PAGE_ID, + CONF_ROTATION, + CONF_FROM, + CONF_TO, + CONF_TRIGGER_ID, +) +from esphome.core import coroutine_with_priority IS_PLATFORM_COMPONENT = True -display_ns = cg.esphome_ns.namespace('display') -DisplayBuffer = display_ns.class_('DisplayBuffer') -DisplayPage = display_ns.class_('DisplayPage') -DisplayPagePtr = DisplayPage.operator('ptr') -DisplayBufferRef = DisplayBuffer.operator('ref') -DisplayPageShowAction = display_ns.class_('DisplayPageShowAction', automation.Action) -DisplayPageShowNextAction = display_ns.class_('DisplayPageShowNextAction', automation.Action) -DisplayPageShowPrevAction = display_ns.class_('DisplayPageShowPrevAction', automation.Action) +display_ns = cg.esphome_ns.namespace("display") +DisplayBuffer = display_ns.class_("DisplayBuffer") +DisplayPage = display_ns.class_("DisplayPage") +DisplayPagePtr = DisplayPage.operator("ptr") +DisplayBufferRef = DisplayBuffer.operator("ref") +DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action) +DisplayPageShowNextAction = display_ns.class_( + "DisplayPageShowNextAction", automation.Action +) +DisplayPageShowPrevAction = display_ns.class_( + "DisplayPageShowPrevAction", automation.Action +) +DisplayIsDisplayingPageCondition = display_ns.class_( + "DisplayIsDisplayingPageCondition", automation.Condition +) +DisplayOnPageChangeTrigger = display_ns.class_( + "DisplayOnPageChangeTrigger", automation.Trigger +) + +CONF_ON_PAGE_CHANGE = "on_page_change" DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -31,69 +53,139 @@ def validate_rotation(value): return cv.enum(DISPLAY_ROTATIONS, int=True)(value) -BASIC_DISPLAY_SCHEMA = cv.Schema({ - cv.Optional(CONF_LAMBDA): cv.lambda_, -}) +BASIC_DISPLAY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_LAMBDA): cv.lambda_, + } +) -FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend({ - cv.Optional(CONF_ROTATION): validate_rotation, - cv.Optional(CONF_PAGES): cv.All(cv.ensure_list({ - cv.GenerateID(): cv.declare_id(DisplayPage), - cv.Required(CONF_LAMBDA): cv.lambda_, - }), cv.Length(min=1)), -}) +FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( + { + cv.Optional(CONF_ROTATION): validate_rotation, + cv.Optional(CONF_PAGES): cv.All( + cv.ensure_list( + { + cv.GenerateID(): cv.declare_id(DisplayPage), + cv.Required(CONF_LAMBDA): cv.lambda_, + } + ), + cv.Length(min=1), + ), + cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayOnPageChangeTrigger + ), + cv.Optional(CONF_FROM): cv.use_id(DisplayPage), + cv.Optional(CONF_TO): cv.use_id(DisplayPage), + } + ), + cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, + } +) -@coroutine -def setup_display_core_(var, config): +async def setup_display_core_(var, config): if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) + + if CONF_AUTO_CLEAR_ENABLED in config: + cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED])) + if CONF_PAGES in config: pages = [] for conf in config[CONF_PAGES]: - lambda_ = yield cg.process_lambda(conf[CONF_LAMBDA], [(DisplayBufferRef, 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void + ) page = cg.new_Pvariable(conf[CONF_ID], lambda_) pages.append(page) cg.add(var.set_pages(pages)) + for conf in config.get(CONF_ON_PAGE_CHANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if CONF_FROM in conf: + page = await cg.get_variable(conf[CONF_FROM]) + cg.add(trigger.set_from(page)) + if CONF_TO in conf: + page = await cg.get_variable(conf[CONF_TO]) + cg.add(trigger.set_to(page)) + await automation.build_automation( + trigger, [(DisplayPagePtr, "from"), (DisplayPagePtr, "to")], conf + ) -@coroutine -def register_display(var, config): - yield setup_display_core_(var, config) +async def register_display(var, config): + await setup_display_core_(var, config) -@automation.register_action('display.page.show', DisplayPageShowAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)), -})) -def display_page_show_to_code(config, action_id, template_arg, args): +@automation.register_action( + "display.page.show", + DisplayPageShowAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)), + } + ), +) +async def display_page_show_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) if isinstance(config[CONF_ID], core.Lambda): - template_ = yield cg.templatable(config[CONF_ID], args, DisplayPagePtr) + template_ = await cg.templatable(config[CONF_ID], args, DisplayPagePtr) cg.add(var.set_page(template_)) else: - paren = yield cg.get_variable(config[CONF_ID]) + paren = await cg.get_variable(config[CONF_ID]) cg.add(var.set_page(paren)) - yield var + return var -@automation.register_action('display.page.show_next', DisplayPageShowNextAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), -})) -def display_page_show_next_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) +@automation.register_action( + "display.page.show_next", + DisplayPageShowNextAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), + } + ), +) +async def display_page_show_next_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('display.page.show_previous', DisplayPageShowPrevAction, - maybe_simple_id({ - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), - })) -def display_page_show_previous_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) +@automation.register_action( + "display.page.show_previous", + DisplayPageShowPrevAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), + } + ), +) +async def display_page_show_previous_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( + "display.is_displaying_page", + DisplayIsDisplayingPageCondition, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer), + cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage), + }, + key=CONF_PAGE_ID, + ), +) +async def display_is_displaying_page_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + page = await cg.get_variable(config[CONF_PAGE_ID]) + var = cg.new_Pvariable(condition_id, template_arg, paren) + cg.add(var.set_page(page)) + + return var @coroutine_with_priority(100.0) -def to_code(config): +async def to_code(config): cg.add_global(display_ns.using) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 453e114338..1458629acd 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,24 +1,28 @@ #include "display_buffer.h" -#include "esphome/core/log.h" + +#include #include "esphome/core/application.h" +#include "esphome/core/color.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace display { -static const char *TAG = "display"; +static const char *const TAG = "display"; -const uint8_t COLOR_OFF = 0; -const uint8_t COLOR_ON = 1; +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; } this->clear(); } -void DisplayBuffer::fill(int color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } +void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { switch (this->rotation_) { @@ -43,7 +47,7 @@ int DisplayBuffer::get_height() { } } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } -void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { +void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -63,7 +67,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { this->draw_absolute_pixel_internal(x, y, color); App.feed_wdt(); } -void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { +void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; int32_t err = dx + dy; @@ -83,29 +87,29 @@ void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { } } } -void HOT DisplayBuffer::horizontal_line(int x, int y, int width, int color) { +void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } -void HOT DisplayBuffer::vertical_line(int x, int y, int height, int color) { +void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } -void DisplayBuffer::rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } -void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } -void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int color) { +void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; int err = 2 - 2 * radius; @@ -128,7 +132,7 @@ void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int colo } } while (dx <= 0); } -void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int color) { +void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; int err = 2 - 2 * radius; @@ -155,7 +159,7 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int co } while (dx <= 0); } -void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { int x_start, y_start; int width, height; this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); @@ -169,7 +173,7 @@ void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, // Unknown char, skip ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); if (!font->get_glyphs().empty()) { - uint8_t glyph_width = font->get_glyphs()[0].width_; + uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width; for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) for (int glyph_y = 0; glyph_y < height; glyph_y++) this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); @@ -192,24 +196,51 @@ void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, } } - x_at += glyph.width_ + glyph.offset_x_; + x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; i += match_length; } } -void DisplayBuffer::vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg) { +void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image) { - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? COLOR_ON : COLOR_OFF); - } + +void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { + switch (image->get_type()) { + case IMAGE_TYPE_BINARY: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); + } + } + break; + case IMAGE_TYPE_GRAYSCALE: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); + } + } + break; } } + +#ifdef USE_GRAPH +void DisplayBuffer::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); } +void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) { + graph->draw_legend(this, x, y, color_on); +} +#endif // USE_GRAPH + 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; @@ -248,7 +279,7 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, break; } } -void DisplayBuffer::print(int x, int y, Font *font, int color, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text); } void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { @@ -257,13 +288,13 @@ void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char void DisplayBuffer::print(int x, int y, Font *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, align, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); @@ -278,7 +309,7 @@ void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) { va_list arg; va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, TextAlign::CENTER_LEFT, format, arg); + this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); va_end(arg); } void DisplayBuffer::set_writer(display_writer_t &&writer) { this->writer_ = writer; } @@ -294,26 +325,39 @@ void DisplayBuffer::set_pages(std::vector pages) { pages[pages.size() - 1]->set_next(pages[0]); this->show_page(pages[0]); } -void DisplayBuffer::show_page(DisplayPage *page) { this->page_ = page; } +void DisplayBuffer::show_page(DisplayPage *page) { + this->previous_page_ = this->page_; + this->page_ = page; + if (this->previous_page_ != this->page_) { + for (auto *t : on_page_change_triggers_) + t->process(this->previous_page_, this->page_); + } +} void DisplayBuffer::show_next_page() { this->page_->show_next(); } void DisplayBuffer::show_prev_page() { this->page_->show_prev(); } void DisplayBuffer::do_update_() { - this->clear(); + if (this->auto_clear_enabled_) { + this->clear(); + } if (this->page_ != nullptr) { this->page_->get_writer()(*this); } else if (this->writer_.has_value()) { (*this->writer_)(*this); } } +void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { + if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) + this->trigger(from, to); +} #ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { @@ -324,35 +368,27 @@ void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time: } #endif -Glyph::Glyph(const char *a_char, const uint8_t *data_start, uint32_t offset, int offset_x, int offset_y, int width, - int height) - : char_(a_char), - data_(data_start + offset), - offset_x_(offset_x), - offset_y_(offset_y), - width_(width), - height_(height) {} bool Glyph::get_pixel(int x, int y) const { - const int x_data = x - this->offset_x_; - const int y_data = y - this->offset_y_; - if (x_data < 0 || x_data >= this->width_ || y_data < 0 || y_data >= this->height_) + const int x_data = x - this->glyph_data_->offset_x; + const int y_data = y - this->glyph_data_->offset_y; + if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height) return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + 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->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->char_; } +const char *Glyph::get_char() const { return this->glyph_data_->a_char; } bool Glyph::compare_to(const char *str) const { // 1 -> this->char_ // 2 -> str for (uint32_t i = 0;; i++) { - if (this->char_[i] == '\0') + if (this->glyph_data_->a_char[i] == '\0') return true; if (str[i] == '\0') return false; - if (this->char_[i] > str[i]) + if (this->glyph_data_->a_char[i] > str[i]) return false; - if (this->char_[i] < str[i]) + if (this->glyph_data_->a_char[i] < str[i]) return true; } // this should not happen @@ -360,19 +396,19 @@ bool Glyph::compare_to(const char *str) const { } int Glyph::match_length(const char *str) const { for (uint32_t i = 0;; i++) { - if (this->char_[i] == '\0') + if (this->glyph_data_->a_char[i] == '\0') return i; - if (str[i] != this->char_[i]) + if (str[i] != this->glyph_data_->a_char[i]) return 0; } // this should not happen return 0; } void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->offset_x_; - *y1 = this->offset_y_; - *width = this->width_; - *height = this->height_; + *x1 = this->glyph_data_->offset_x; + *y1 = this->glyph_data_->offset_y; + *width = this->glyph_data_->width; + *height = this->glyph_data_->height; } int Font::match_next_glyph(const char *str, int *match_length) { int lo = 0; @@ -402,17 +438,17 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in if (glyph_n < 0) { // Unknown char, skip if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].width_; + x += this->get_glyphs()[0].glyph_data_->width; i++; continue; } const Glyph &glyph = this->glyphs_[glyph_n]; if (!has_char) - min_x = glyph.offset_x_; + min_x = glyph.glyph_data_->offset_x; else - min_x = std::min(min_x, x + glyph.offset_x_); - x += glyph.width_ + glyph.offset_x_; + min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; i += match_length; has_char = true; @@ -421,22 +457,84 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *width = x - min_x; } const std::vector &Font::get_glyphs() const { return this->glyphs_; } -Font::Font(std::vector &&glyphs, int baseline, int bottom) - : glyphs_(std::move(glyphs)), baseline_(baseline), bottom_(bottom) {} +Font::Font(const GlyphData *data, int data_nr, int baseline, int bottom) : baseline_(baseline), bottom_(bottom) { + for (int i = 0; i < data_nr; ++i) + glyphs_.emplace_back(data + i); +} bool Image::get_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return false; const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t pos = x + y * width_8; - return 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 Color::BLACK; + const uint32_t pos = (x + y * this->width_) * 3; + 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 Color::BLACK; + const uint32_t pos = (x + y * this->width_); + 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_; } int Image::get_height() const { return this->height_; } -Image::Image(const uint8_t *data_start, int width, int height) - : width_(width), height_(height), data_start_(data_start) {} +ImageType Image::get_type() const { return this->type_; } +Image::Image(const uint8_t *data_start, int width, int height, ImageType type) + : width_(width), height_(height), type_(type), data_start_(data_start) {} -DisplayPage::DisplayPage(const display_writer_t &writer) : writer_(writer) {} +bool Animation::get_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return false; + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; + if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) + return false; + const uint32_t pos = x + y * width_8 + frame_index; + return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); +} +Color Animation::get_color_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_ + frame_index) * 3; + 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 Color::BLACK; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_ + frame_index); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); + 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), 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() { + this->current_frame_++; + if (this->current_frame_ >= animation_frame_count_) { + this->current_frame_ = 0; + } +} + +DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show_next() { this->next_->show(); } void DisplayPage::show_prev() { this->prev_->show(); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b12fad8c8a..c803180a2d 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -3,11 +3,17 @@ #include "esphome/core/component.h" #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 { @@ -63,9 +69,11 @@ enum class TextAlign { }; /// Turn the pixel OFF. -extern const uint8_t COLOR_OFF; +extern const Color COLOR_OFF; /// Turn the pixel ON. -extern const uint8_t COLOR_ON; +extern const Color COLOR_ON; + +enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2 }; enum DisplayRotation { DISPLAY_ROTATION_0_DEGREES = 0, @@ -78,20 +86,21 @@ class Font; class Image; class DisplayBuffer; class DisplayPage; +class DisplayOnPageChangeTrigger; using display_writer_t = std::function; #define LOG_DISPLAY(prefix, type, obj) \ - if (obj != nullptr) { \ + if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, prefix type); \ - ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, obj->rotation_); \ - ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, obj->get_width(), obj->get_height()); \ + ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \ + ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ } class DisplayBuffer { public: /// Fill the entire screen with the given color. - virtual void fill(int color); + virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. void clear(); @@ -100,29 +109,29 @@ class DisplayBuffer { /// Get the height of the image in pixels with rotation applied. int get_height(); /// Set a single pixel at the specified coordinates to the given color. - void draw_pixel_at(int x, int y, int color = COLOR_ON); + void draw_pixel_at(int x, int y, Color color = COLOR_ON); /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. - void line(int x1, int y1, int x2, int y2, int color = COLOR_ON); + void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. - void horizontal_line(int x, int y, int width, int color = COLOR_ON); + void horizontal_line(int x, int y, int width, Color color = COLOR_ON); /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. - void vertical_line(int x, int y, int height, int color = COLOR_ON); + void vertical_line(int x, int y, int height, Color color = COLOR_ON); /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at /// [x1+width,y1+height]. - void rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. - void filled_rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. - void circle(int center_x, int center_xy, int radius, int color = COLOR_ON); + void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. - void filled_circle(int center_x, int center_y, int radius, int color = COLOR_ON); + void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -133,7 +142,7 @@ class DisplayBuffer { * @param align The alignment of the text. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, TextAlign align, const char *text); + void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); /** Print `text` with the top left at [x,y] with `font`. * @@ -143,7 +152,7 @@ class DisplayBuffer { * @param color The color to draw the text with. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, const char *text); + void print(int x, int y, Font *font, Color color, const char *text); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -174,7 +183,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) + void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) __attribute__((format(printf, 7, 8))); /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. @@ -186,7 +195,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, const char *format, ...) __attribute__((format(printf, 6, 7))); + void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * @@ -220,7 +229,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -232,7 +241,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -259,8 +268,39 @@ class DisplayBuffer { __attribute__((format(strftime, 5, 0))); #endif - /// Draw the `image` with the top-left corner at [x,y] to the screen. - void image(int x, int y, Image *image); + /** Draw the `image` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + +#ifdef USE_GRAPH + /** Draw the `graph` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param graph The graph id to draw + * @param color_on The color to replace in binary images for the on bits. + */ + void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); + + /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param graph The graph id for which the legend applies to + * @param graph The graph id for which the legend applies to + * @param graph The graph id for which the legend applies to + * @param name_font The font used for the trace name + * @param value_font The font used for the trace value and units + * @param color_on The color of the border + */ + void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); +#endif // USE_GRAPH /** Get the text bounds of the given string. * @@ -286,13 +326,20 @@ class DisplayBuffer { void set_pages(std::vector pages); + const DisplayPage *get_active_page() const { return this->page_; } + + void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } + /// Internal method to set the display rotation with. void set_rotation(DisplayRotation rotation); - protected: - void vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg); + // Internal method to set display auto clearing. + void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } - virtual void draw_absolute_pixel_internal(int x, int y, int color) = 0; + protected: + void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); + + virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; virtual int get_height_internal() = 0; @@ -306,11 +353,14 @@ class DisplayBuffer { DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; optional writer_{}; DisplayPage *page_{nullptr}; + DisplayPage *previous_page_{nullptr}; + std::vector on_page_change_triggers_; + bool auto_clear_enabled_{true}; }; class DisplayPage { public: - DisplayPage(const display_writer_t &writer); + DisplayPage(display_writer_t writer); void show(); void show_next(); void show_prev(); @@ -326,10 +376,18 @@ class DisplayPage { DisplayPage *next_{nullptr}; }; +struct GlyphData { + const char *a_char; + const uint8_t *data; + int offset_x; + int offset_y; + int width; + int height; +}; + class Glyph { public: - Glyph(const char *a_char, const uint8_t *data_start, uint32_t offset, int offset_x, int offset_y, int width, - int height); + Glyph(const GlyphData *data) : glyph_data_(data) {} bool get_pixel(int x, int y) const; @@ -345,12 +403,7 @@ class Glyph { friend Font; friend DisplayBuffer; - const char *char_; - const uint8_t *data_; - int offset_x_; - int offset_y_; - int width_; - int height_; + const GlyphData *glyph_data_; }; class Font { @@ -361,7 +414,7 @@ class Font { * @param baseline The y-offset from the top of the text to the baseline. * @param bottom The y-offset from the top of the text to the bottom (i.e. height). */ - Font(std::vector &&glyphs, int baseline, int bottom); + Font(const GlyphData *data, int data_nr, int baseline, int bottom); int match_next_glyph(const char *str, int *match_length); @@ -377,20 +430,41 @@ class Font { class Image { public: - Image(const uint8_t *data_start, int width, int height); - bool get_pixel(int x, int y) const; + Image(const uint8_t *data_start, int width, int height, ImageType type); + virtual bool get_pixel(int x, int y) const; + virtual Color get_color_pixel(int x, int y) const; + virtual Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; + ImageType get_type() const; protected: int width_; int height_; + ImageType type_; const uint8_t *data_start_; }; +class Animation : public Image { + public: + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); + bool get_pixel(int x, int y) const override; + Color get_color_pixel(int x, int y) const override; + Color get_grayscale_pixel(int x, int y) const override; + + int get_animation_frame_count() const; + int get_current_frame() const; + void next_frame(); + + protected: + int current_frame_; + int animation_frame_count_; +}; + template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) + void play(Ts... x) override { auto *page = this->page_.value(x...); if (page != nullptr) { @@ -402,20 +476,44 @@ template class DisplayPageShowAction : public Action { template class DisplayPageShowNextAction : public Action { public: DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_next_page(); } - protected: DisplayBuffer *buffer_; }; template class DisplayPageShowPrevAction : public Action { public: DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_prev_page(); } - protected: DisplayBuffer *buffer_; }; +template class DisplayIsDisplayingPageCondition : public Condition { + public: + DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {} + + void set_page(DisplayPage *page) { this->page_ = page; } + bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + + protected: + DisplayBuffer *parent_; + DisplayPage *page_; +}; + +class DisplayOnPageChangeTrigger : public Trigger { + public: + explicit DisplayOnPageChangeTrigger(DisplayBuffer *parent) { parent->add_on_page_change_trigger(this); } + void process(DisplayPage *from, DisplayPage *to); + void set_from(DisplayPage *p) { this->from_ = p; } + void set_to(DisplayPage *p) { this->to_ = p; } + + protected: + DisplayPage *from_{nullptr}; + DisplayPage *to_{nullptr}; +}; + } // namespace display } // namespace esphome diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h new file mode 100644 index 0000000000..202de912de --- /dev/null +++ b/esphome/components/display/display_color_utils.h @@ -0,0 +1,110 @@ +#pragma once +#include "esphome/core/color.h" + +namespace esphome { +namespace display { +enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 }; +enum ColorBitness : uint8_t { COLOR_BITNESS_888 = 0, COLOR_BITNESS_565 = 1, COLOR_BITNESS_332 = 2 }; +inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 255) { return (max_value * i / scale); } + +class ColorUtil { + public: + static Color to_color(uint32_t colorcode, ColorOrder color_order, + ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) { + uint8_t first_color, second_color, third_color; + uint8_t first_bits = 0; + uint8_t second_bits = 0; + uint8_t third_bits = 0; + + switch (color_bitness) { + case COLOR_BITNESS_888: + first_bits = 8; + second_bits = 8; + third_bits = 8; + break; + case COLOR_BITNESS_565: + first_bits = 5; + second_bits = 6; + third_bits = 5; + break; + case COLOR_BITNESS_332: + first_bits = 3; + second_bits = 3; + third_bits = 2; + break; + } + + first_color = right_bit_aligned ? esp_scale(((colorcode >> (second_bits + third_bits)) & ((1 << first_bits) - 1)), + ((1 << first_bits) - 1)) + : esp_scale(((colorcode >> 16) & 0xFF), (1 << first_bits) - 1); + + second_color = right_bit_aligned + ? esp_scale(((colorcode >> third_bits) & ((1 << second_bits) - 1)), ((1 << second_bits) - 1)) + : esp_scale(((colorcode >> 8) & 0xFF), ((1 << second_bits) - 1)); + + third_color = (right_bit_aligned ? esp_scale(((colorcode >> 0) & ((1 << third_bits) - 1)), ((1 << third_bits) - 1)) + : esp_scale(((colorcode >> 0) & 0xFF), (1 << third_bits) - 1)); + + Color color_return; + + switch (color_order) { + case COLOR_ORDER_RGB: + color_return.r = first_color; + color_return.g = second_color; + color_return.b = third_color; + break; + case COLOR_ORDER_BGR: + color_return.b = first_color; + color_return.g = second_color; + color_return.r = third_color; + break; + case COLOR_ORDER_GRB: + color_return.g = first_color; + color_return.r = second_color; + color_return.b = third_color; + break; + } + return color_return; + } + static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) { + uint16_t red_color, green_color, blue_color; + + red_color = esp_scale8(color.red, ((1 << 3) - 1)); + green_color = esp_scale8(color.green, ((1 << 3) - 1)); + blue_color = esp_scale8(color.blue, (1 << 2) - 1); + + switch (color_order) { + case COLOR_ORDER_RGB: + return red_color << 5 | green_color << 2 | blue_color; + case COLOR_ORDER_BGR: + return blue_color << 6 | green_color << 3 | red_color; + case COLOR_ORDER_GRB: + return green_color << 5 | red_color << 2 | blue_color; + } + return 0; + } + static uint16_t color_to_565(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) { + uint16_t red_color, green_color, blue_color; + + red_color = esp_scale8(color.red, ((1 << 5) - 1)); + green_color = esp_scale8(color.green, ((1 << 6) - 1)); + blue_color = esp_scale8(color.blue, (1 << 5) - 1); + + switch (color_order) { + case COLOR_ORDER_RGB: + return red_color << 11 | green_color << 5 | blue_color; + case COLOR_ORDER_BGR: + return blue_color << 11 | green_color << 5 | red_color; + case COLOR_ORDER_GRB: + return green_color << 10 | red_color << 5 | blue_color; + } + return 0; + } + + static uint32_t color_to_grayscale4(Color color) { + uint32_t gs4 = esp_scale8(color.white, 15); + return gs4; + } +}; +} // namespace display +} // namespace esphome diff --git a/esphome/components/ds1307/__init__.py b/esphome/components/ds1307/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp new file mode 100644 index 0000000000..d249e9743a --- /dev/null +++ b/esphome/components/ds1307/ds1307.cpp @@ -0,0 +1,105 @@ +#include "ds1307.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://datasheets.maximintegrated.com/en/ds/DS1307.pdf + +namespace esphome { +namespace ds1307 { + +static const char *const TAG = "ds1307"; + +void DS1307Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up DS1307..."); + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void DS1307Component::update() { this->read_time(); } + +void DS1307Component::dump_config() { + ESP_LOGCONFIG(TAG, "DS1307:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with DS1307 failed!"); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } + +void DS1307Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (ds1307_.reg.ch) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), + .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), + .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), + .day_of_week = uint8_t(ds1307_.reg.weekday), + .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), + .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void DS1307Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + ds1307_.reg.year = (now.year - 2000) % 10; + ds1307_.reg.year_10 = (now.year - 2000) / 10 % 10; + ds1307_.reg.month = now.month % 10; + ds1307_.reg.month_10 = now.month / 10; + ds1307_.reg.day = now.day_of_month % 10; + ds1307_.reg.day_10 = now.day_of_month / 10; + ds1307_.reg.weekday = now.day_of_week; + ds1307_.reg.hour = now.hour % 10; + ds1307_.reg.hour_10 = now.hour / 10; + ds1307_.reg.minute = now.minute % 10; + ds1307_.reg.minute_10 = now.minute / 10; + ds1307_.reg.second = now.second % 10; + ds1307_.reg.second_10 = now.second / 10; + ds1307_.reg.ch = false; + + this->write_rtc_(); +} + +bool DS1307Component::read_rtc_() { + if (!this->read_bytes(0, this->ds1307_.raw, sizeof(this->ds1307_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds1307_.reg.hour_10, + ds1307_.reg.hour, ds1307_.reg.minute_10, ds1307_.reg.minute, ds1307_.reg.second_10, ds1307_.reg.second, + ds1307_.reg.year_10, ds1307_.reg.year, ds1307_.reg.month_10, ds1307_.reg.month, ds1307_.reg.day_10, + ds1307_.reg.day, ONOFF(ds1307_.reg.ch), ds1307_.reg.rs, ONOFF(ds1307_.reg.sqwe), ONOFF(ds1307_.reg.out)); + + return true; +} + +bool DS1307Component::write_rtc_() { + if (!this->write_bytes(0, this->ds1307_.raw, sizeof(this->ds1307_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds1307_.reg.hour_10, + ds1307_.reg.hour, ds1307_.reg.minute_10, ds1307_.reg.minute, ds1307_.reg.second_10, ds1307_.reg.second, + ds1307_.reg.year_10, ds1307_.reg.year, ds1307_.reg.month_10, ds1307_.reg.month, ds1307_.reg.day_10, + ds1307_.reg.day, ONOFF(ds1307_.reg.ch), ds1307_.reg.rs, ONOFF(ds1307_.reg.sqwe), ONOFF(ds1307_.reg.out)); + return true; +} +} // namespace ds1307 +} // namespace esphome diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h new file mode 100644 index 0000000000..2e9ac2275c --- /dev/null +++ b/esphome/components/ds1307/ds1307.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace ds1307 { + +class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union DS1307Reg { + struct { + uint8_t second : 4; + uint8_t second_10 : 3; + bool ch : 1; + + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t unused_1 : 1; + + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t unused_2 : 2; + + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t unused_4 : 2; + + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t unused_5 : 3; + + uint8_t year : 4; + uint8_t year_10 : 4; + + uint8_t rs : 2; + uint8_t unused_6 : 2; + bool sqwe : 1; + uint8_t unused_7 : 2; + bool out : 1; + } reg; + mutable uint8_t raw[sizeof(reg)]; + } ds1307_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace ds1307 +} // namespace esphome diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py new file mode 100644 index 0000000000..ddc2939038 --- /dev/null +++ b/esphome/components/ds1307/time.py @@ -0,0 +1,58 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import i2c, time +from esphome.const import CONF_ID + + +CODEOWNERS = ["@badbadc0ffee"] +DEPENDENCIES = ["i2c"] +ds1307_ns = cg.esphome_ns.namespace("ds1307") +DS1307Component = ds1307_ns.class_("DS1307Component", time.RealTimeClock, i2c.I2CDevice) +WriteAction = ds1307_ns.class_("WriteAction", automation.Action) +ReadAction = ds1307_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DS1307Component), + } +).extend(i2c.i2c_device_schema(0x68)) + + +@automation.register_action( + "ds1307.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DS1307Component), + } + ), +) +async def ds1307_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "ds1307.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DS1307Component), + } + ), +) +async def ds1307_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py new file mode 100644 index 0000000000..7a7681082e --- /dev/null +++ b/esphome/components/dsmr/__init__.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import uart +from esphome.const import ( + CONF_ID, + CONF_UART_ID, + CONF_RECEIVE_TIMEOUT, +) + +CODEOWNERS = ["@glmnet", "@zuidwijk"] + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_CRC_CHECK = "crc_check" +CONF_DECRYPTION_KEY = "decryption_key" +CONF_DSMR_ID = "dsmr_id" +CONF_GAS_MBUS_ID = "gas_mbus_id" +CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" +CONF_REQUEST_INTERVAL = "request_interval" +CONF_REQUEST_PIN = "request_pin" + +# 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_, + cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, + cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_REQUEST_INTERVAL, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_RECEIVE_TIMEOUT, default="200ms" + ): cv.positive_time_period_milliseconds, + } + ).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]) + cg.add(var.set_max_telegram_length(config[CONF_MAX_TELEGRAM_LENGTH])) + if CONF_DECRYPTION_KEY in config: + cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) + await cg.register_component(var, config) + + if CONF_REQUEST_PIN in config: + request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN]) + cg.add(var.set_request_pin(request_pin)) + cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) + cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) + + 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..7b339e5fe0 --- /dev/null +++ b/esphome/components/dsmr/dsmr.cpp @@ -0,0 +1,327 @@ +#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::setup() { + this->telegram_ = new char[this->max_telegram_len_]; // NOLINT + if (this->request_pin_ != nullptr) { + this->request_pin_->setup(); + } +} + +void Dsmr::loop() { + if (this->ready_to_request_data_()) { + if (this->decryption_key_.empty()) { + this->receive_telegram_(); + } else { + this->receive_encrypted_telegram_(); + } + } +} + +bool Dsmr::ready_to_request_data_() { + // When using a request pin, then wait for the next request interval. + if (this->request_pin_ != nullptr) { + if (!this->requesting_data_ && this->request_interval_reached_()) { + this->start_requesting_data_(); + } + } + // Otherwise, sink serial data until next request interval. + else { + if (this->request_interval_reached_()) { + this->start_requesting_data_(); + } + if (!this->requesting_data_) { + while (this->available()) { + this->read(); + } + } + } + return this->requesting_data_; +} + +bool Dsmr::request_interval_reached_() { + if (this->last_request_time_ == 0) { + return true; + } + return millis() - this->last_request_time_ > this->request_interval_; +} + +bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; } + +bool Dsmr::available_within_timeout_() { + // Data are available for reading on the UART bus? + // Then we can start reading right away. + if (this->available()) { + this->last_read_time_ = millis(); + return true; + } + // When we're not in the process of reading a telegram, then there is + // no need to actively wait for new data to come in. + if (!header_found_) { + return false; + } + // A telegram is being read. The smart meter might not deliver a telegram + // in one go, but instead send it in chunks with small pauses in between. + // When the UART RX buffer cannot hold a full telegram, then make sure + // that the UART read buffer does not overflow while other components + // perform their work in their loop. Do this by not returning control to + // the main loop, until the read timeout is reached. + if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) { + while (!this->receive_timeout_reached_()) { + delay(5); + if (this->available()) { + this->last_read_time_ = millis(); + return true; + } + } + } + // No new data has come in during the read timeout? Then stop reading the + // telegram and start waiting for the next one to arrive. + if (this->receive_timeout_reached_()) { + ESP_LOGW(TAG, "Timeout while reading data for telegram"); + this->reset_telegram_(); + } + + return false; +} + +void Dsmr::start_requesting_data_() { + if (!this->requesting_data_) { + if (this->request_pin_ != nullptr) { + ESP_LOGV(TAG, "Start requesting data from P1 port"); + this->request_pin_->digital_write(true); + } else { + ESP_LOGV(TAG, "Start reading data from P1 port"); + } + this->requesting_data_ = true; + this->last_request_time_ = millis(); + } +} + +void Dsmr::stop_requesting_data_() { + if (this->requesting_data_) { + if (this->request_pin_ != nullptr) { + ESP_LOGV(TAG, "Stop requesting data from P1 port"); + this->request_pin_->digital_write(false); + } else { + ESP_LOGV(TAG, "Stop reading data from P1 port"); + } + while (this->available()) { + this->read(); + } + this->requesting_data_ = false; + } +} + +void Dsmr::reset_telegram_() { + this->header_found_ = false; + this->footer_found_ = false; + this->bytes_read_ = 0; + this->crypt_bytes_read_ = 0; + this->crypt_telegram_len_ = 0; + this->last_read_time_ = 0; +} + +void Dsmr::receive_telegram_() { + while (this->available_within_timeout_()) { + const char c = this->read(); + + // Find a new telegram header, i.e. forward slash. + if (c == '/') { + ESP_LOGV(TAG, "Header of telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + if (!this->header_found_) + continue; + + // Check for buffer overflow. + if (this->bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); + 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. + if (c == '(') { + while (true) { + auto previous_char = this->telegram_[this->bytes_read_ - 1]; + if (previous_char == '\n' || previous_char == '\r') { + this->bytes_read_--; + } else { + break; + } + } + } + + // Store the byte in the buffer. + this->telegram_[this->bytes_read_] = c; + this->bytes_read_++; + + // Check for a footer, i.e. exlamation mark, followed by a hex checksum. + if (c == '!') { + ESP_LOGV(TAG, "Footer of telegram found"); + this->footer_found_ = true; + continue; + } + // Check for the end of the hex checksum, i.e. a newline. + if (this->footer_found_ && c == '\n') { + // Parse the telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; + } + } +} + +void Dsmr::receive_encrypted_telegram_() { + while (this->available_within_timeout_()) { + const char c = this->read(); + + // Find a new telegram start byte. + if (!this->header_found_) { + if ((uint8_t) c != 0xDB) { + continue; + } + ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + + // Check for buffer overflow. + if (this->crypt_bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Store the byte in the buffer. + this->crypt_telegram_[this->crypt_bytes_read_] = c; + this->crypt_bytes_read_++; + + // Read the length of the incoming encrypted telegram. + if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { + // Complete header + data bytes + this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); + ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); + } + + // Check for the end of the encrypted telegram. + if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { + continue; + } + ESP_LOGV(TAG, "End of encrypted telegram found"); + + // Decrypt the encrypted telegram. + 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++) + this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &this->crypt_telegram_[18], + // cipher size + this->crypt_bytes_read_ - 17); + delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) + + this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); + + // Parse the decrypted telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; + } +} + +bool Dsmr::parse_telegram() { + MyData data; + ESP_LOGV(TAG, "Trying to parse telegram"); + this->stop_requesting_data_(); + ::dsmr::ParseResult res = + ::dsmr::P1Parser::parse(&data, this->telegram_, this->bytes_read_, false, + this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. + if (res.err) { + // Parsing error, show it + auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_); + ESP_LOGE(TAG, "%s", err_str.c_str()); + return false; + } else { + this->status_clear_warning(); + this->publish_sensors(data); + return true; + } +} + +void Dsmr::dump_config() { + ESP_LOGCONFIG(TAG, "DSMR:"); + ESP_LOGCONFIG(TAG, " Max telegram length: %d", this->max_telegram_len_); + ESP_LOGCONFIG(TAG, " Receive timeout: %.1fs", this->receive_timeout_ / 1e3f); + if (this->request_pin_ != nullptr) { + LOG_PIN(" Request Pin: ", this->request_pin_); + } + if (this->request_interval_ > 0) { + ESP_LOGCONFIG(TAG, " Request Interval: %.1fs", this->request_interval_ / 1e3f); + } + +#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(); + if (this->crypt_telegram_ != nullptr) { + delete[] this->crypt_telegram_; + this->crypt_telegram_ = nullptr; + } + 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); + this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); + } + + if (this->crypt_telegram_ == nullptr) { + this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT + } +} + +} // 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..76f79ee55c --- /dev/null +++ b/esphome/components/dsmr/dsmr.h @@ -0,0 +1,138 @@ +#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 { + +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 setup() override; + 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); + void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } + void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } + void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } + void set_receive_timeout(uint32_t timeout) { this->receive_timeout_ = timeout; } + +// 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_(); + void reset_telegram_(); + + /// Wait for UART data to become available within the read timeout. + /// + /// The smart meter might provide data in chunks, causing available() to + /// return 0. When we're already reading a telegram, then we don't return + /// right away (to handle further data in an upcoming loop) but wait a + /// little while using this method to see if more data are incoming. + /// By not returning, we prevent other components from taking so much + /// time that the UART RX buffer overflows and bytes of the telegram get + /// lost in the process. + bool available_within_timeout_(); + + // Request telegram + uint32_t request_interval_; + bool request_interval_reached_(); + GPIOPin *request_pin_{nullptr}; + uint32_t last_request_time_{0}; + bool requesting_data_{false}; + bool ready_to_request_data_(); + void start_requesting_data_(); + void stop_requesting_data_(); + + // Read telegram + uint32_t receive_timeout_; + bool receive_timeout_reached_(); + size_t max_telegram_len_; + char *telegram_{nullptr}; + size_t bytes_read_{0}; + uint8_t *crypt_telegram_{nullptr}; + size_t crypt_telegram_len_{0}; + size_t crypt_bytes_read_{0}; + uint32_t last_read_time_{0}; + 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..d809d0d105 --- /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_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional("total_exported_energy"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + 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_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + 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 36147d0b63..9a881c81f0 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -5,16 +5,16 @@ namespace esphome { namespace duty_cycle { -static const char *TAG = "duty_cycle"; +static const char *const TAG = "duty_cycle"; void DutyCycleSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up Duty Cycle Sensor '%s'...", this->get_name().c_str()); this->pin_->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); @@ -23,28 +23,31 @@ void DutyCycleSensor::dump_config() { } void DutyCycleSensor::update() { const uint32_t now = micros(); - const bool level = this->store_.last_level; - const uint32_t last_interrupt = this->store_.last_interrupt; + const uint32_t last_interrupt = this->store_.last_interrupt; // Read the measurement taken by the interrupt uint32_t on_time = this->store_.on_time; - if (level) - on_time += now - last_interrupt; - - const float total_time = float(now - this->last_update_); - - const float value = (on_time / total_time) * 100.0f; - ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); - this->publish_state(value); - - this->store_.on_time = 0; + this->store_.on_time = 0; // Start new measurement, exactly aligned with the micros() reading this->store_.last_interrupt = now; + + if (this->last_update_ != 0) { + const bool level = this->store_.last_level; + + if (level) + on_time += now - last_interrupt; + + const float total_time = float(now - this->last_update_); + + const float value = (on_time * 100.0f) / total_time; + ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); + this->publish_state(value); + } this->last_update_ = now; } 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..ffb1802e14 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,10 +27,10 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { void update() override; protected: - GPIOPin *pin_; + InternalGPIOPin *pin_; - DutyCycleSensorStore store_; - uint32_t last_update_; + DutyCycleSensorStore store_{}; + uint32_t last_update_{0}; }; } // namespace duty_cycle diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py index 51d99aae6a..6a367328e6 100644 --- a/esphome/components/duty_cycle/sensor.py +++ b/esphome/components/duty_cycle/sensor.py @@ -2,22 +2,40 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_ID, CONF_PIN, UNIT_PERCENT, ICON_PERCENT +from esphome.const import ( + CONF_ID, + CONF_PIN, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + ICON_PERCENT, +) -duty_cycle_ns = cg.esphome_ns.namespace('duty_cycle') -DutyCycleSensor = duty_cycle_ns.class_('DutyCycleSensor', sensor.Sensor, cg.PollingComponent) +duty_cycle_ns = cg.esphome_ns.namespace("duty_cycle") +DutyCycleSensor = duty_cycle_ns.class_( + "DutyCycleSensor", sensor.Sensor, cg.PollingComponent +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PERCENT, ICON_PERCENT, 1).extend({ - cv.GenerateID(): cv.declare_id(DutyCycleSensor), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, - pins.validate_has_interrupt), -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + 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), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py new file mode 100644 index 0000000000..bb662e0989 --- /dev/null +++ b/esphome/components/e131/__init__.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +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 +) +E131Component = e131_ns.class_("E131Component", cg.Component) + +METHODS = {"UNICAST": e131_ns.E131_UNICAST, "MULTICAST": e131_ns.E131_MULTICAST} + +CHANNELS = { + "MONO": e131_ns.E131_MONO, + "RGB": e131_ns.E131_RGB, + "RGBW": e131_ns.E131_RGBW, +} + +CONF_UNIVERSE = "universe" +CONF_E131_ID = "e131_id" + +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, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_method(METHODS[config[CONF_METHOD]])) + + +@register_addressable_effect( + "e131", + E131AddressableLightEffect, + "E1.31", + { + cv.GenerateID(CONF_E131_ID): cv.use_id(E131Component), + cv.Required(CONF_UNIVERSE): cv.int_range(min=1, max=512), + cv.Optional(CONF_CHANNELS, default="RGB"): cv.one_of(*CHANNELS, upper=True), + }, +) +async def e131_light_effect_to_code(config, effect_id): + parent = await cg.get_variable(config[CONF_E131_ID]) + + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_first_universe(config[CONF_UNIVERSE])) + cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]])) + cg.add(effect.set_e131(parent)) + return effect diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp new file mode 100644 index 0000000000..35510fe204 --- /dev/null +++ b/esphome/components/e131/e131.cpp @@ -0,0 +1,110 @@ +#ifdef USE_ARDUINO + +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 +#include +#endif + +#ifdef USE_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace e131 { + +static const char *const TAG = "e131"; +static const int PORT = 5568; + +E131Component::E131Component() {} + +E131Component::~E131Component() { + if (udp_) { + udp_->stop(); + } +} + +void E131Component::setup() { + udp_ = make_unique(); + + if (!udp_->begin(PORT)) { + ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); + mark_failed(); + return; + } + + join_igmp_groups_(); +} + +void E131Component::loop() { + std::vector payload; + E131Packet packet; + int universe = 0; + + while (uint16_t packet_size = udp_->parsePacket()) { + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!packet_(payload, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); + continue; + } + + if (!process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +} + +void E131Component::add_effect(E131AddressableLightEffect *light_effect) { + if (light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.insert(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + join_(universe); + } +} + +void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { + if (!light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.erase(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + leave_(universe); + } +} + +bool E131Component::process_(int universe, const E131Packet &packet) { + bool handled = false; + + ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count); + + for (auto light_effect : light_effects_) { + handled = light_effect->process_(universe, packet) || handled; + } + + return handled; +} + +} // namespace e131 +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h new file mode 100644 index 0000000000..3819e522a5 --- /dev/null +++ b/esphome/components/e131/e131.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" + +#include +#include +#include + +class UDP; + +namespace esphome { +namespace e131 { + +class E131AddressableLightEffect; + +enum E131ListenMethod { E131_MULTICAST, E131_UNICAST }; + +const int E131_MAX_PROPERTY_VALUES_COUNT = 513; + +struct E131Packet { + uint16_t count; + uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; +}; + +class E131Component : public esphome::Component { + public: + E131Component(); + ~E131Component(); + + void setup() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + public: + void add_effect(E131AddressableLightEffect *light_effect); + void remove_effect(E131AddressableLightEffect *light_effect); + + public: + void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } + + protected: + bool packet_(const std::vector &data, int &universe, E131Packet &packet); + bool process_(int universe, const E131Packet &packet); + bool join_igmp_groups_(); + void join_(int universe); + void leave_(int universe); + + protected: + E131ListenMethod listen_method_{E131_MULTICAST}; + std::unique_ptr udp_; + std::set light_effects_; + std::map universe_consumers_; + std::map universe_packets_; +}; + +} // 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 new file mode 100644 index 0000000000..371f3b9cbf --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -0,0 +1,96 @@ +#ifdef USE_ARDUINO + +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace e131 { + +static const char *const TAG = "e131_addressable_light_effect"; +static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); + +E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } + +int E131AddressableLightEffect::get_lights_per_universe() const { return MAX_DATA_SIZE / channels_; } + +int E131AddressableLightEffect::get_first_universe() const { return first_universe_; } + +int E131AddressableLightEffect::get_last_universe() const { return first_universe_ + get_universe_count() - 1; } + +int E131AddressableLightEffect::get_universe_count() const { + // Round up to lights_per_universe + auto lights = get_lights_per_universe(); + return (get_addressable_()->size() + lights - 1) / lights; +} + +void E131AddressableLightEffect::start() { + AddressableLightEffect::start(); + + if (this->e131_) { + this->e131_->add_effect(this); + } +} + +void E131AddressableLightEffect::stop() { + if (this->e131_) { + this->e131_->remove_effect(this); + } + + AddressableLightEffect::stop(); +} + +void E131AddressableLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { + // ignore, it is run by `E131Component::update()` +} + +bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet) { + auto it = get_addressable_(); + + // check if this is our universe and data are valid + if (universe < first_universe_ || universe > get_last_universe()) + return false; + + int output_offset = (universe - first_universe_) * get_lights_per_universe(); + // limit amount of lights per universe and received + int output_end = + std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); + auto input_data = packet.values + 1; + + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset, + output_end); + + switch (channels_) { + case E131_MONO: + for (; output_offset < output_end; output_offset++, input_data++) { + auto output = (*it)[output_offset]; + output.set(Color(input_data[0], input_data[0], input_data[0], input_data[0])); + } + break; + + case E131_RGB: + for (; output_offset < output_end; output_offset++, input_data += 3) { + auto output = (*it)[output_offset]; + output.set( + Color(input_data[0], input_data[1], input_data[2], (input_data[0] + input_data[1] + input_data[2]) / 3)); + } + break; + + case E131_RGBW: + for (; output_offset < output_end; output_offset++, input_data += 4) { + auto output = (*it)[output_offset]; + output.set(Color(input_data[0], input_data[1], input_data[2], input_data[3])); + } + 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 new file mode 100644 index 0000000000..e78f6bb0e0 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -0,0 +1,52 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +namespace esphome { +namespace e131 { + +class E131Component; +struct E131Packet; + +enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; + +class E131AddressableLightEffect : public light::AddressableLightEffect { + public: + E131AddressableLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const Color ¤t_color) override; + + public: + int get_data_per_universe() const; + int get_lights_per_universe() const; + int get_first_universe() const; + int get_last_universe() const; + int get_universe_count() const; + + public: + void set_first_universe(int universe) { this->first_universe_ = universe; } + void set_channels(E131LightChannels channels) { this->channels_ = channels; } + void set_e131(E131Component *e131) { this->e131_ = e131; } + + protected: + bool process_(int universe, const E131Packet &packet); + + protected: + int first_universe_{0}; + int last_universe_{0}; + E131LightChannels channels_{E131_RGB}; + E131Component *e131_{nullptr}; + + friend class E131Component; +}; + +} // namespace e131 +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp new file mode 100644 index 0000000000..b20eb9f666 --- /dev/null +++ b/esphome/components/e131/e131_packet.cpp @@ -0,0 +1,144 @@ +#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 { +namespace e131 { + +static const char *const TAG = "e131"; + +static const uint8_t ACN_ID[12] = {0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00}; +static const uint32_t VECTOR_ROOT = 4; +static const uint32_t VECTOR_FRAME = 2; +static const uint8_t VECTOR_DMP = 2; + +// E1.31 Packet Structure +union E131RawPacket { + struct { + // Root Layer + uint16_t preamble_size; + uint16_t postamble_size; + uint8_t acn_id[12]; + uint16_t root_flength; + uint32_t root_vector; + uint8_t cid[16]; + + // Frame Layer + uint16_t frame_flength; + uint32_t frame_vector; + uint8_t source_name[64]; + uint8_t priority; + uint16_t reserved; + uint8_t sequence_number; + uint8_t options; + uint16_t universe; + + // DMP Layer + uint16_t dmp_flength; + uint8_t dmp_vector; + uint8_t type; + uint16_t first_address; + uint16_t address_increment; + uint16_t property_value_count; + uint8_t property_values[E131_MAX_PROPERTY_VALUES_COUNT]; + } __attribute__((packed)); + + uint8_t raw[638]; +}; + +// We need to have at least one `1` value +// Get the offset of `property_values[1]` +const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); + +bool E131Component::join_igmp_groups_() { + if (listen_method_ != E131_MULTICAST) + return false; + if (!udp_) + return false; + + for (auto universe : universe_consumers_) { + if (!universe.second) + continue; + + 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); + + if (err) { + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + } + } + + return true; +} + +void E131Component::join_(int universe) { + // store only latest received packet for the given universe + auto consumers = ++universe_consumers_[universe]; + + if (consumers > 1) { + return; // we already joined before + } + + if (join_igmp_groups_()) { + ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); + } +} + +void E131Component::leave_(int universe) { + auto consumers = --universe_consumers_[universe]; + + if (consumers > 0) { + return; // we have other consumers of the given universe + } + + if (listen_method_ == E131_MULTICAST) { + ip4_addr_t multicast_addr = { + static_cast(network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; + + igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); + } + + ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); +} + +bool E131Component::packet_(const std::vector &data, int &universe, E131Packet &packet) { + if (data.size() < E131_MIN_PACKET_SIZE) + return false; + + auto sbuff = reinterpret_cast(&data[0]); + + if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) + return false; + if (htonl(sbuff->root_vector) != VECTOR_ROOT) + return false; + if (htonl(sbuff->frame_vector) != VECTOR_FRAME) + return false; + if (sbuff->dmp_vector != VECTOR_DMP) + return false; + if (sbuff->property_values[0] != 0) + return false; + + universe = htons(sbuff->universe); + packet.count = htons(sbuff->property_value_count); + if (packet.count > E131_MAX_PROPERTY_VALUES_COUNT) + return false; + + memcpy(packet.values, sbuff->property_values, packet.count); + return true; +} + +} // namespace e131 +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/endstop/cover.py b/esphome/components/endstop/cover.py index 0d65cc1078..9f3cd395a5 100644 --- a/esphome/components/endstop/cover.py +++ b/esphome/components/endstop/cover.py @@ -2,45 +2,58 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor, cover -from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, \ - CONF_CLOSE_ENDSTOP, CONF_ID, CONF_OPEN_ACTION, CONF_OPEN_DURATION, \ - CONF_OPEN_ENDSTOP, CONF_STOP_ACTION, CONF_MAX_DURATION +from esphome.const import ( + CONF_CLOSE_ACTION, + CONF_CLOSE_DURATION, + CONF_CLOSE_ENDSTOP, + CONF_ID, + CONF_OPEN_ACTION, + CONF_OPEN_DURATION, + CONF_OPEN_ENDSTOP, + CONF_STOP_ACTION, + CONF_MAX_DURATION, +) -endstop_ns = cg.esphome_ns.namespace('endstop') -EndstopCover = endstop_ns.class_('EndstopCover', cover.Cover, cg.Component) +endstop_ns = cg.esphome_ns.namespace("endstop") +EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component) -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(EndstopCover), - cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), - - cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), - cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, - - cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), - cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, - - cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EndstopCover), + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield cover.register_cover(var, config) + await cg.register_component(var, config) + await cover.register_cover(var, config) - yield automation.build_automation(var.get_stop_trigger(), [], config[CONF_STOP_ACTION]) + await automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) - bin = yield cg.get_variable(config[CONF_OPEN_ENDSTOP]) + bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP]) cg.add(var.set_open_endstop(bin)) cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) - yield automation.build_automation(var.get_open_trigger(), [], config[CONF_OPEN_ACTION]) + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) - bin = yield cg.get_variable(config[CONF_CLOSE_ENDSTOP]) + bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP]) cg.add(var.set_close_endstop(bin)) cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) - yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) + await automation.build_automation( + var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] + ) if CONF_MAX_DURATION in config: cg.add(var.set_max_duration(config[CONF_MAX_DURATION])) diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 1c239226c1..67c6a4ebd3 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -1,10 +1,11 @@ #include "endstop_cover.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace endstop { -static const char *TAG = "endstop.cover"; +static const char *const TAG = "endstop.cover"; using namespace esphome::cover; @@ -94,7 +95,7 @@ void EndstopCover::dump_config() { float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py new file mode 100644 index 0000000000..d6f1180aa7 --- /dev/null +++ b/esphome/components/esp32/__init__.py @@ -0,0 +1,414 @@ +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_SOURCE, + 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 ( # noqa + KEY_BOARD, + KEY_ESP32, + KEY_SDKCONFIG_OPTIONS, + KEY_VARIANT, + VARIANT_ESP32C3, + VARIANT_FRIENDLY, + VARIANTS, +) +from .boards import BOARD_TO_VARIANT + +# 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] + ) + 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" + + +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" + + +# 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": (cv.Version(2, 0, 0), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(1, 0, 6), None), + "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), + } + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] + else: + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) + + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) + + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) + ) + + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected Arduino framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." + ) + + return value + + +def _esp_idf_check_versions(value): + value = value.copy() + lookups = { + "dev": (cv.Version(4, 3, 1), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(4, 3, 0), None), + "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), + } + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] + else: + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) + + if version < cv.Version(4, 0, 0): + raise cv.Invalid("Only ESP-IDF 4.0+ is supported.") + + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_espidf_version(version) + + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) + ) + + if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected ESP-IDF framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." + ) + + return value + + +def _parse_platform_version(value): + try: + # if platform version is a valid version constraint, prefix the default package + cv.platformio_version_constraint(value) + return f"platformio/espressif32 @ {value}" + except cv.Invalid: + return value + + +def _detect_variant(value): + if CONF_VARIANT not in value: + board = value[CONF_BOARD] + if board not in BOARD_TO_VARIANT: + raise cv.Invalid( + "This board is unknown, please set the variant manually", + path=[CONF_BOARD], + ) + + value = value.copy() + value[CONF_VARIANT] = BOARD_TO_VARIANT[board] + + return value + + +CONF_PLATFORM_VERSION = "platform_version" + +ARDUINO_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + } + ), + _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_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: 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): cv.one_of(*VARIANTS, upper=True), + cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, + } + ), + _detect_variant, + 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_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) + + cg.add_platformio_option("lib_ldf_mode", "off") + + conf = config[CONF_FRAMEWORK] + cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + 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_SOURCE]}"], + ) + 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) + + # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms + add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) + + # Setup watchdog + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + + 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("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_SOURCE]}"], + ) + + 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..7f7bb2259f --- /dev/null +++ b/esphome/components/esp32/boards.py @@ -0,0 +1,1051 @@ +from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3 + +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}, +} + +""" +BOARD_TO_VARIANT generated with: + +git clone https://github.com/platformio/platform-espressif32 +for x in platform-espressif32/boards/*.json; do + mcu=$(jq -r .build.mcu <"$x"); + fname=$(basename "$x") + board="${fname%.*}" + variant=$(echo "$mcu" | tr '[:lower:]' '[:upper:]') + echo " \"$board\": VARIANT_${variant}," +done | sort +""" + +BOARD_TO_VARIANT = { + "alksesp32": VARIANT_ESP32, + "az-delivery-devkit-v4": VARIANT_ESP32, + "bpi-bit": VARIANT_ESP32, + "briki_abc_esp32": VARIANT_ESP32, + "briki_mbc-wb_esp32": VARIANT_ESP32, + "d-duino-32": VARIANT_ESP32, + "esp320": VARIANT_ESP32, + "esp32-c3-devkitm-1": VARIANT_ESP32C3, + "esp32cam": VARIANT_ESP32, + "esp32-devkitlipo": VARIANT_ESP32, + "esp32dev": VARIANT_ESP32, + "esp32doit-devkit-v1": VARIANT_ESP32, + "esp32doit-espduino": VARIANT_ESP32, + "esp32-evb": VARIANT_ESP32, + "esp32-gateway": VARIANT_ESP32, + "esp32-poe-iso": VARIANT_ESP32, + "esp32-poe": VARIANT_ESP32, + "esp32-pro": VARIANT_ESP32, + "esp32-s2-kaluga-1": VARIANT_ESP32S2, + "esp32-s2-saola-1": VARIANT_ESP32S2, + "esp32thing_plus": VARIANT_ESP32, + "esp32thing": VARIANT_ESP32, + "esp32vn-iot-uno": VARIANT_ESP32, + "espea32": VARIANT_ESP32, + "espectro32": VARIANT_ESP32, + "espino32": VARIANT_ESP32, + "esp-wrover-kit": VARIANT_ESP32, + "etboard": VARIANT_ESP32, + "featheresp32-s2": VARIANT_ESP32S2, + "featheresp32": VARIANT_ESP32, + "firebeetle32": VARIANT_ESP32, + "fm-devkit": VARIANT_ESP32, + "frogboard": VARIANT_ESP32, + "healthypi4": VARIANT_ESP32, + "heltec_wifi_kit_32_v2": VARIANT_ESP32, + "heltec_wifi_kit_32": VARIANT_ESP32, + "heltec_wifi_lora_32_V2": VARIANT_ESP32, + "heltec_wifi_lora_32": VARIANT_ESP32, + "heltec_wireless_stick_lite": VARIANT_ESP32, + "heltec_wireless_stick": VARIANT_ESP32, + "honeylemon": VARIANT_ESP32, + "hornbill32dev": VARIANT_ESP32, + "hornbill32minima": VARIANT_ESP32, + "imbrios-logsens-v1p1": VARIANT_ESP32, + "inex_openkb": VARIANT_ESP32, + "intorobot": VARIANT_ESP32, + "iotaap_magnolia": VARIANT_ESP32, + "iotbusio": VARIANT_ESP32, + "iotbusproteus": VARIANT_ESP32, + "kits-edu": VARIANT_ESP32, + "labplus_mpython": VARIANT_ESP32, + "lolin32_lite": VARIANT_ESP32, + "lolin32": VARIANT_ESP32, + "lolin_d32_pro": VARIANT_ESP32, + "lolin_d32": VARIANT_ESP32, + "lopy4": VARIANT_ESP32, + "lopy": VARIANT_ESP32, + "m5stack-atom": VARIANT_ESP32, + "m5stack-core2": VARIANT_ESP32, + "m5stack-core-esp32": VARIANT_ESP32, + "m5stack-coreink": VARIANT_ESP32, + "m5stack-fire": VARIANT_ESP32, + "m5stack-grey": VARIANT_ESP32, + "m5stack-timer-cam": VARIANT_ESP32, + "m5stick-c": VARIANT_ESP32, + "magicbit": VARIANT_ESP32, + "mgbot-iotik32a": VARIANT_ESP32, + "mgbot-iotik32b": VARIANT_ESP32, + "mhetesp32devkit": VARIANT_ESP32, + "mhetesp32minikit": VARIANT_ESP32, + "microduino-core-esp32": VARIANT_ESP32, + "nano32": VARIANT_ESP32, + "nina_w10": VARIANT_ESP32, + "node32s": VARIANT_ESP32, + "nodemcu-32s": VARIANT_ESP32, + "nscreen-32": VARIANT_ESP32, + "odroid_esp32": VARIANT_ESP32, + "onehorse32dev": VARIANT_ESP32, + "oroca_edubot": VARIANT_ESP32, + "pico32": VARIANT_ESP32, + "piranha_esp32": VARIANT_ESP32, + "pocket_32": VARIANT_ESP32, + "pycom_gpy": VARIANT_ESP32, + "qchip": VARIANT_ESP32, + "quantum": VARIANT_ESP32, + "sensesiot_weizen": VARIANT_ESP32, + "sg-o_airMon": VARIANT_ESP32, + "s_odi_ultra": VARIANT_ESP32, + "sparkfun_lora_gateway_1-channel": VARIANT_ESP32, + "tinypico": VARIANT_ESP32, + "ttgo-lora32-v1": VARIANT_ESP32, + "ttgo-lora32-v21": VARIANT_ESP32, + "ttgo-lora32-v2": VARIANT_ESP32, + "ttgo-t1": VARIANT_ESP32, + "ttgo-t7-v13-mini32": VARIANT_ESP32, + "ttgo-t7-v14-mini32": VARIANT_ESP32, + "ttgo-t-beam": VARIANT_ESP32, + "ttgo-t-watch": VARIANT_ESP32, + "turta_iot_node": VARIANT_ESP32, + "vintlabs-devkit-v1": VARIANT_ESP32, + "wemosbat": VARIANT_ESP32, + "wemos_d1_mini32": VARIANT_ESP32, + "wesp32": VARIANT_ESP32, + "widora-air": VARIANT_ESP32, + "wifiduino32": VARIANT_ESP32, + "xinabox_cw02": VARIANT_ESP32, +} diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py new file mode 100644 index 0000000000..d92b449ee9 --- /dev/null +++ b/esphome/components/esp32/const.py @@ -0,0 +1,29 @@ +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, +] + +VARIANT_FRIENDLY = { + VARIANT_ESP32: "ESP32", + VARIANT_ESP32S2: "ESP32-S2", + VARIANT_ESP32S3: "ESP32-S3", + VARIANT_ESP32C3: "ESP32-C3", + VARIANT_ESP32H2: "ESP32-H2", +} + +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..6123d83a34 --- /dev/null +++ b/esphome/components/esp32/core.cpp @@ -0,0 +1,90 @@ +#ifdef USE_ESP32 + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" +#include +#include +#include +#include +#include + +#if ESP_IDF_VERSION_MAJOR >= 4 +#include +#endif + +#ifdef USE_ARDUINO +#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) { delay_microseconds_safe(us); } +void arch_restart() { + esp_restart(); + // restart() doesn't always end execution + while (true) { // NOLINT(clang-diagnostic-unreachable-code) + yield(); + } +} + +void arch_init() { + // Enable the task watchdog only on the loop task (from which we're currently running) +#if defined(USE_ESP_IDF) + esp_task_wdt_add(nullptr); + // Idle task watchdog is disabled on ESP-IDF +#elif defined(USE_ARDUINO) + enableLoopWDT(); + // Disable idle task watchdog on the core we're using (Arduino pins the task to a core) +#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0) && CONFIG_ARDUINO_RUNNING_CORE == 0 + disableCore0WDT(); +#endif +#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1) && CONFIG_ARDUINO_RUNNING_CORE == 1 + disableCore1WDT(); +#endif +#endif +} +void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } + +uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint32_t arch_get_cpu_cycle_count() { +#if ESP_IDF_VERSION_MAJOR >= 4 + return cpu_hal_get_cycle_count(); +#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..5819943f37 --- /dev/null +++ b/esphome/components/esp32/gpio.py @@ -0,0 +1,211 @@ +from dataclasses import dataclass +from typing import Any + +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, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32H2, + esp32_ns, +) + + +from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports +from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports +from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports +from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports +from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports + + +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) + + +@dataclass +class ESP32ValidationFunctions: + pin_validation: Any + usage_validation: Any + + +_esp32_validations = { + VARIANT_ESP32: ESP32ValidationFunctions( + pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports + ), + VARIANT_ESP32S2: ESP32ValidationFunctions( + pin_validation=esp32_s2_validate_gpio_pin, + usage_validation=esp32_s2_validate_supports, + ), + VARIANT_ESP32C3: ESP32ValidationFunctions( + pin_validation=esp32_c3_validate_gpio_pin, + usage_validation=esp32_c3_validate_supports, + ), + VARIANT_ESP32S3: ESP32ValidationFunctions( + pin_validation=esp32_s3_validate_gpio_pin, + usage_validation=esp32_s3_validate_supports, + ), + VARIANT_ESP32H2: ESP32ValidationFunctions( + pin_validation=esp32_h2_validate_gpio_pin, + usage_validation=esp32_h2_validate_supports, + ), +} + + +def validate_gpio_pin(value): + value = _translate_pin(value) + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in _esp32_validations: + raise cv.Invalid("Unsupported ESP32 variant {variant}") + + return _esp32_validations[variant].pin_validation(value) + + +def validate_supports(value): + 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] + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in _esp32_validations: + raise cv.Invalid("Unsupported ESP32 variant {variant}") + + if is_open_drain and not is_output: + raise cv.Invalid( + "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] + ) + + value = _esp32_validations[variant].usage_validation(value) + 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..ba92894f97 --- /dev/null +++ b/esphome/components/esp32/gpio_arduino.cpp @@ -0,0 +1,114 @@ +#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"; + +static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } else { + return 0; + } +} + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + 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) { + pinMode(pin_, flags_to_mode(flags)); // 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 +} +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + pinMode(arg->pin, flags_to_mode(flags)); // NOLINT +} + +} // namespace esphome + +#endif // USE_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_esp32.py b/esphome/components/esp32/gpio_esp32.py new file mode 100644 index 0000000000..dbafb73dba --- /dev/null +++ b/esphome/components/esp32/gpio_esp32.py @@ -0,0 +1,77 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +import esphome.config_validation as cv + + +_ESP_SDIO_PINS = { + 6: "Flash Clock", + 7: "Flash Data 0", + 8: "Flash Data 1", + 11: "Flash Command", +} + +_ESP32_STRAPPING_PINS = {0, 2, 4, 12, 15} +_LOGGER = logging.getLogger(__name__) + + +def esp32_validate_gpio_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 _ESP32_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + if value in (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 esp32_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + is_output = mode[CONF_OUTPUT] + 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_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] + ) + + return value diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py new file mode 100644 index 0000000000..fc1cef29e5 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -0,0 +1,53 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, +) +import esphome.config_validation as cv + +_ESP32C3_SPI_PSRAM_PINS = { + 12: "SPIHD", + 13: "SPIWP", + 14: "SPICS0", + 15: "SPICLK", + 16: "SPID", + 17: "SPIQ", +} + +_ESP32C3_STRAPPING_PINS = {2, 8, 9} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_c3_validate_gpio_pin(value): + if value < 0 or value > 21: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") + if value in _ESP32C3_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" + ) + if value in _ESP32C3_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + + return value + + +def esp32_c3_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 21: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") + + if is_input: + # All ESP32 pins support input mode + pass + return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py new file mode 100644 index 0000000000..5196ef0c09 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -0,0 +1,11 @@ +import esphome.config_validation as cv + + +def esp32_h2_validate_gpio_pin(value): + # ESP32-H2 not yet supported + raise cv.Invalid("ESP32-H2 isn't supported yet") + + +def esp32_h2_validate_supports(value): + # ESP32-H2 not yet supported + raise cv.Invalid("ESP32-H2 isn't supported yet") diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py new file mode 100644 index 0000000000..db244b6259 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -0,0 +1,80 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) + +import esphome.config_validation as cv + +_ESP32S2_SPI_PSRAM_PINS = { + 26: "SPICS1", + 27: "SPIHD", + 28: "SPIWP", + 29: "SPICS0", + 30: "SPICLK", + 31: "SPIQ", + 32: "SPID", +} + +_ESP32S2_STRAPPING_PINS = {0, 45, 46} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_s2_validate_gpio_pin(value): + if value < 0 or value > 46: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + + if value in _ESP32S2_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-S2s and is already used by the SPI/PSRAM interface (function: {_ESP32S2_SPI_PSRAM_PINS[value]})" + ) + if value in _ESP32S2_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + + if value in (22, 23, 24, 25): + # These pins are not exposed in GPIO mux (reason unknown) + # but they're missing from IO_MUX list in datasheet + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S2s.") + + return value + + +def esp32_s2_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + is_output = mode[CONF_OUTPUT] + is_pullup = mode[CONF_PULLUP] + is_pulldown = mode[CONF_PULLDOWN] + + if num < 0 or num > 46: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + if is_input: + # All ESP32 pins support input mode + pass + if is_output and num == 46: + raise cv.Invalid( + f"GPIO{num} does not support output pin mode.", + [CONF_MODE, CONF_OUTPUT], + ) + if is_pullup and num == 46: + raise cv.Invalid( + f"GPIO{num} does not support pullups.", [CONF_MODE, CONF_PULLUP] + ) + if is_pulldown and num == 46: + raise cv.Invalid( + f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] + ) + + return value diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py new file mode 100644 index 0000000000..f729a757c2 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -0,0 +1,74 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, +) + +import esphome.config_validation as cv + +_ESP_32S3_SPI_PSRAM_PINS = { + 26: "SPICS1", + 27: "SPIHD", + 28: "SPIWP", + 29: "SPICS0", + 30: "SPICLK", + 31: "SPIQ", + 32: "SPID", +} + +_ESP_32_ESP32_S3R8_PSRAM_PINS = { + 33: "SPIIO4", + 34: "SPIIO5", + 35: "SPIIO6", + 36: "SPIIO7", + 37: "SPIDQS", +} + +_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_s3_validate_gpio_pin(value): + if value < 0 or value > 48: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + + if value in _ESP_32S3_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" + ) + if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: + _LOGGER.warning( + "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", + value, + ) + + if value in _ESP_32S3_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + + if value in (22, 23, 24, 25): + # These pins are not exposed in GPIO mux (reason unknown) + # but they're missing from IO_MUX list in datasheet + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") + + return value + + +def esp32_s3_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 48: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + if is_input: + # All ESP32 pins support input mode + pass + return value diff --git a/esphome/components/esp32/gpio_idf.cpp b/esphome/components/esp32/gpio_idf.cpp new file mode 100644 index 0000000000..498843ebff --- /dev/null +++ b/esphome/components/esp32/gpio_idf.cpp @@ -0,0 +1,143 @@ +#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) + +static gpio_mode_t IRAM_ATTR 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; + } +} + +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::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; +} + +void IDFInternalGPIOPin::setup() { + 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); + gpio_set_drive_capability(pin_, drive_strength_); +} + +void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { + // can't call gpio_config here because that logs in esp-idf which may cause issues + gpio_set_direction(pin_, flags_to_mode(flags)); + gpio_pull_mode_t pull_mode = GPIO_FLOATING; + if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { + pull_mode = GPIO_PULLUP_PULLDOWN; + } else if (flags & gpio::FLAG_PULLUP) { + pull_mode = GPIO_PULLUP_ONLY; + } else if (flags & gpio::FLAG_PULLDOWN) { + pull_mode = GPIO_PULLDOWN_ONLY; + } + gpio_set_pull_mode(pin_, pull_mode); +} + +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); } +void IDFInternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } + +} // 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 +} +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + gpio_set_direction(arg->pin, flags_to_mode(flags)); + gpio_pull_mode_t pull_mode = GPIO_FLOATING; + if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { + pull_mode = GPIO_PULLUP_PULLDOWN; + } else if (flags & gpio::FLAG_PULLUP) { + pull_mode = GPIO_PULLUP_ONLY; + } else if (flags & gpio::FLAG_PULLDOWN) { + pull_mode = GPIO_PULLDOWN_ONLY; + } + gpio_set_pull_mode(arg->pin, pull_mode); +} + +} // 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..a07d11378a --- /dev/null +++ b/esphome/components/esp32/gpio_idf.h @@ -0,0 +1,40 @@ +#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; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return (uint8_t) pin_; } + bool is_inverted() const override { return inverted_; } + + protected: + 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..8c2b67a942 --- /dev/null +++ b/esphome/components/esp32/preferences.cpp @@ -0,0 +1,154 @@ +#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() { + nvs_flash_init(); + 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 new file mode 100644 index 0000000000..4b5c741ad9 --- /dev/null +++ b/esphome/components/esp32_ble/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option + +DEPENDENCIES = ["esp32"] +CODEOWNERS = ["@jesserockz"] +CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] + +esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") +ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLE), + } +).extend(cv.COMPONENT_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 new file mode 100644 index 0000000000..ecd591d169 --- /dev/null +++ b/esphome/components/esp32_ble/ble.cpp @@ -0,0 +1,213 @@ +#ifdef USE_ESP32 + +#include "ble.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_ARDUINO +#include +#endif + +namespace esphome { +namespace esp32_ble { + +static const char *const TAG = "esp32_ble"; + +void ESP32BLE::setup() { + global_ble = this; + ESP_LOGCONFIG(TAG, "Setting up BLE..."); + + if (!ble_setup_()) { + ESP_LOGE(TAG, "BLE could not be set up"); + this->mark_failed(); + return; + } + + this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) + + this->advertising_->set_scan_response(true); + this->advertising_->set_min_preferred_interval(0x06); + this->advertising_->start(); + + ESP_LOGD(TAG, "BLE setup complete"); +} + +void ESP32BLE::mark_failed() { + Component::mark_failed(); +#ifdef USE_ESP32_BLE_SERVER + if (this->server_ != nullptr) { + this->server_->mark_failed(); + } +#endif +} + +bool ESP32BLE::ble_setup_() { + esp_err_t err = nvs_flash_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); + 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); + + err = esp_bluedroid_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); + return false; + } + err = esp_bluedroid_enable(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); + return false; + } + err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; + } + + if (this->has_server()) { + err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); + return false; + } + } + + if (this->has_client()) { + err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } + } + + 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; + } + + esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); + return false; + } + + // BLE takes some time to be fully set up, 200ms should be more than enough + delay(200); // NOLINT + + return true; +} + +void ESP32BLE::loop() { + BLEEvent *ble_event = this->ble_events_.pop(); + while (ble_event != nullptr) { + switch (ble_event->type_) { + 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 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; // 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); // 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); + switch (event) { + default: + break; + } +} + +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); // 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) { + ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); +#ifdef USE_ESP32_BLE_SERVER + this->server_->gatts_event_handler(event, gatts_if, param); +#endif +} + +void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + // this->client_->gattc_event_handler(event, gattc_if, param); +} + +float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } + +void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE:"); } + +ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h new file mode 100644 index 0000000000..0477dee070 --- /dev/null +++ b/esphome/components/esp32_ble/ble.h @@ -0,0 +1,75 @@ +#pragma once + +#include "ble_advertising.h" + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "queue.h" + +#ifdef USE_ESP32_BLE_SERVER +#include "esphome/components/esp32_ble_server/ble_server.h" +#endif + +#ifdef USE_ESP32 + +#include +#include +#include +namespace esphome { +namespace esp32_ble { + +// NOLINTNEXTLINE(modernize-use-using) +typedef struct { + void *peer_device; + bool connected; + uint16_t mtu; +} conn_status_t; + +class ESP32BLE : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + void mark_failed() override; + + bool has_server() { +#ifdef USE_ESP32_BLE_SERVER + return this->server_ != nullptr; +#else + return false; +#endif + } + bool has_client() { return false; } + + BLEAdvertising *get_advertising() { return this->advertising_; } + +#ifdef USE_ESP32_BLE_SERVER + void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; } +#endif + protected: + static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + + void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_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_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + + bool ble_setup_(); + +#ifdef USE_ESP32_BLE_SERVER + esp32_ble_server::BLEServer *server_{nullptr}; +#endif + Queue ble_events_; + BLEAdvertising *advertising_; +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern ESP32BLE *global_ble; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp new file mode 100644 index 0000000000..31b1f4c383 --- /dev/null +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -0,0 +1,107 @@ +#include "ble_advertising.h" + +#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; + this->advertising_data_.include_txpower = true; + this->advertising_data_.min_interval = 0x20; + this->advertising_data_.max_interval = 0x40; + this->advertising_data_.appearance = 0x00; + this->advertising_data_.manufacturer_len = 0; + this->advertising_data_.p_manufacturer_data = nullptr; + this->advertising_data_.service_data_len = 0; + this->advertising_data_.p_service_data = nullptr; + this->advertising_data_.service_uuid_len = 0; + this->advertising_data_.p_service_uuid = nullptr; + this->advertising_data_.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT); + + this->advertising_params_.adv_int_min = 0x20; + this->advertising_params_.adv_int_max = 0x40; + this->advertising_params_.adv_type = ADV_TYPE_IND; + this->advertising_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; + this->advertising_params_.channel_map = ADV_CHNL_ALL; + this->advertising_params_.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY; + this->advertising_params_.peer_addr_type = BLE_ADDR_TYPE_PUBLIC; +} + +void BLEAdvertising::add_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.push_back(uuid); } +void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { + this->advertising_uuids_.erase(std::remove(this->advertising_uuids_.begin(), this->advertising_uuids_.end(), uuid), + this->advertising_uuids_.end()); +} + +void BLEAdvertising::start() { + int num_services = this->advertising_uuids_.size(); + if (num_services == 0) { + 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++) { + ESPBTUUID uuid = this->advertising_uuids_[i]; + memcpy(p, uuid.as_128bit().get_uuid().uuid.uuid128, 16); + p += 16; + } + } + + esp_err_t err; + + this->advertising_data_.set_scan_rsp = false; + this->advertising_data_.include_name = !this->scan_response_; + this->advertising_data_.include_txpower = !this->scan_response_; + err = esp_ble_gap_config_adv_data(&this->advertising_data_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err); + return; + } + + memcpy(&this->scan_response_data_, &this->advertising_data_, sizeof(esp_ble_adv_data_t)); + this->scan_response_data_.set_scan_rsp = true; + this->scan_response_data_.include_name = true; + this->scan_response_data_.include_txpower = true; + this->scan_response_data_.appearance = 0; + this->scan_response_data_.flag = 0; + err = esp_ble_gap_config_adv_data(&this->scan_response_data_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err); + return; + } + + if (this->advertising_data_.service_uuid_len > 0) { + delete[] this->advertising_data_.p_service_uuid; + this->advertising_data_.p_service_uuid = nullptr; + } + + err = esp_ble_gap_start_advertising(&this->advertising_params_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); + return; + } +} + +void BLEAdvertising::stop() { + esp_err_t err = esp_ble_gap_stop_advertising(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err); + return; + } +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h new file mode 100644 index 0000000000..01e2ba1295 --- /dev/null +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#ifdef USE_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +class ESPBTUUID; + +class BLEAdvertising { + public: + BLEAdvertising(); + + void add_service_uuid(ESPBTUUID uuid); + void remove_service_uuid(ESPBTUUID uuid); + void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } + void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } + + void start(); + void stop(); + + protected: + bool scan_response_; + esp_ble_adv_data_t advertising_data_; + esp_ble_adv_data_t scan_response_data_; + esp_ble_adv_params_t advertising_params_; + std::vector advertising_uuids_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp new file mode 100644 index 0000000000..8556aa87df --- /dev/null +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -0,0 +1,190 @@ +#include "ble_uuid.h" + +#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; + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = uuid; + return ret; +} +ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = uuid; + return ret; +} +ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_128; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + 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; + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + return ret; +} +ESPBTUUID ESPBTUUID::as_128bit() const { + if (this->uuid_.len == ESP_UUID_LEN_128) { + return *this; + } + uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint32_t uuid32; + if (this->uuid_.len == ESP_UUID_LEN_32) { + uuid32 = this->uuid_.uuid.uuid32; + } else { + uuid32 = this->uuid_.uuid.uuid16; + } + for (uint8_t i = 0; i < this->uuid_.len; i++) { + data[12 + i] = ((uuid32 >> i * 8) & 0xFF); + } + return ESPBTUUID::from_raw(data); +} +bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { + if (this->uuid_.len == ESP_UUID_LEN_16) { + return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; + } else if (this->uuid_.len == ESP_UUID_LEN_32) { + for (uint8_t i = 0; i < 3; i++) { + bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; + bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; + if (a && b) + return true; + } + } else { + for (uint8_t i = 0; i < 15; i++) { + if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) + return true; + } + } + return false; +} +bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { + if (this->uuid_.len == uuid.uuid_.len) { + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { + return true; + } + break; + case ESP_UUID_LEN_32: + if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { + return true; + } + break; + case ESP_UUID_LEN_128: + for (int i = 0; i < ESP_UUID_LEN_128; i++) { + if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { + return false; + } + } + return true; + break; + } + } else { + return this->as_128bit() == uuid.as_128bit(); + } + return false; +} +esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } +std::string ESPBTUUID::to_string() { + char sbuf[64]; + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + break; + case ESP_UUID_LEN_32: + sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); + break; + default: + case ESP_UUID_LEN_128: + char *bpos = sbuf; + for (int8_t i = 15; i >= 0; i--) { + sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); + bpos += 2; + if (i == 6 || i == 8 || i == 10 || i == 12) + sprintf(bpos++, "-"); + } + sbuf[47] = '\0'; + break; + } + return sbuf; +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h new file mode 100644 index 0000000000..f953f9fede --- /dev/null +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +class ESPBTUUID { + public: + ESPBTUUID(); + + static ESPBTUUID from_uint16(uint16_t uuid); + + static ESPBTUUID from_uint32(uint32_t uuid); + + 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; + + bool contains(uint8_t data1, uint8_t data2) const; + + bool operator==(const ESPBTUUID &uuid) const; + bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } + + esp_bt_uuid_t get_uuid(); + + std::string to_string(); + + protected: + esp_bt_uuid_t uuid_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h new file mode 100644 index 0000000000..8d05eca058 --- /dev/null +++ b/esphome/components/esp32_ble/queue.h @@ -0,0 +1,141 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +/* + * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather + * 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. + */ + +namespace esphome { +namespace esp32_ble { + +template class Queue { + public: + Queue() { m_ = xSemaphoreCreateMutex(); } + + void push(T *element) { + if (element == nullptr) + return; + 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(); + } + xSemaphoreGive(m_); + } + return element; + } + + protected: + std::queue q_; + SemaphoreHandle_t m_; +}; + +// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = GAP; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of notify event data. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); + this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; + break; + default: + break; + } + this->type_ = GATTC; + }; + + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); + // Need to also make a copy of write data. + switch (e) { + case ESP_GATTS_WRITE_EVT: + memcpy(this->event_.gatts.data, p->write.value, p->write.len); + this->event_.gatts.gatts_param.write.value = this->event_.gatts.data; + break; + default: + break; + } + this->type_ = GATTS; + }; + + 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; + esp_ble_gattc_cb_param_t gattc_param; + 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; + esp_ble_gatts_cb_param_t gatts_param; + uint8_t data[64]; + } gatts; + } event_; + // NOLINTNEXTLINE(readability-identifier-naming) + enum ble_event_t : uint8_t { + GAP, + GATTC, + GATTS, + } type_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 2f02e71fef..d6cbb15dd2 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -1,29 +1,36 @@ 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] -CONFLICTS_WITH = ['esp32_ble_tracker'] +DEPENDENCIES = ["esp32"] +CONFLICTS_WITH = ["esp32_ble_tracker"] -esp32_ble_beacon_ns = cg.esphome_ns.namespace('esp32_ble_beacon') -ESP32BLEBeacon = esp32_ble_beacon_ns.class_('ESP32BLEBeacon', cg.Component) +esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") +ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) -CONF_MAJOR = 'major' -CONF_MINOR = 'minor' +CONF_MAJOR = "major" +CONF_MINOR = "minor" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), - cv.Required(CONF_TYPE): cv.one_of('IBEACON', upper=True), - cv.Required(CONF_UUID): cv.uuid, - cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, - cv.Optional(CONF_MINOR, default=61958): cv.uint16_t, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), + cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), + cv.Required(CONF_UUID): cv.uuid, + cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, + cv.Optional(CONF_MINOR, default=61958): cv.uint16_t, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +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) - yield cg.register_component(var, config) + 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 b7810bd056..955bc8595f 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 *TAG = "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() { @@ -50,7 +57,7 @@ void ESP32BLEBeacon::setup() { ); } -float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::DATA; } +float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::BLUETOOTH; } void ESP32BLEBeacon::ble_core_task(void *params) { ble_setup(); @@ -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 new file mode 100644 index 0000000000..2fcc5c7743 --- /dev/null +++ b/esphome/components/esp32_ble_server/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MODEL +from esphome.components import esp32_ble +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option + +AUTO_LOAD = ["esp32_ble"] +CODEOWNERS = ["@jesserockz"] +CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +DEPENDENCIES = ["esp32"] + +CONF_MANUFACTURER = "manufacturer" +CONF_BLE_ID = "ble_id" + +esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") +BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component) +BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEServer), + cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), + cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, + cv.Optional(CONF_MODEL): cv.string, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_BLE_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) + if CONF_MODEL in config: + cg.add(var.set_model(config[CONF_MODEL])) + 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 new file mode 100644 index 0000000000..ee0808d2c4 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_2901.cpp @@ -0,0 +1,18 @@ +#include "ble_2901.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { + +BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {} +BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2901)) { + this->set_value(data, length); + this->permissions_ = ESP_GATT_PERM_READ; +} + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_2901.h b/esphome/components/esp32_ble_server/ble_2901.h new file mode 100644 index 0000000000..60f53e55b2 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_2901.h @@ -0,0 +1,19 @@ +#pragma once + +#include "ble_descriptor.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { + +class BLE2901 : public BLEDescriptor { + public: + BLE2901(const std::string &value); + BLE2901(const uint8_t *data, size_t length); +}; + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_2902.cpp b/esphome/components/esp32_ble_server/ble_2902.cpp new file mode 100644 index 0000000000..2f34573c37 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_2902.cpp @@ -0,0 +1,20 @@ +#include "ble_2902.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace esp32_ble_server { + +BLE2902::BLE2902() : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2902)) { + this->value_.attr_len = 2; + uint8_t data[2] = {0, 0}; + memcpy(this->value_.attr_value, data, 2); +} + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_2902.h b/esphome/components/esp32_ble_server/ble_2902.h new file mode 100644 index 0000000000..64605924ad --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_2902.h @@ -0,0 +1,18 @@ +#pragma once + +#include "ble_descriptor.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { + +class BLE2902 : public BLEDescriptor { + public: + BLE2902(); +}; + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp new file mode 100644 index 0000000000..fae8c13934 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -0,0 +1,306 @@ +#include "ble_characteristic.h" +#include "ble_server.h" +#include "ble_service.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { + +static const char *const TAG = "esp32_ble_server.characteristic"; + +BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { + this->set_value_lock_ = xSemaphoreCreateBinary(); + xSemaphoreGive(this->set_value_lock_); + + this->properties_ = (esp_gatt_char_prop_t) 0; + + this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0); + this->set_indicate_property((properties & PROPERTY_INDICATE) != 0); + this->set_notify_property((properties & PROPERTY_NOTIFY) != 0); + this->set_read_property((properties & PROPERTY_READ) != 0); + this->set_write_property((properties & PROPERTY_WRITE) != 0); + this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0); +} + +void BLECharacteristic::set_value(std::vector value) { + xSemaphoreTake(this->set_value_lock_, 0L); + this->value_ = std::move(value); + xSemaphoreGive(this->set_value_lock_); +} +void BLECharacteristic::set_value(const std::string &value) { + this->set_value(std::vector(value.begin(), value.end())); +} +void BLECharacteristic::set_value(const uint8_t *data, size_t length) { + this->set_value(std::vector(data, data + length)); +} +void BLECharacteristic::set_value(uint8_t &data) { + uint8_t temp[1]; + temp[0] = data; + this->set_value(temp, 1); +} +void BLECharacteristic::set_value(uint16_t &data) { + uint8_t temp[2]; + temp[0] = data; + temp[1] = data >> 8; + this->set_value(temp, 2); +} +void BLECharacteristic::set_value(uint32_t &data) { + uint8_t temp[4]; + temp[0] = data; + temp[1] = data >> 8; + temp[2] = data >> 16; + temp[3] = data >> 24; + this->set_value(temp, 4); +} +void BLECharacteristic::set_value(int &data) { + uint8_t temp[4]; + temp[0] = data; + temp[1] = data >> 8; + temp[2] = data >> 16; + temp[3] = data >> 24; + this->set_value(temp, 4); +} +void BLECharacteristic::set_value(float &data) { + float temp = data; + this->set_value((uint8_t *) &temp, 4); +} +void BLECharacteristic::set_value(double &data) { + double temp = data; + this->set_value((uint8_t *) &temp, 8); +} +void BLECharacteristic::set_value(bool &data) { + uint8_t temp[1]; + temp[0] = data; + this->set_value(temp, 1); +} + +void BLECharacteristic::notify(bool notification) { + if (!notification) { + ESP_LOGW(TAG, "notification=false is not yet supported"); + // TODO: Handle when notification=false + } + if (this->service_->get_server()->get_connected_client_count() == 0) + return; + + for (auto &client : this->service_->get_server()->get_clients()) { + size_t length = this->value_.size(); + esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first, + this->handle_, length, this->value_.data(), false); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err); + return; + } + } +} + +void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } + +void BLECharacteristic::do_create(BLEService *service) { + this->service_ = service; + esp_attr_control_t control; + control.auto_rsp = ESP_GATT_RSP_BY_APP; + + ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str()); + + esp_bt_uuid_t uuid = this->uuid_.get_uuid(); + esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast(this->permissions_), + this->properties_, nullptr, &control); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_add_char failed: %d", err); + return; + } + + this->state_ = CREATING; +} + +bool BLECharacteristic::is_created() { + if (this->state_ == CREATED) + return true; + + if (this->state_ != CREATING_DEPENDENTS) + return false; + + bool created = true; + for (auto *descriptor : this->descriptors_) { + created &= descriptor->is_created(); + } + if (created) + this->state_ = CREATED; + return this->state_ == CREATED; +} + +bool BLECharacteristic::is_failed() { + if (this->state_ == FAILED) + return true; + + bool failed = false; + for (auto *descriptor : this->descriptors_) { + failed |= descriptor->is_failed(); + } + if (failed) + this->state_ = FAILED; + return this->state_ == FAILED; +} + +void BLECharacteristic::set_broadcast_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); +} +void BLECharacteristic::set_indicate_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); +} +void BLECharacteristic::set_notify_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); +} +void BLECharacteristic::set_read_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); +} +void BLECharacteristic::set_write_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); +} +void BLECharacteristic::set_write_no_response_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); +} + +void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_ADD_CHAR_EVT: { + if (this->uuid_ == ESPBTUUID::from_uuid(param->add_char.char_uuid)) { + this->handle_ = param->add_char.attr_handle; + + for (auto *descriptor : this->descriptors_) { + descriptor->do_create(this); + } + + this->state_ = CREATING_DEPENDENTS; + } + break; + } + case ESP_GATTS_READ_EVT: { + if (param->read.handle != this->handle_) + break; // Not this characteristic + + if (!param->read.need_rsp) + break; // For some reason you can request a read but not want a response + + uint16_t max_offset = 22; + + esp_gatt_rsp_t response; + if (param->read.is_long) { + if (this->value_.size() - this->value_read_offset_ < max_offset) { + // Last message in the chain + response.attr_value.len = this->value_.size() - this->value_read_offset_; + response.attr_value.offset = this->value_read_offset_; + memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len); + this->value_read_offset_ = 0; + } else { + response.attr_value.len = max_offset; + response.attr_value.offset = this->value_read_offset_; + memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len); + this->value_read_offset_ += max_offset; + } + } else { + response.attr_value.offset = 0; + if (this->value_.size() + 1 > max_offset) { + response.attr_value.len = max_offset; + this->value_read_offset_ = max_offset; + } else { + response.attr_value.len = this->value_.size(); + } + memcpy(response.attr_value.value, this->value_.data(), response.attr_value.len); + } + + response.attr_value.handle = this->handle_; + response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; + + esp_err_t err = + esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &response); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); + } + break; + } + case ESP_GATTS_WRITE_EVT: { + if (this->handle_ != param->write.handle) + return; + + if (param->write.is_prep) { + this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); + this->write_event_ = true; + } else { + this->set_value(param->write.value, param->write.len); + } + + if (param->write.need_rsp) { + esp_gatt_rsp_t response; + + response.attr_value.len = param->write.len; + response.attr_value.handle = this->handle_; + response.attr_value.offset = param->write.offset; + response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; + memcpy(response.attr_value.value, param->write.value, param->write.len); + + esp_err_t err = + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); + } + } + + if (!param->write.is_prep) { + this->on_write_(this->value_); + } + + break; + } + + case ESP_GATTS_EXEC_WRITE_EVT: { + if (!this->write_event_) + break; + this->write_event_ = false; + if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { + this->on_write_(this->value_); + } + esp_err_t err = + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); + } + break; + } + default: + break; + } + + for (auto *descriptor : this->descriptors_) { + descriptor->gatts_event_handler(event, gatts_if, param); + } +} + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h new file mode 100644 index 0000000000..d7af3a934a --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -0,0 +1,99 @@ +#pragma once + +#include "ble_descriptor.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + +#include + +#ifdef USE_ESP32 + +#include +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble_server { + +using namespace esp32_ble; + +class BLEService; + +class BLECharacteristic { + public: + BLECharacteristic(ESPBTUUID uuid, uint32_t properties); + + void set_value(const uint8_t *data, size_t length); + void set_value(std::vector value); + void set_value(const std::string &value); + void set_value(uint8_t &data); + void set_value(uint16_t &data); + void set_value(uint32_t &data); + void set_value(int &data); + void set_value(float &data); + void set_value(double &data); + void set_value(bool &data); + + void set_broadcast_property(bool value); + void set_indicate_property(bool value); + void set_notify_property(bool value); + void set_read_property(bool value); + void set_write_property(bool value); + void set_write_no_response_property(bool value); + + void notify(bool notification = true); + + 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_ = func; } + + void add_descriptor(BLEDescriptor *descriptor); + + BLEService *get_service() { return this->service_; } + ESPBTUUID get_uuid() { return this->uuid_; } + std::vector &get_value() { return this->value_; } + + static const uint32_t PROPERTY_READ = 1 << 0; + static const uint32_t PROPERTY_WRITE = 1 << 1; + static const uint32_t PROPERTY_NOTIFY = 1 << 2; + static const uint32_t PROPERTY_BROADCAST = 1 << 3; + static const uint32_t PROPERTY_INDICATE = 1 << 4; + static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + + bool is_created(); + bool is_failed(); + + protected: + bool write_event_{false}; + BLEService *service_; + ESPBTUUID uuid_; + esp_gatt_char_prop_t properties_; + uint16_t handle_{0xFFFF}; + + uint16_t value_read_offset_{0}; + std::vector value_; + SemaphoreHandle_t set_value_lock_; + + std::vector descriptors_; + + std::function &)> on_write_; + + esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; + + enum State : uint8_t { + FAILED = 0x00, + INIT, + CREATING, + CREATING_DEPENDENTS, + CREATED, + } state_{INIT}; +}; + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp new file mode 100644 index 0000000000..bfb6224335 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -0,0 +1,78 @@ +#include "ble_descriptor.h" +#include "ble_characteristic.h" +#include "ble_service.h" +#include "esphome/core/log.h" + +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { + +static const char *const TAG = "esp32_ble_server.descriptor"; + +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); // NOLINT +} + +BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT + +void BLEDescriptor::do_create(BLECharacteristic *characteristic) { + this->characteristic_ = characteristic; + esp_attr_control_t control; + control.auto_rsp = ESP_GATT_AUTO_RSP; + + ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str()); + esp_bt_uuid_t uuid = this->uuid_.get_uuid(); + esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid, + this->permissions_, &this->value_, &control); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_add_char_descr failed: %d", err); + this->state_ = FAILED; + return; + } + this->state_ = CREATING; +} + +void BLEDescriptor::set_value(const std::string &value) { this->set_value((uint8_t *) value.data(), value.length()); } +void BLEDescriptor::set_value(const uint8_t *data, size_t length) { + if (length > this->value_.attr_max_len) { + ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); + return; + } + this->value_.attr_len = length; + memcpy(this->value_.attr_value, data, length); +} + +void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_ADD_CHAR_DESCR_EVT: { + if (this->characteristic_ != nullptr && this->uuid_ == ESPBTUUID::from_uuid(param->add_char_descr.descr_uuid) && + this->characteristic_->get_service()->get_handle() == param->add_char_descr.service_handle && + this->characteristic_ == this->characteristic_->get_service()->get_last_created_characteristic()) { + this->handle_ = param->add_char_descr.attr_handle; + this->state_ = CREATED; + } + break; + } + case ESP_GATTS_WRITE_EVT: { + if (this->handle_ == param->write.handle) { + this->value_.attr_len = param->write.len; + memcpy(this->value_.attr_value, param->write.value, param->write.len); + } + break; + } + default: + break; + } +} + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h new file mode 100644 index 0000000000..4b8fb345c3 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/components/esp32_ble/ble_uuid.h" + +#ifdef USE_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble_server { + +using namespace esp32_ble; + +class BLECharacteristic; + +class BLEDescriptor { + public: + BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100); + virtual ~BLEDescriptor(); + void do_create(BLECharacteristic *characteristic); + + void set_value(const std::string &value); + void set_value(const uint8_t *data, size_t length); + + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + bool is_created() { return this->state_ == CREATED; } + bool is_failed() { return this->state_ == FAILED; } + + protected: + BLECharacteristic *characteristic_{nullptr}; + ESPBTUUID uuid_; + uint16_t handle_{0xFFFF}; + + esp_attr_value_t value_; + + esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; + + enum State : uint8_t { + FAILED = 0x00, + INIT, + CREATING, + CREATED, + } state_{INIT}; +}; + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp new file mode 100644 index 0000000000..15bea07021 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -0,0 +1,167 @@ +#include "ble_server.h" + +#include "esphome/components/esp32_ble/ble.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/version.h" + +#ifdef USE_ESP32 + +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble_server { + +static const char *const TAG = "esp32_ble_server"; + +static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A; +static const uint16_t MODEL_UUID = 0x2A24; +static const uint16_t VERSION_UUID = 0x2A26; +static const uint16_t MANUFACTURER_UUID = 0x2A29; + +void BLEServer::setup() { + if (this->is_failed()) { + ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE"); + return; + } + + ESP_LOGD(TAG, "Setting up BLE Server..."); + + global_ble_server = this; +} + +void BLEServer::loop() { + switch (this->state_) { + case RUNNING: + return; + + case INIT: { + esp_err_t err = esp_ble_gatts_app_register(0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_app_register failed: %d", err); + this->mark_failed(); + return; + } + this->state_ = REGISTERING; + break; + } + case REGISTERING: { + if (this->registered_) { + this->device_information_service_ = this->create_service(DEVICE_INFORMATION_SERVICE_UUID); + + this->create_device_characteristics_(); + + this->state_ = STARTING_SERVICE; + } + break; + } + case STARTING_SERVICE: { + if (!this->device_information_service_->is_created()) { + break; + } + if (this->device_information_service_->is_running()) { + this->state_ = RUNNING; + this->can_proceed_ = true; + ESP_LOGD(TAG, "BLE server setup successfully"); + } else if (!this->device_information_service_->is_starting()) { + this->device_information_service_->start(); + } + break; + } + } +} + +bool BLEServer::create_device_characteristics_() { + if (this->model_.has_value()) { + BLECharacteristic *model = + this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); + model->set_value(this->model_.value()); + } else { + BLECharacteristic *model = + this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); + model->set_value(ESPHOME_BOARD); + } + + BLECharacteristic *version = + this->device_information_service_->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ); + version->set_value("ESPHome " ESPHOME_VERSION); + + BLECharacteristic *manufacturer = + this->device_information_service_->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ); + manufacturer->set_value(this->manufacturer_); + + return true; +} + +std::shared_ptr BLEServer::create_service(const uint8_t *uuid, bool advertise) { + return this->create_service(ESPBTUUID::from_raw(uuid), advertise); +} +std::shared_ptr BLEServer::create_service(uint16_t uuid, bool advertise) { + return this->create_service(ESPBTUUID::from_uint16(uuid), advertise); +} +std::shared_ptr BLEServer::create_service(const std::string &uuid, bool advertise) { + return this->create_service(ESPBTUUID::from_raw(uuid), advertise); +} +std::shared_ptr BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, + uint8_t inst_id) { + ESP_LOGV(TAG, "Creating service - %s", uuid.to_string().c_str()); + std::shared_ptr service = std::make_shared(uuid, num_handles, inst_id); + this->services_.emplace_back(service); + if (advertise) { + esp32_ble::global_ble->get_advertising()->add_service_uuid(uuid); + } + service->do_create(this); + return service; +} + +void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_CONNECT_EVT: { + ESP_LOGD(TAG, "BLE Client connected"); + this->add_client_(param->connect.conn_id, (void *) this); + this->connected_clients_++; + for (auto *component : this->service_components_) { + component->on_client_connect(); + } + break; + } + case ESP_GATTS_DISCONNECT_EVT: { + ESP_LOGD(TAG, "BLE Client disconnected"); + if (this->remove_client_(param->disconnect.conn_id)) + this->connected_clients_--; + esp32_ble::global_ble->get_advertising()->start(); + for (auto *component : this->service_components_) { + component->on_client_disconnect(); + } + break; + } + case ESP_GATTS_REG_EVT: { + this->gatts_if_ = gatts_if; + this->registered_ = true; + break; + } + default: + break; + } + + for (const auto &service : this->services_) { + service->gatts_event_handler(event, gatts_if, param); + } +} + +float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } + +void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } + +BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h new file mode 100644 index 0000000000..d275eeab01 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -0,0 +1,98 @@ +#pragma once + +#include "ble_service.h" +#include "ble_characteristic.h" + +#include "esphome/components/esp32_ble/ble_advertising.h" +#include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/esp32_ble/queue.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +#include +#include + +#ifdef USE_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble_server { + +using namespace esp32_ble; + +class BLEServiceComponent { + public: + virtual void on_client_connect(){}; + virtual void on_client_disconnect(){}; + virtual void start(); + virtual void stop(); +}; + +class BLEServer : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + bool can_proceed() override { return this->can_proceed_; } + + void teardown(); + + void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; } + void set_model(const std::string &model) { this->model_ = model; } + + std::shared_ptr create_service(const uint8_t *uuid, bool advertise = false); + std::shared_ptr create_service(uint16_t uuid, bool advertise = false); + std::shared_ptr create_service(const std::string &uuid, bool advertise = false); + std::shared_ptr create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, + uint8_t inst_id = 0); + + esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } + uint32_t get_connected_client_count() { return this->connected_clients_; } + const std::map &get_clients() { return this->clients_; } + + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } + + protected: + bool create_device_characteristics_(); + + void add_client_(uint16_t conn_id, void *client) { + this->clients_.insert(std::pair(conn_id, client)); + } + bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } + + bool can_proceed_{false}; + + std::string manufacturer_; + optional model_; + esp_gatt_if_t gatts_if_{0}; + bool registered_{false}; + + uint32_t connected_clients_{0}; + std::map clients_; + + std::vector> services_; + std::shared_ptr device_information_service_; + + std::vector service_components_; + + enum State : uint8_t { + INIT = 0x00, + REGISTERING, + STARTING_SERVICE, + RUNNING, + } state_{INIT}; +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern BLEServer *global_ble_server; + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp new file mode 100644 index 0000000000..281164f0f5 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -0,0 +1,143 @@ +#include "ble_service.h" +#include "ble_server.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { + +static const char *const TAG = "esp32_ble_server.service"; + +BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id) + : uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id) {} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics_) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) +} + +BLECharacteristic *BLEService::get_characteristic(ESPBTUUID uuid) { + for (auto *chr : this->characteristics_) + if (chr->get_uuid() == uuid) + return chr; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(ESPBTUUID::from_uint16(uuid)); +} +BLECharacteristic *BLEService::create_characteristic(uint16_t uuid, esp_gatt_char_prop_t properties) { + return create_characteristic(ESPBTUUID::from_uint16(uuid), properties); +} +BLECharacteristic *BLEService::create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties) { + 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; +} + +void BLEService::do_create(BLEServer *server) { + this->server_ = server; + + esp_gatt_srvc_id_t srvc_id; + srvc_id.is_primary = true; + srvc_id.id.inst_id = this->inst_id_; + srvc_id.id.uuid = this->uuid_.get_uuid(); + + esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err); + this->init_state_ = FAILED; + return; + } + this->init_state_ = CREATING; +} + +bool BLEService::do_create_characteristics_() { + if (this->created_characteristic_count_ >= this->characteristics_.size() && + (this->last_created_characteristic_ == nullptr || this->last_created_characteristic_->is_created())) + return false; // Signifies there are no characteristics, or they are all finished being created. + + if (this->last_created_characteristic_ != nullptr && !this->last_created_characteristic_->is_created()) + return true; // Signifies that the previous characteristic is still being created. + + auto *characteristic = this->characteristics_[this->created_characteristic_count_++]; + this->last_created_characteristic_ = characteristic; + characteristic->do_create(this); + return true; +} + +void BLEService::start() { + if (this->do_create_characteristics_()) + return; + + esp_err_t err = esp_ble_gatts_start_service(this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); + return; + } + this->running_state_ = STARTING; +} + +void BLEService::stop() { + esp_err_t err = esp_ble_gatts_stop_service(this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); + return; + } + this->running_state_ = STOPPING; +} + +bool BLEService::is_created() { return this->init_state_ == CREATED; } +bool BLEService::is_failed() { + if (this->init_state_ == FAILED) + return true; + bool failed = false; + for (auto *characteristic : this->characteristics_) + failed |= characteristic->is_failed(); + + if (failed) + this->init_state_ = FAILED; + return this->init_state_ == FAILED; +} + +void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_CREATE_EVT: { + if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) && + this->inst_id_ == param->create.service_id.id.inst_id) { + this->handle_ = param->create.service_handle; + this->init_state_ = CREATED; + } + break; + } + case ESP_GATTS_START_EVT: { + if (param->start.service_handle == this->handle_) { + this->running_state_ = RUNNING; + } + break; + } + case ESP_GATTS_STOP_EVT: { + if (param->start.service_handle == this->handle_) { + this->running_state_ = STOPPED; + } + break; + } + default: + break; + } + + for (auto *characteristic : this->characteristics_) { + characteristic->gatts_event_handler(event, gatts_if, param); + } +} + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h new file mode 100644 index 0000000000..16cc897238 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -0,0 +1,81 @@ +#pragma once + +#include "ble_characteristic.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + +#ifdef USE_ESP32 + +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble_server { + +class BLEServer; + +using namespace esp32_ble; + +class BLEService { + public: + BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id); + ~BLEService(); + BLECharacteristic *get_characteristic(ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); + + BLECharacteristic *create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties); + BLECharacteristic *create_characteristic(uint16_t uuid, esp_gatt_char_prop_t properties); + BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties); + + ESPBTUUID get_uuid() { return this->uuid_; } + BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; } + uint16_t get_handle() { return this->handle_; } + + BLEServer *get_server() { return this->server_; } + + void do_create(BLEServer *server); + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + void start(); + void stop(); + + bool is_created(); + bool is_failed(); + + bool is_running() { return this->running_state_ == RUNNING; } + bool is_starting() { return this->running_state_ == STARTING; } + + protected: + std::vector characteristics_; + BLECharacteristic *last_created_characteristic_{nullptr}; + uint32_t created_characteristic_count_{0}; + BLEServer *server_; + ESPBTUUID uuid_; + uint16_t num_handles_; + uint16_t handle_{0xFFFF}; + uint8_t inst_id_; + + bool do_create_characteristics_(); + + enum InitState : uint8_t { + FAILED = 0x00, + INIT, + CREATING, + CREATING_DEPENDENTS, + CREATED, + } init_state_{INIT}; + + enum RunningState : uint8_t { + STARTING, + RUNNING, + STOPPING, + STOPPED, + } running_state_{STOPPED}; +}; + +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index e3aaf29eea..e647b74a8f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -2,33 +2,46 @@ import re import esphome.codegen as cg 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, CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_MANUFACTURER_ID, \ - CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE, \ - CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE -from esphome.core import coroutine +from esphome.const import ( + CONF_ID, + CONF_INTERVAL, + CONF_DURATION, + CONF_TRIGGER_ID, + CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, + CONF_MANUFACTURER_ID, + CONF_ON_BLE_ADVERTISE, + CONF_ON_BLE_SERVICE_DATA_ADVERTISE, + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, +) +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] -AUTO_LOAD = ['xiaomi_ble', 'ruuvi_ble', 'oralb_ble'] +DEPENDENCIES = ["esp32"] -CONF_ESP32_BLE_ID = 'esp32_ble_id' -CONF_SCAN_PARAMETERS = 'scan_parameters' -CONF_WINDOW = 'window' -CONF_ACTIVE = 'active' -esp32_ble_tracker_ns = cg.esphome_ns.namespace('esp32_ble_tracker') -ESP32BLETracker = esp32_ble_tracker_ns.class_('ESP32BLETracker', cg.Component) -ESPBTDeviceListener = esp32_ble_tracker_ns.class_('ESPBTDeviceListener') -ESPBTDevice = esp32_ble_tracker_ns.class_('ESPBTDevice') -ESPBTDeviceConstRef = ESPBTDevice.operator('ref').operator('const') +CONF_ESP32_BLE_ID = "esp32_ble_id" +CONF_SCAN_PARAMETERS = "scan_parameters" +CONF_WINDOW = "window" +CONF_ACTIVE = "active" +esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") +ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") +ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") +ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") +ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const") adv_data_t = cg.std_vector.template(cg.uint8) -adv_data_t_const_ref = adv_data_t.operator('ref').operator('const') +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") # Triggers ESPBTAdvertiseTrigger = esp32_ble_tracker_ns.class_( - 'ESPBTAdvertiseTrigger', automation.Trigger.template(ESPBTDeviceConstRef)) + "ESPBTAdvertiseTrigger", automation.Trigger.template(ESPBTDeviceConstRef) +) BLEServiceDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( - 'BLEServiceDataAdvertiseTrigger', automation.Trigger.template(adv_data_t_const_ref)) + "BLEServiceDataAdvertiseTrigger", automation.Trigger.template(adv_data_t_const_ref) +) BLEManufacturerDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( - 'BLEManufacturerDataAdvertiseTrigger', automation.Trigger.template(adv_data_t_const_ref)) + "BLEManufacturerDataAdvertiseTrigger", + automation.Trigger.template(adv_data_t_const_ref), +) def validate_scan_parameters(config): @@ -37,19 +50,22 @@ def validate_scan_parameters(config): window = config[CONF_WINDOW] if window > interval: - raise cv.Invalid("Scan window ({}) needs to be smaller than scan interval ({})" - "".format(window, interval)) + raise cv.Invalid( + f"Scan window ({window}) needs to be smaller than scan interval ({interval})" + ) if interval.total_milliseconds * 3 > duration.total_milliseconds: - raise cv.Invalid("Scan duration needs to be at least three times the scan interval to" - "cover all BLE channels.") + raise cv.Invalid( + "Scan duration needs to be at least three times the scan interval to" + "cover all BLE channels." + ) return config -bt_uuid16_format = 'XXXX' -bt_uuid32_format = 'XXXXXXXX' -bt_uuid128_format = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' +bt_uuid16_format = "XXXX" +bt_uuid32_format = "XXXXXXXX" +bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" def bt_uuid(value): @@ -60,72 +76,111 @@ def bt_uuid(value): pattern = re.compile("^[A-F|0-9]{4,}$") if not pattern.match(value): raise cv.Invalid( - f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'") + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" + ) return value if len(value) == len(bt_uuid32_format): pattern = re.compile("^[A-F|0-9]{8,}$") if not pattern.match(value): raise cv.Invalid( - f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'") + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" + ) return value if len(value) == len(bt_uuid128_format): pattern = re.compile( - "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$") + "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + ) if not pattern.match(value): raise cv.Invalid( - f"Invalid hexadecimal value for 128 UUID format: '{in_value}'") + f"Invalid hexadecimal value for 128 UUID format: '{in_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" + ) def as_hex(value): - return cg.RawExpression(f'0x{value}ULL') + return cg.RawExpression(f"0x{value}ULL") 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)]] + 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))}}}" + ) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(ESP32BLETracker), - cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({ - cv.Optional(CONF_DURATION, default='5min'): cv.positive_time_period_seconds, - cv.Optional(CONF_INTERVAL, default='320ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_WINDOW, default='30ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ACTIVE, default=True): cv.boolean, - }), validate_scan_parameters), - cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, - }), - cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEServiceDataAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, - cv.Required(CONF_SERVICE_UUID): bt_uuid, - }), - cv.Optional(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEManufacturerDataAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, - cv.Required(CONF_MANUFACTURER_ID): bt_uuid, - }), +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLETracker), + cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( + cv.Schema( + { + cv.Optional( + CONF_DURATION, default="5min" + ): cv.positive_time_period_seconds, + cv.Optional( + CONF_INTERVAL, default="320ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WINDOW, default="30ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + } + ), + validate_scan_parameters, + ), + cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + } + ), + cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEServiceDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_SERVICE_UUID): bt_uuid, + } + ), + cv.Optional( + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE + ): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEManufacturerDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_MANUFACTURER_ID): bt_uuid, + } + ), + } +).extend(cv.COMPONENT_SCHEMA) - cv.Optional('scan_interval'): cv.invalid("This option has been removed in 1.14 (Reason: " - "it never had an effect)"), -}).extend(cv.COMPONENT_SCHEMA) - -ESP_BLE_DEVICE_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_ESP32_BLE_ID): cv.use_id(ESP32BLETracker), -}) +ESP_BLE_DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ESP32_BLE_ID): cv.use_id(ESP32BLETracker), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) params = config[CONF_SCAN_PARAMETERS] cg.add(var.set_scan_duration(params[CONF_DURATION])) cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) @@ -135,7 +190,7 @@ def to_code(config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) - yield automation.build_automation(trigger, [(ESPBTDeviceConstRef, 'x')], conf) + await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format): @@ -143,11 +198,11 @@ 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)) - yield automation.build_automation(trigger, [(adv_data_t_const_ref, 'x')], conf) + await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format): @@ -155,15 +210,23 @@ 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)) - yield automation.build_automation(trigger, [(adv_data_t_const_ref, 'x')], conf) + 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) -@coroutine -def register_ble_device(var, config): - paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) +async def register_ble_device(var, config): + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) - yield var + return var + + +async def register_client(var, config): + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + return var 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 5109af21fa..084dab4c84 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,27 +1,33 @@ +#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 namespace esphome { namespace esp32_ble_tracker { -static const char *TAG = "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; @@ -34,6 +40,8 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { return u; } +float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; } + void ESP32BLETracker::setup() { global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); @@ -44,13 +52,29 @@ void ESP32BLETracker::setup() { return; } - global_esp32_ble_tracker->start_scan(true); + global_esp32_ble_tracker->start_scan_(true); } void ESP32BLETracker::loop() { - if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + 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); + else + 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) + 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)) { @@ -69,6 +93,17 @@ void ESP32BLETracker::loop() { if (listener->parse_device(device)) found = true; + for (auto *client : this->clients_) + if (client->parse_device(device)) { + found = true; + 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_); + } + } + } + if (!found) { this->print_bt_device_info(device); } @@ -99,11 +134,37 @@ bool ESP32BLETracker::ble_setup() { return false; } - // 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); @@ -122,6 +183,11 @@ bool ESP32BLETracker::ble_setup() { ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); return false; } + err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } // Empty name esp_ble_gap_set_device_name(""); @@ -139,7 +205,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; @@ -166,31 +232,48 @@ void ESP32BLETracker::start_scan(bool first) { }); } +void ESP32BLETracker::register_client(ESPBTClient *client) { + client->app_id = ++this->app_id_; + this->clients_.push_back(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); // 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) { 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); 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_result(const esp_ble_gap_cb_param_t::ble_scan_result_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) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { if (this->scan_result_index_ < 16) { @@ -203,6 +286,19 @@ 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); // 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) { + for (auto *client : global_esp32_ble_tracker->clients_) { + client->gattc_event_handler(event, gattc_if, param); + } +} + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -223,6 +319,72 @@ 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; + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + return ret; +} ESPBTUUID ESPBTUUID::as_128bit() const { if (this->uuid_.len == ESP_UUID_LEN_128) { return *this; @@ -241,7 +403,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const { } bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { if (this->uuid_.len == ESP_UUID_LEN_16) { - return (this->uuid_.uuid.uuid16 >> 8) == data2 || (this->uuid_.uuid.uuid16 & 0xFF) == data1; + return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; } else if (this->uuid_.len == ESP_UUID_LEN_32) { for (uint8_t i = 0; i < 3; i++) { bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; @@ -284,21 +446,26 @@ 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: - sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); break; case ESP_UUID_LEN_32: - sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); break; default: case ESP_UUID_LEN_128: - for (uint8_t i = 0; i < 16; i++) - sprintf(sbuf + i * 3, "%02X:", this->uuid_.uuid.uuid128[i]); + char *bpos = sbuf; + for (int8_t i = 15; i >= 0; i--) { + sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); + bpos += 2; + if (i == 6 || i == 8 || i == 10 || i == 12) + sprintf(bpos++, "-"); + } sbuf[47] = '\0'; break; } @@ -316,6 +483,7 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData } void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + this->scan_result_ = param; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) this->address_[i] = param.bda[i]; this->address_type_ = param.ble_addr_type; @@ -357,15 +525,23 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } for (auto &data : this->manufacturer_datas_) { - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str()); + ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str()); + if (this->get_ibeacon().has_value()) { + auto ibeacon = this->get_ibeacon().value(); + ESP_LOGVV(TAG, " iBeacon data:"); + ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str()); + ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major()); + ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor()); + ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power()); + } } for (auto &data : this->service_datas_) { ESP_LOGVV(TAG, " Service data:"); ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); - ESP_LOGVV(TAG, " Data: %s", hexencode(data.data).c_str()); + ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); } - ESP_LOGVV(TAG, "Adv data: %s", hexencode(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); + ESP_LOGVV(TAG, "Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 8d011abfe3..9ff2a5a861 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -2,12 +2,14 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "queue.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include #include +#include #include namespace esphome { @@ -23,15 +25,20 @@ 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; bool contains(uint8_t data1, uint8_t data2) const; 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_; @@ -74,16 +81,12 @@ class ESPBTDevice { uint64_t address_uint64() const; + const uint8_t *address() const { return address_; } + esp_ble_addr_type_t get_address_type() const { return this->address_type_; } 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_; } @@ -94,6 +97,8 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } + const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } + optional get_ibeacon() const { for (auto &it : this->manufacturer_datas_) { auto res = ESPBLEiBeacon::from_manufacturer_data(it); @@ -118,6 +123,7 @@ class ESPBTDevice { std::vector service_uuids_; std::vector manufacturer_datas_{}; std::vector service_datas_{}; + esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; }; class ESP32BLETracker; @@ -132,6 +138,32 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; +enum class ClientState { + // Connection is idle, no device detected. + IDLE, + // Device advertisement found. + DISCOVERED, + // Connection in progress. + CONNECTING, + // Initial connection established. + CONNECTED, + // The client and sub-clients have completed setup. + ESTABLISHED, +}; + +class ESPBTClient : public ESPBTDeviceListener { + 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 connect() = 0; + void set_state(ClientState st) { this->state_ = st; } + ClientState state() const { return state_; } + int app_id; + + protected: + ClientState state_; +}; + class ESP32BLETracker : public Component { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -142,6 +174,7 @@ class ESP32BLETracker : public Component { /// Setup the FreeRTOS task and the Bluetooth stack. void setup() override; void dump_config() override; + float get_setup_priority() const override; void loop() override; @@ -150,25 +183,37 @@ class ESP32BLETracker : public Component { this->listeners_.push_back(listener); } + void register_client(ESPBTClient *client); + void print_bt_device_info(const ESPBTDevice &device); protected: /// 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); /// 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); + + 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); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; std::vector listeners_; + /// Client parameters. + std::vector clients_; /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; /// The interval in seconds to perform scans. @@ -182,8 +227,11 @@ class ESP32BLETracker : public Component { esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + 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 new file mode 100644 index 0000000000..f09b2ca8d7 --- /dev/null +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -0,0 +1,108 @@ +#pragma once + +#ifdef USE_ESP32 +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include +#include + +#include +#include +#include +#include + +/* + * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather + * 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. + */ + +namespace esphome { +namespace esp32_ble_tracker { + +template class Queue { + public: + Queue() { m_ = xSemaphoreCreateMutex(); } + + void push(T *element) { + if (element == nullptr) + return; + 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(); + } + xSemaphoreGive(m_); + } + return element; + } + + protected: + std::queue q_; + SemaphoreHandle_t m_; +}; + +// Received GAP and GATTC events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = 0; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of relevant event data. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); + this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; + break; + default: + break; + } + this->type_ = 1; + }; + + union { + 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 { // 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; + uint8_t data[64]; + } gattc; + } event_; + uint8_t type_; // 0=gap 1=gattc +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 81980d9d38..1135b31798 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,111 +1,141 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import 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 +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_PIN, + CONF_SCL, + CONF_SDA, + 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 +from esphome.cpp_helpers import setup_entity -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] -DEPENDENCIES = ['api'] +DEPENDENCIES = ["esp32"] -esp32_camera_ns = cg.esphome_ns.namespace('esp32_camera') -ESP32Camera = esp32_camera_ns.class_('ESP32Camera', cg.PollingComponent, cg.Nameable) -ESP32CameraFrameSize = esp32_camera_ns.enum('ESP32CameraFrameSize') +esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") +ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) +ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") FRAME_SIZES = { - '160X120': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, - 'QQVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, - '128X160': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_128X160, - 'QQVGA2': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_128X160, - '176X144': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144, - 'QCIF': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144, - '240X176': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176, - 'HQVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176, - '320X240': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240, - 'QVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240, - '400X296': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296, - 'CIF': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296, - '640X480': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480, - 'VGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480, - '800X600': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600, - 'SVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600, - '1024X768': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768, - 'XGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768, - '1280X1024': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, - 'SXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, - '1600X1200': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, - 'UXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, + "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, + "QQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, + "176X144": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144, + "QCIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144, + "240X176": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176, + "HQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176, + "320X240": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240, + "QVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240, + "400X296": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296, + "CIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296, + "640X480": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480, + "VGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480, + "800X600": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600, + "SVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600, + "1024X768": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768, + "XGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768, + "1280X1024": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, + "SXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, + "1600X1200": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, + "UXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, } -CONF_VSYNC_PIN = 'vsync_pin' -CONF_HREF_PIN = 'href_pin' -CONF_PIXEL_CLOCK_PIN = 'pixel_clock_pin' -CONF_EXTERNAL_CLOCK = 'external_clock' -CONF_I2C_PINS = 'i2c_pins' -CONF_POWER_DOWN_PIN = 'power_down_pin' +CONF_VSYNC_PIN = "vsync_pin" +CONF_HREF_PIN = "href_pin" +CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin" +CONF_EXTERNAL_CLOCK = "external_clock" +CONF_I2C_PINS = "i2c_pins" +CONF_POWER_DOWN_PIN = "power_down_pin" -CONF_MAX_FRAMERATE = 'max_framerate' -CONF_IDLE_FRAMERATE = 'idle_framerate' -CONF_JPEG_QUALITY = 'jpeg_quality' -CONF_VERTICAL_FLIP = 'vertical_flip' -CONF_HORIZONTAL_MIRROR = 'horizontal_mirror' -CONF_CONTRAST = 'contrast' -CONF_SATURATION = 'saturation' -CONF_TEST_PATTERN = 'test_pattern' +CONF_MAX_FRAMERATE = "max_framerate" +CONF_IDLE_FRAMERATE = "idle_framerate" +CONF_JPEG_QUALITY = "jpeg_quality" +CONF_VERTICAL_FLIP = "vertical_flip" +CONF_HORIZONTAL_MIRROR = "horizontal_mirror" +CONF_AEC2 = "aec2" +CONF_AE_LEVEL = "ae_level" +CONF_AEC_VALUE = "aec_value" +CONF_SATURATION = "saturation" +CONF_TEST_PATTERN = "test_pattern" camera_range_param = cv.int_range(min=-2, max=2) -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.Required(CONF_EXTERNAL_CLOCK): cv.Schema({ - cv.Required(CONF_PIN): pins.output_pin, - cv.Optional(CONF_FREQUENCY, default='20MHz'): cv.All(cv.frequency, cv.one_of(20e6, 10e6)), - }), - cv.Required(CONF_I2C_PINS): cv.Schema({ - cv.Required(CONF_SDA): pins.output_pin, - cv.Required(CONF_SCL): pins.output_pin, - }), - cv.Optional(CONF_RESET_PIN): pins.output_pin, - cv.Optional(CONF_POWER_DOWN_PIN): pins.output_pin, - - cv.Optional(CONF_MAX_FRAMERATE, default='10 fps'): cv.All(cv.framerate, - cv.Range(min=0, min_included=False, - max=60)), - cv.Optional(CONF_IDLE_FRAMERATE, default='0.1 fps'): cv.All(cv.framerate, - cv.Range(min=0, max=1)), - cv.Optional(CONF_RESOLUTION, default='640X480'): cv.enum(FRAME_SIZES, upper=True), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63), - cv.Optional(CONF_CONTRAST, default=0): camera_range_param, - cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, - cv.Optional(CONF_SATURATION, default=0): camera_range_param, - cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, - cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, - cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ESP32Camera), + 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.internal_gpio_input_pin_number, + cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( + cv.frequency, cv.one_of(20e6, 10e6) + ), + } + ), + cv.Required(CONF_I2C_PINS): cv.Schema( + { + 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.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) + ), + cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( + cv.framerate, cv.Range(min=0, max=1) + ), + cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( + FRAME_SIZES, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63), + cv.Optional(CONF_CONTRAST, default=0): camera_range_param, + cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, + cv.Optional(CONF_SATURATION, default=0): camera_range_param, + cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, + cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, + cv.Optional(CONF_AEC2, default=False): cv.boolean, + cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, + cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), + cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) SETTERS = { - CONF_DATA_PINS: 'set_data_pins', - CONF_VSYNC_PIN: 'set_vsync_pin', - CONF_HREF_PIN: 'set_href_pin', - CONF_PIXEL_CLOCK_PIN: 'set_pixel_clock_pin', - CONF_RESET_PIN: 'set_reset_pin', - CONF_POWER_DOWN_PIN: 'set_power_down_pin', - CONF_JPEG_QUALITY: 'set_jpeg_quality', - CONF_VERTICAL_FLIP: 'set_vertical_flip', - CONF_HORIZONTAL_MIRROR: 'set_horizontal_mirror', - CONF_CONTRAST: 'set_contrast', - CONF_BRIGHTNESS: 'set_brightness', - CONF_SATURATION: 'set_saturation', - CONF_TEST_PATTERN: 'set_test_pattern', + CONF_DATA_PINS: "set_data_pins", + CONF_VSYNC_PIN: "set_vsync_pin", + CONF_HREF_PIN: "set_href_pin", + CONF_PIXEL_CLOCK_PIN: "set_pixel_clock_pin", + CONF_RESET_PIN: "set_reset_pin", + CONF_POWER_DOWN_PIN: "set_power_down_pin", + CONF_JPEG_QUALITY: "set_jpeg_quality", + CONF_VERTICAL_FLIP: "set_vertical_flip", + CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", + CONF_AEC2: "set_aec2", + CONF_AE_LEVEL: "set_ae_level", + CONF_AEC_VALUE: "set_aec_value", + CONF_CONTRAST: "set_contrast", + CONF_BRIGHTNESS: "set_brightness", + CONF_SATURATION: "set_saturation", + CONF_TEST_PATTERN: "set_test_pattern", } -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) - yield cg.register_component(var, config) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await setup_entity(var, config) + await cg.register_component(var, config) for key, setter in SETTERS.items(): if key in config: @@ -122,5 +152,10 @@ def to_code(config): cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) cg.add(var.set_frame_size(config[CONF_RESOLUTION])) - cg.add_define('USE_ESP32_CAMERA') - cg.add_build_flag('-DBOARD_HAS_PSRAM') + 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 1d9faf7ea2..a6d4a30c27 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -1,12 +1,15 @@ +#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 { -static const char *TAG = "esp32_camera"; +static const char *const TAG = "esp32_camera"; void ESP32Camera::setup() { global_esp32_camera = this; @@ -23,6 +26,9 @@ void ESP32Camera::setup() { sensor_t *s = esp_camera_sensor_get(); s->set_vflip(s, this->vertical_flip_); s->set_hmirror(s, this->horizontal_mirror_); + s->set_aec2(s, this->aec2_); // 0 = disable , 1 = enable + s->set_ae_level(s, this->ae_level_); // -2 to 2 + s->set_aec_value(s, this->aec_value_); // 0 to 1200 s->set_contrast(s, this->contrast_); s->set_brightness(s, this->brightness_); s->set_saturation(s, this->saturation_); @@ -42,7 +48,10 @@ void ESP32Camera::dump_config() { auto conf = this->config_; ESP_LOGCONFIG(TAG, "ESP32 Camera:"); ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); + ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_)); +#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); @@ -55,9 +64,6 @@ void ESP32Camera::dump_config() { case FRAMESIZE_QQVGA: ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); break; - case FRAMESIZE_QQVGA2: - ESP_LOGCONFIG(TAG, " Resolution: 128x160 (QQVGA2)"); - break; case FRAMESIZE_QCIF: ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); break; @@ -108,9 +114,9 @@ void ESP32Camera::dump_config() { // ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb); // ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain); // ESP_LOGCONFIG(TAG, " Auto Exposure Control: %u", st.aec); - // ESP_LOGCONFIG(TAG, " Auto Exposure Control 2: %u", st.aec2); - // ESP_LOGCONFIG(TAG, " Auto Exposure Level: %d", st.ae_level); - // ESP_LOGCONFIG(TAG, " Auto Exposure Value: %u", st.aec_value); + ESP_LOGCONFIG(TAG, " Auto Exposure Control 2: %u", st.aec2); + ESP_LOGCONFIG(TAG, " Auto Exposure Level: %d", st.ae_level); + ESP_LOGCONFIG(TAG, " Auto Exposure Value: %u", st.aec_value); // ESP_LOGCONFIG(TAG, " AGC: %u", st.agc); // ESP_LOGCONFIG(TAG, " AGC Gain: %u", st.agc_gain); // ESP_LOGCONFIG(TAG, " Gain Ceiling: %u", st.gainceiling); @@ -170,7 +176,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; @@ -183,6 +189,7 @@ ESP32Camera::ESP32Camera(const std::string &name) : Nameable(name) { global_esp32_camera = this; } +ESP32Camera::ESP32Camera() : ESP32Camera("") {} void ESP32Camera::set_data_pins(std::array pins) { this->config_.pin_d0 = pins[0]; this->config_.pin_d1 = pins[1]; @@ -209,9 +216,6 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { case ESP32_CAMERA_SIZE_160X120: this->config_.frame_size = FRAMESIZE_QQVGA; break; - case ESP32_CAMERA_SIZE_128X160: - this->config_.frame_size = FRAMESIZE_QQVGA2; - break; case ESP32_CAMERA_SIZE_176X144: this->config_.frame_size = FRAMESIZE_QCIF; break; @@ -249,6 +253,9 @@ void ESP32Camera::add_image_callback(std::functionvertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } +void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; } +void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; } +void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; } void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; } void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; } void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; } @@ -281,10 +288,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 fd8597d0c1..b20485a0f7 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 { @@ -37,7 +40,6 @@ class CameraImageReader { enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_160X120, // QQVGA - ESP32_CAMERA_SIZE_128X160, // QQVGA2 ESP32_CAMERA_SIZE_176X144, // QCIF ESP32_CAMERA_SIZE_240X176, // HQVGA ESP32_CAMERA_SIZE_320X240, // QVGA @@ -49,9 +51,10 @@ 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); + ESP32Camera(); void set_data_pins(std::array pins); void set_vsync_pin(uint8_t pin); void set_href_pin(uint8_t pin); @@ -64,6 +67,9 @@ class ESP32Camera : public Component, public Nameable { void set_power_down_pin(uint8_t pin); void set_vertical_flip(bool vertical_flip); void set_horizontal_mirror(bool horizontal_mirror); + void set_aec2(bool aec2); + void set_ae_level(int ae_level); + void set_aec_value(uint32_t aec_value); void set_contrast(int contrast); void set_brightness(int brightness); void set_saturation(int saturation); @@ -88,6 +94,9 @@ class ESP32Camera : public Component, public Nameable { camera_config_t config_{}; bool vertical_flip_{true}; bool horizontal_mirror_{true}; + bool aec2_{false}; + int ae_level_{0}; + uint32_t aec_value_{300}; int contrast_{0}; int brightness_{0}; int saturation_{0}; @@ -105,6 +114,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_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py new file mode 100644 index 0000000000..d8afea27b4 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -0,0 +1,28 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_PORT, CONF_MODE + +CODEOWNERS = ["@ayufan"] +DEPENDENCIES = ["esp32_camera"] +MULTI_CONF = True + +esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") +CameraWebServer = esp32_camera_web_server_ns.class_("CameraWebServer", cg.Component) +Mode = esp32_camera_web_server_ns.enum("Mode") + +MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CameraWebServer), + cv.Required(CONF_PORT): cv.port, + cv.Required(CONF_MODE): cv.enum(MODES, upper=True), + }, +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + server = cg.new_Pvariable(config[CONF_ID]) + cg.add(server.set_port(config[CONF_PORT])) + cg.add(server.set_mode(config[CONF_MODE])) + await cg.register_component(server, config) diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp new file mode 100644 index 0000000000..653a274bf4 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -0,0 +1,248 @@ +#ifdef USE_ESP32 + +#include "camera_web_server.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include +#include + +namespace esphome { +namespace esp32_camera_web_server { + +static const int IMAGE_REQUEST_TIMEOUT = 2000; +static const char *const TAG = "esp32_camera_web_server"; + +#define PART_BOUNDARY "123456789000000000000987654321" +#define CONTENT_TYPE "image/jpeg" +#define CONTENT_LENGTH "Content-Length" + +static const char *const STREAM_HEADER = "HTTP/1.0 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "Content-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY "\r\n" + "\r\n" + "--" PART_BOUNDARY "\r\n"; +static const char *const STREAM_ERROR = "Content-Type: text/plain\r\n" + "\r\n" + "No frames send.\r\n" + "--" PART_BOUNDARY "\r\n"; +static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n"; +static const char *const STREAM_BOUNDARY = "\r\n" + "--" PART_BOUNDARY "\r\n"; + +CameraWebServer::CameraWebServer() {} + +CameraWebServer::~CameraWebServer() {} + +void CameraWebServer::setup() { + if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) { + this->mark_failed(); + return; + } + + this->semaphore_ = xSemaphoreCreateBinary(); + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = this->port_; + config.ctrl_port = this->port_; + config.max_open_sockets = 1; + config.backlog_conn = 2; + config.lru_purge_enable = true; + + if (httpd_start(&this->httpd_, &config) != ESP_OK) { + mark_failed(); + return; + } + + httpd_uri_t uri = { + .uri = "/", + .method = HTTP_GET, + .handler = [](struct httpd_req *req) { return ((CameraWebServer *) req->user_ctx)->handler_(req); }, + .user_ctx = this}; + + httpd_register_uri_handler(this->httpd_, &uri); + + esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) { + if (this->running_) { + this->image_ = std::move(image); + xSemaphoreGive(this->semaphore_); + } + }); +} + +void CameraWebServer::on_shutdown() { + this->running_ = false; + this->image_ = nullptr; + httpd_stop(this->httpd_); + this->httpd_ = nullptr; + vSemaphoreDelete(this->semaphore_); + this->semaphore_ = nullptr; +} + +void CameraWebServer::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 Camera Web Server:"); + ESP_LOGCONFIG(TAG, " Port: %d", this->port_); + if (this->mode_ == STREAM) + ESP_LOGCONFIG(TAG, " Mode: stream"); + else + ESP_LOGCONFIG(TAG, " Mode: snapshot"); + + if (this->is_failed()) { + ESP_LOGE(TAG, " Setup Failed"); + } +} + +float CameraWebServer::get_setup_priority() const { return setup_priority::LATE; } + +void CameraWebServer::loop() { + if (!this->running_) { + this->image_ = nullptr; + } +} + +std::shared_ptr CameraWebServer::wait_for_image_() { + std::shared_ptr image; + image.swap(this->image_); + + if (!image) { + // retry as we might still be fetching image + xSemaphoreTake(this->semaphore_, IMAGE_REQUEST_TIMEOUT / portTICK_PERIOD_MS); + image.swap(this->image_); + } + + return image; +} + +esp_err_t CameraWebServer::handler_(struct httpd_req *req) { + esp_err_t res = ESP_FAIL; + + this->image_ = nullptr; + this->running_ = true; + + switch (this->mode_) { + case STREAM: + res = this->streaming_handler_(req); + break; + + case SNAPSHOT: + res = this->snapshot_handler_(req); + break; + } + + this->running_ = false; + this->image_ = nullptr; + return res; +} + +static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) { + int ret; + + while (buf_len > 0) { + ret = httpd_send(r, buf, buf_len); + if (ret < 0) { + return ESP_FAIL; + } + buf += ret; + buf_len -= ret; + } + return ESP_OK; +} + +esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { + esp_err_t res = ESP_OK; + char part_buf[64]; + + // This manually constructs HTTP response to avoid chunked encoding + // which is not supported by some clients + + res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER)); + if (res != ESP_OK) { + ESP_LOGW(TAG, "STREAM: failed to set HTTP header"); + return res; + } + + uint32_t last_frame = millis(); + uint32_t frames = 0; + + while (res == ESP_OK && this->running_) { + if (esp32_camera::global_esp32_camera != nullptr) { + esp32_camera::global_esp32_camera->request_stream(); + } + + auto image = this->wait_for_image_(); + + if (!image) { + ESP_LOGW(TAG, "STREAM: failed to acquire frame"); + res = ESP_FAIL; + } + if (res == ESP_OK) { + size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length()); + res = httpd_send_all(req, part_buf, hlen); + } + if (res == ESP_OK) { + res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length()); + } + if (res == ESP_OK) { + res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); + } + if (res == ESP_OK) { + frames++; + int64_t frame_time = millis() - last_frame; + last_frame = millis(); + + ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time, + 1000.0 / (uint32_t) frame_time); + } + } + + if (!frames) { + res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR)); + } + + ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); + + return res; +} + +esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { + esp_err_t res = ESP_OK; + + if (esp32_camera::global_esp32_camera != nullptr) { + esp32_camera::global_esp32_camera->request_image(); + } + + auto image = this->wait_for_image_(); + + if (!image) { + ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame"); + httpd_resp_send_500(req); + res = ESP_FAIL; + return res; + } + + res = httpd_resp_set_type(req, CONTENT_TYPE); + if (res != ESP_OK) { + ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type"); + return res; + } + + httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); + + if (res == ESP_OK) { + res = httpd_resp_set_hdr(req, CONTENT_LENGTH, esphome::to_string(image->get_data_length()).c_str()); + } + if (res == ESP_OK) { + res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length()); + } + return res; +} + +} // namespace esp32_camera_web_server +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h new file mode 100644 index 0000000000..df30a43ed2 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -0,0 +1,51 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include "esphome/components/esp32_camera/esp32_camera.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +struct httpd_req; + +namespace esphome { +namespace esp32_camera_web_server { + +enum Mode { STREAM, SNAPSHOT }; + +class CameraWebServer : public Component { + public: + CameraWebServer(); + ~CameraWebServer(); + + void setup() override; + void on_shutdown() override; + void dump_config() override; + float get_setup_priority() const override; + void set_port(uint16_t port) { this->port_ = port; } + void set_mode(Mode mode) { this->mode_ = mode; } + void loop() override; + + protected: + std::shared_ptr wait_for_image_(); + esp_err_t handler_(struct httpd_req *req); + esp_err_t streaming_handler_(struct httpd_req *req); + esp_err_t snapshot_handler_(struct httpd_req *req); + + protected: + uint16_t port_{0}; + void *httpd_{nullptr}; + SemaphoreHandle_t semaphore_; + std::shared_ptr image_; + bool running_{false}; + Mode mode_{STREAM}; +}; + +} // namespace esp32_camera_web_server +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_dac/__init__.py b/esphome/components/esp32_dac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_dac/esp32_dac.cpp b/esphome/components/esp32_dac/esp32_dac.cpp new file mode 100644 index 0000000000..7f37e2ce47 --- /dev/null +++ b/esphome/components/esp32_dac/esp32_dac.cpp @@ -0,0 +1,54 @@ +#include "esp32_dac.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP32 + +#ifdef USE_ARDUINO +#include +#endif +#ifdef USE_ESP_IDF +#include +#endif + +namespace esphome { +namespace esp32_dac { + +static const char *const TAG = "esp32_dac"; + +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() { + ESP_LOGCONFIG(TAG, "ESP32 DAC:"); + LOG_PIN(" Pin: ", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +void ESP32DAC::write_state(float state) { + if (this->pin_->is_inverted()) + 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 +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_dac/esp32_dac.h b/esphome/components/esp32_dac/esp32_dac.h new file mode 100644 index 0000000000..0fb1ddebf0 --- /dev/null +++ b/esphome/components/esp32_dac/esp32_dac.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_dac { + +class ESP32DAC : public output::FloatOutput, public Component { + public: + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } + + /// Initialize pin + void setup() override; + void dump_config() override; + /// HARDWARE setup_priority + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + + InternalGPIOPin *pin_; +}; + +} // namespace esp32_dac +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py new file mode 100644 index 0000000000..f119198618 --- /dev/null +++ b/esphome/components/esp32_dac/output.py @@ -0,0 +1,35 @@ +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 + +DEPENDENCIES = ["esp32"] + + +def valid_dac_pin(value): + num = value[CONF_NUMBER] + cv.one_of(25, 26)(num) + return value + + +esp32_dac_ns = cg.esphome_ns.namespace("esp32_dac") +ESP32DAC = esp32_dac_ns.class_("ESP32DAC", output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(ESP32DAC), + cv.Required(CONF_PIN): cv.All( + pins.internal_gpio_output_pin_schema, valid_dac_pin + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/esp32_hall/esp32_hall.cpp b/esphome/components/esp32_hall/esp32_hall.cpp index d7964bcaa6..762497aedc 100644 --- a/esphome/components/esp32_hall/esp32_hall.cpp +++ b/esphome/components/esp32_hall/esp32_hall.cpp @@ -1,16 +1,18 @@ +#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 { -static const char *TAG = "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 ec24f1aab6..a752da2c97 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -1,19 +1,37 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, UNIT_MICROTESLA, ICON_MAGNET +from esphome.const import ( + CONF_ID, + 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_('ESP32HallSensor', sensor.Sensor, cg.PollingComponent) +esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall") +ESP32HallSensor = esp32_hall_ns.class_( + "ESP32HallSensor", sensor.Sensor, cg.PollingComponent +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1).extend({ - cv.GenerateID(): cv.declare_id(ESP32HallSensor), -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(ESP32HallSensor), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py new file mode 100644 index 0000000000..80c53f7c2a --- /dev/null +++ b/esphome/components/esp32_improv/__init__.py @@ -0,0 +1,70 @@ +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_ID + + +AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] +CODEOWNERS = ["@jesserockz"] +CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +DEPENDENCIES = ["wifi", "esp32"] + +CONF_AUTHORIZED_DURATION = "authorized_duration" +CONF_AUTHORIZER = "authorizer" +CONF_BLE_SERVER_ID = "ble_server_id" +CONF_IDENTIFY_DURATION = "identify_duration" +CONF_STATUS_INDICATOR = "status_indicator" +CONF_WIFI_TIMEOUT = "wifi_timeout" + +esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") +ESP32ImprovComponent = esp32_improv_ns.class_( + "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent +) + + +def validate_none_(value): + if value in ("none", "None"): + return None + if cv.boolean(value) is False: + return None + raise cv.Invalid("Must be none") + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), + cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer), + cv.Required(CONF_AUTHORIZER): cv.Any( + validate_none_, cv.use_id(binary_sensor.BinarySensor) + ), + cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), + cv.Optional( + CONF_IDENTIFY_DURATION, default="10s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_AUTHORIZED_DURATION, default="1min" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID]) + cg.add(ble_server.register_service_component(var)) + + cg.add_define("USE_IMPROV") + cg.add_library("esphome/Improv", "1.0.0") + + cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) + cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) + + if CONF_AUTHORIZER in config and config[CONF_AUTHORIZER] is not None: + activator = await cg.get_variable(config[CONF_AUTHORIZER]) + cg.add(var.set_authorizer(activator)) + + if CONF_STATUS_INDICATOR in config: + status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR]) + cg.add(var.set_status_indicator(status_indicator)) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp new file mode 100644 index 0000000000..788e7a9460 --- /dev/null +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -0,0 +1,288 @@ +#include "esp32_improv_component.h" + +#include "esphome/components/esp32_ble/ble.h" +#include "esphome/components/esp32_ble_server/ble_2902.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_improv { + +static const char *const TAG = "esp32_improv.component"; +static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; + +ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } + +void ESP32ImprovComponent::setup() { + this->service_ = global_ble_server->create_service(improv::SERVICE_UUID, true); + this->setup_characteristics(); +} + +void ESP32ImprovComponent::setup_characteristics() { + this->status_ = this->service_->create_characteristic( + improv::STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); + BLEDescriptor *status_descriptor = new BLE2902(); + this->status_->add_descriptor(status_descriptor); + + this->error_ = this->service_->create_characteristic( + improv::ERROR_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); + BLEDescriptor *error_descriptor = new BLE2902(); + this->error_->add_descriptor(error_descriptor); + + this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); + this->rpc_->on_write([this](const std::vector &data) { + if (!data.empty()) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); + BLEDescriptor *rpc_descriptor = new BLE2902(); + this->rpc_->add_descriptor(rpc_descriptor); + + this->rpc_response_ = this->service_->create_characteristic( + improv::RPC_RESULT_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); + BLEDescriptor *rpc_response_descriptor = new BLE2902(); + this->rpc_response_->add_descriptor(rpc_response_descriptor); + + this->capabilities_ = + this->service_->create_characteristic(improv::CAPABILITIES_UUID, BLECharacteristic::PROPERTY_READ); + BLEDescriptor *capabilities_descriptor = new BLE2902(); + this->capabilities_->add_descriptor(capabilities_descriptor); + uint8_t capabilities = 0x00; + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; + this->capabilities_->set_value(capabilities); + this->setup_complete_ = true; +} + +void ESP32ImprovComponent::loop() { + if (!this->incoming_data_.empty()) + this->process_incoming_data_(); + uint32_t now = millis(); + + switch (this->state_) { + case improv::STATE_STOPPED: + if (this->status_indicator_ != nullptr) + this->status_indicator_->turn_off(); + + if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { + if (this->service_->is_running()) { + esp32_ble::global_ble->get_advertising()->start(); + + this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; + ESP_LOGD(TAG, "Service started!"); + } else { + this->service_->start(); + } + } + break; + case improv::STATE_AWAITING_AUTHORIZATION: { + if (this->authorizer_ == nullptr || this->authorizer_->state) { + this->set_state_(improv::STATE_AUTHORIZED); + this->authorized_start_ = now; + } else { + if (this->status_indicator_ != nullptr) { + if (!this->check_identify_()) + this->status_indicator_->turn_on(); + } + } + break; + } + case improv::STATE_AUTHORIZED: { + if (this->authorizer_ != nullptr) { + if (now - this->authorized_start_ > this->authorized_duration_) { + ESP_LOGD(TAG, "Authorization timeout"); + this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + return; + } + } + if (this->status_indicator_ != nullptr) { + if (!this->check_identify_()) { + if ((now % 1000) < 500) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } + } + } + break; + } + case improv::STATE_PROVISIONING: { + if (this->status_indicator_ != nullptr) { + if ((now % 200) < 100) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } + } + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::vector urls = {ESPHOME_MY_LINK}; +#ifdef USE_WEBSERVER + auto ip = wifi::global_wifi_component->wifi_sta_ip(); + std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); + urls.push_back(webserver_url); +#endif + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); + this->send_response_(data); + this->set_timeout("end-service", 1000, [this] { + this->service_->stop(); + this->set_state_(improv::STATE_STOPPED); + }); + } + break; + } + case improv::STATE_PROVISIONED: { + this->incoming_data_.clear(); + if (this->status_indicator_ != nullptr) + this->status_indicator_->turn_off(); + break; + } + } +} + +bool ESP32ImprovComponent::check_identify_() { + uint32_t now = millis(); + + bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_; + + if (identify) { + uint32_t time = now % 1000; + if (time < 600 && time % 200 < 100) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } + } + return identify; +} + +void ESP32ImprovComponent::set_state_(improv::State state) { + ESP_LOGV(TAG, "Setting state: %d", state); + this->state_ = 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) + this->status_->notify(); + } +} + +void ESP32ImprovComponent::set_error_(improv::Error error) { + if (error != improv::ERROR_NONE) + ESP_LOGE(TAG, "Error: %d", 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) + this->error_->notify(); + } +} + +void ESP32ImprovComponent::send_response_(std::vector &response) { + this->rpc_response_->set_value(response); + if (this->state_ != improv::STATE_STOPPED) + this->rpc_response_->notify(); +} + +void ESP32ImprovComponent::start() { + if (this->state_ != improv::STATE_STOPPED) + return; + + ESP_LOGD(TAG, "Setting Improv to start"); + this->should_start_ = true; +} + +void ESP32ImprovComponent::stop() { + this->set_timeout("end-service", 1000, [this] { + this->service_->stop(); + this->set_state_(improv::STATE_STOPPED); + }); +} + +float ESP32ImprovComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + +void ESP32ImprovComponent::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 Improv:"); + LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_); + ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr)); +} + +void ESP32ImprovComponent::process_incoming_data_() { + uint8_t length = this->incoming_data_[1]; + + ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); + if (this->incoming_data_.size() - 3 == length) { + this->set_error_(improv::ERROR_NONE); + improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); + switch (command.command) { + case improv::BAD_CHECKSUM: + ESP_LOGW(TAG, "Error decoding Improv payload"); + this->set_error_(improv::ERROR_INVALID_RPC); + this->incoming_data_.clear(); + break; + case improv::WIFI_SETTINGS: { + if (this->state_ != improv::STATE_AUTHORIZED) { + ESP_LOGW(TAG, "Settings received, but not authorized"); + this->set_error_(improv::ERROR_NOT_AUTHORIZED); + this->incoming_data_.clear(); + return; + } + wifi::WiFiAP sta{}; + sta.set_ssid(command.ssid); + sta.set_password(command.password); + this->connecting_sta_ = sta; + + wifi::global_wifi_component->set_sta(sta); + wifi::global_wifi_component->start_scanning(); + this->set_state_(improv::STATE_PROVISIONING); + ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + command.password.c_str()); + + auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); + this->set_timeout("wifi-connect-timeout", 30000, f); + this->incoming_data_.clear(); + break; + } + case improv::IDENTIFY: + this->incoming_data_.clear(); + this->identify_start_ = millis(); + break; + default: + ESP_LOGW(TAG, "Unknown Improv payload"); + this->set_error_(improv::ERROR_UNKNOWN_RPC); + this->incoming_data_.clear(); + } + } else if (this->incoming_data_.size() - 2 > length) { + ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer..."); + this->incoming_data_.clear(); + } else { + ESP_LOGV(TAG, "Waiting for split data packets..."); + } +} + +void ESP32ImprovComponent::on_wifi_connect_timeout_() { + this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); + this->set_state_(improv::STATE_AUTHORIZED); + if (this->authorizer_ != nullptr) + this->authorized_start_ = millis(); + ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + wifi::global_wifi_component->clear_sta(); +} + +void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); }; + +ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esp32_improv +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h new file mode 100644 index 0000000000..45639f2f63 --- /dev/null +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_server/ble_characteristic.h" +#include "esphome/components/esp32_ble_server/ble_server.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/wifi/wifi_component.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace esp32_improv { + +using namespace esp32_ble_server; + +class ESP32ImprovComponent : public Component, public BLEServiceComponent { + public: + ESP32ImprovComponent(); + void dump_config() override; + void loop() override; + void setup() override; + void setup_characteristics(); + void on_client_disconnect() override; + + float get_setup_priority() const override; + void start() override; + void stop() override; + bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + + void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; } + void set_status_indicator(output::BinaryOutput *status_indicator) { this->status_indicator_ = status_indicator; } + void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; } + void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; } + + protected: + bool should_start_{false}; + bool setup_complete_{false}; + + uint32_t identify_start_{0}; + uint32_t identify_duration_; + uint32_t authorized_start_{0}; + uint32_t authorized_duration_; + + std::vector incoming_data_; + wifi::WiFiAP connecting_sta_; + + std::shared_ptr service_; + BLECharacteristic *status_; + BLECharacteristic *error_; + BLECharacteristic *rpc_; + BLECharacteristic *rpc_response_; + BLECharacteristic *capabilities_; + + binary_sensor::BinarySensor *authorizer_{nullptr}; + output::BinaryOutput *status_indicator_{nullptr}; + + improv::State state_{improv::STATE_STOPPED}; + improv::Error error_state_{improv::ERROR_NONE}; + + void set_state_(improv::State state); + void set_error_(improv::Error error); + 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 +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 3bdc988fcc..cdf6aa3abd 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -1,15 +1,22 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_HIGH_VOLTAGE_REFERENCE, CONF_ID, CONF_IIR_FILTER, \ - CONF_LOW_VOLTAGE_REFERENCE, CONF_MEASUREMENT_DURATION, CONF_SETUP_MODE, CONF_SLEEP_DURATION, \ - CONF_VOLTAGE_ATTENUATION, ESP_PLATFORM_ESP32 +from esphome.const import ( + CONF_HIGH_VOLTAGE_REFERENCE, + CONF_ID, + CONF_IIR_FILTER, + CONF_LOW_VOLTAGE_REFERENCE, + CONF_MEASUREMENT_DURATION, + CONF_SETUP_MODE, + CONF_SLEEP_DURATION, + CONF_VOLTAGE_ATTENUATION, +) from esphome.core import TimePeriod -AUTO_LOAD = ['binary_sensor'] -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +AUTO_LOAD = ["binary_sensor"] +DEPENDENCIES = ["esp32"] -esp32_touch_ns = cg.esphome_ns.namespace('esp32_touch') -ESP32TouchComponent = esp32_touch_ns.class_('ESP32TouchComponent', cg.Component) +esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") +ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) def validate_voltage(values): @@ -17,51 +24,61 @@ def validate_voltage(values): if isinstance(value, float) and value.is_integer(): value = int(value) value = cv.string(value) - if not value.endswith('V'): - value += 'V' + if not value.endswith("V"): + value += "V" return cv.one_of(*values)(value) return validator LOW_VOLTAGE_REFERENCE = { - '0.5V': cg.global_ns.TOUCH_LVOLT_0V5, - '0.6V': cg.global_ns.TOUCH_LVOLT_0V6, - '0.7V': cg.global_ns.TOUCH_LVOLT_0V7, - '0.8V': cg.global_ns.TOUCH_LVOLT_0V8, + "0.5V": cg.global_ns.TOUCH_LVOLT_0V5, + "0.6V": cg.global_ns.TOUCH_LVOLT_0V6, + "0.7V": cg.global_ns.TOUCH_LVOLT_0V7, + "0.8V": cg.global_ns.TOUCH_LVOLT_0V8, } HIGH_VOLTAGE_REFERENCE = { - '2.4V': cg.global_ns.TOUCH_HVOLT_2V4, - '2.5V': cg.global_ns.TOUCH_HVOLT_2V5, - '2.6V': cg.global_ns.TOUCH_HVOLT_2V6, - '2.7V': cg.global_ns.TOUCH_HVOLT_2V7, + "2.4V": cg.global_ns.TOUCH_HVOLT_2V4, + "2.5V": cg.global_ns.TOUCH_HVOLT_2V5, + "2.6V": cg.global_ns.TOUCH_HVOLT_2V6, + "2.7V": cg.global_ns.TOUCH_HVOLT_2V7, } VOLTAGE_ATTENUATION = { - '1.5V': cg.global_ns.TOUCH_HVOLT_ATTEN_1V5, - '1V': cg.global_ns.TOUCH_HVOLT_ATTEN_1V, - '0.5V': cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, - '0V': cg.global_ns.TOUCH_HVOLT_ATTEN_0V, + "1.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V5, + "1V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V, + "0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, + "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, } -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(ESP32TouchComponent), - cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, - cv.Optional(CONF_IIR_FILTER, default='0ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SLEEP_DURATION, default='27306us'): - cv.All(cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906))), - cv.Optional(CONF_MEASUREMENT_DURATION, default='8192us'): - cv.All(cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=8192))), - cv.Optional(CONF_LOW_VOLTAGE_REFERENCE, default='0.5V'): - validate_voltage(LOW_VOLTAGE_REFERENCE), - cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default='2.7V'): - validate_voltage(HIGH_VOLTAGE_REFERENCE), - cv.Optional(CONF_VOLTAGE_ATTENUATION, default='0V'): validate_voltage(VOLTAGE_ATTENUATION), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32TouchComponent), + cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, + cv.Optional( + CONF_IIR_FILTER, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SLEEP_DURATION, default="27306us"): cv.All( + cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906)) + ), + cv.Optional(CONF_MEASUREMENT_DURATION, default="8192us"): cv.All( + cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=8192)) + ), + cv.Optional(CONF_LOW_VOLTAGE_REFERENCE, default="0.5V"): validate_voltage( + LOW_VOLTAGE_REFERENCE + ), + cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( + HIGH_VOLTAGE_REFERENCE + ), + cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( + VOLTAGE_ATTENUATION + ), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): touch = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(touch, config) + await cg.register_component(touch, config) cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE])) cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) @@ -69,12 +86,23 @@ def to_code(config): sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15)) cg.add(touch.set_sleep_duration(sleep_duration)) - measurement_duration = int(round(config[CONF_MEASUREMENT_DURATION].total_microseconds * - 7.99987793)) + measurement_duration = int( + round(config[CONF_MEASUREMENT_DURATION].total_microseconds * 7.99987793) + ) cg.add(touch.set_measurement_duration(measurement_duration)) - cg.add(touch.set_low_voltage_reference( - LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]])) - cg.add(touch.set_high_voltage_reference( - HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]])) - cg.add(touch.set_voltage_attenuation(VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]])) + cg.add( + touch.set_low_voltage_reference( + LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + ) + ) + cg.add( + touch.set_high_voltage_reference( + HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + ) + ) + cg.add( + touch.set_voltage_attenuation( + VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]] + ) + ) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index 5142879a04..bd3e06545d 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -1,14 +1,18 @@ 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.const import ( + CONF_PIN, + CONF_THRESHOLD, + CONF_ID, +) +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_ESP32_TOUCH_ID = "esp32_touch_id" +CONF_WAKEUP_THRESHOLD = "wakeup_threshold" TOUCH_PADS = { 4: cg.global_ns.TOUCH_PAD_NUM0, @@ -25,25 +29,34 @@ 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 -ESP32TouchBinarySensor = esp32_touch_ns.class_('ESP32TouchBinarySensor', binary_sensor.BinarySensor) +ESP32TouchBinarySensor = esp32_touch_ns.class_( + "ESP32TouchBinarySensor", binary_sensor.BinarySensor +) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(ESP32TouchBinarySensor), - cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_id(ESP32TouchComponent), - cv.Required(CONF_PIN): validate_touch_pad, - cv.Required(CONF_THRESHOLD): cv.uint16_t, -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ESP32TouchBinarySensor), + 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, + } +) -def to_code(config): - hub = yield 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]) - yield binary_sensor.register_binary_sensor(var, config) +async def to_code(config): + hub = await cg.get_variable(config[CONF_ESP32_TOUCH_ID]) + var = cg.new_Pvariable( + config[CONF_ID], + 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 ce0028159e..b225ae1a8a 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -1,12 +1,14 @@ -#include "esp32_touch.h" -#include "esphome/core/log.h" +#ifdef USE_ESP32 -#ifdef ARDUINO_ARCH_ESP32 +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace esp32_touch { -static const char *TAG = "esp32_touch"; +static const char *const TAG = "esp32_touch"; void ESP32TouchComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 Touch Hub..."); @@ -92,7 +94,6 @@ void ESP32TouchComponent::dump_config() { if (this->iir_filter_enabled_()) { ESP_LOGCONFIG(TAG, " IIR Filter: %ums", this->iir_filter_); - touch_pad_filter_start(this->iir_filter_); } else { ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); } @@ -124,6 +125,8 @@ void ESP32TouchComponent::loop() { if (should_print) { ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); } + + App.feed_wdt(); } if (should_print) { @@ -133,15 +136,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..34c792499d --- /dev/null +++ b/esphome/components/esp8266/__init__.py @@ -0,0 +1,212 @@ +import logging + +from esphome.const import ( + CONF_BOARD, + CONF_BOARD_FLASH_MODE, + CONF_FRAMEWORK, + CONF_SOURCE, + 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] + ) + 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, 3) +# for arduino 3 framework versions +ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) + + +def _arduino_check_versions(value): + value = value.copy() + lookups = { + "dev": (cv.Version(3, 0, 2), "https://github.com/esp8266/Arduino.git"), + "latest": (cv.Version(3, 0, 2), None), + "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), + } + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] + else: + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) + + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) + + platform_version = value.get(CONF_PLATFORM_VERSION) + if platform_version is None: + if version >= cv.Version(3, 0, 0): + platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) + elif version >= cv.Version(2, 5, 0): + platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION)) + else: + platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) + value[CONF_PLATFORM_VERSION] = platform_version + + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected Arduino framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." + ) + + return value + + +def _parse_platform_version(value): + try: + # if platform version is a valid version constraint, prefix the default package + cv.platformio_version_constraint(value) + return f"platformio/espressif8266 @ {value}" + except cv.Invalid: + return value + + +CONF_PLATFORM_VERSION = "platform_version" +ARDUINO_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + } + ), + _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("lib_ldf_mode", "off") + + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_ESP8266") + cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + cg.add_define("ESPHOME_VARIANT", "ESP8266") + + 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", conf[CONF_PLATFORM_VERSION]) + cg.add_platformio_option( + "platform_packages", + [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], + ) + + # 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..410e934615 --- /dev/null +++ b/esphome/components/esp8266/boards.py @@ -0,0 +1,208 @@ +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}, +} 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..828d71a3bd --- /dev/null +++ b/esphome/components/esp8266/core.cpp @@ -0,0 +1,60 @@ +#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) { delay_microseconds_safe(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 arch_init() {} +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 IRAM_ATTR HOT 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..a24f217756 --- /dev/null +++ b/esphome/components/esp8266/gpio.cpp @@ -0,0 +1,109 @@ +#ifdef USE_ESP8266 + +#include "gpio.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace esp8266 { + +static const char *const TAG = "esp8266"; + +static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { + if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + if (pin == 16) { + // GPIO16 doesn't have a pullup, so pinMode would fail. + // However, sometimes this method is called with pullup mode anyway + // for example from dallas one_wire. For those cases convert this + // to a INPUT mode. + return INPUT; + } + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN_16; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } else { + return 0; + } +} + +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) { + pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT +} + +std::string ESP8266GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +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); +} +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT +} + +} // 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..fa5c94dff5 --- /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 == 16: + raise cv.Invalid( + "GPIO Pin 16 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..a8f8bd0d41 --- /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 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 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 96290871e0..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 @@ -11,7 +14,7 @@ namespace esphome { namespace esp8266_pwm { -static const char *TAG = "esp8266_pwm"; +static const char *const TAG = "esp8266_pwm"; void ESP8266PWM::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP8266 PWM Output..."); @@ -37,6 +40,11 @@ void HOT ESP8266PWM::write_state(float state) { uint32_t duty_off = total_time_us - duty_on; if (duty_on == 0) { + // This is a hacky fix for servos: Servo PWM high time is maximum 2.4ms by default + // The frequency check is to affect this fix for servos mostly as the frequency is usually 50-300 hz + if (this->pin_->digital_read() && 50 <= this->frequency_ && this->frequency_ <= 300) { + delay(3); + } stopWaveform(this->pin_->get_pin()); this->pin_->digital_write(this->pin_->is_inverted()); } else if (duty_off == 0) { @@ -49,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 b6839985b0..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,10 +12,10 @@ 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) { + void update_frequency(float frequency) override { this->set_frequency(frequency); this->write_state(this->last_output_); } @@ -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}; @@ -43,9 +45,10 @@ template class SetFrequencyAction : public Action { this->parent_->update_frequency(freq); } - protected: ESP8266PWM *parent_; }; } // namespace esp8266_pwm } // namespace esphome + +#endif diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index e973490525..3d52e5af16 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -2,9 +2,14 @@ from esphome import pins, automation from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NUMBER, CONF_PIN, ESP_PLATFORM_ESP8266 +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_NUMBER, + CONF_PIN, +) -ESP_PLATFORMS = [ESP_PLATFORM_ESP8266] +DEPENDENCIES = ["esp8266"] def valid_pwm_pin(value): @@ -13,36 +18,46 @@ def valid_pwm_pin(value): return value -esp8266_pwm_ns = cg.esphome_ns.namespace('esp8266_pwm') -ESP8266PWM = esp8266_pwm_ns.class_('ESP8266PWM', output.FloatOutput, cg.Component) -SetFrequencyAction = esp8266_pwm_ns.class_('SetFrequencyAction', automation.Action) +esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm") +ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component) +SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(ESP8266PWM), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_output_pin_schema, valid_pwm_pin), - cv.Optional(CONF_FREQUENCY, default='1kHz'): validate_frequency, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(ESP8266PWM), + cv.Required(CONF_PIN): cv.All( + pins.internal_gpio_output_pin_schema, valid_pwm_pin + ), + cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield output.register_output(var, config) + await cg.register_component(var, config) + await output.register_output(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) cg.add(var.set_frequency(config[CONF_FREQUENCY])) -@automation.register_action('output.esp8266_pwm.set_frequency', SetFrequencyAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(ESP8266PWM), - cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), -})) -def esp8266_set_frequency_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "output.esp8266_pwm.set_frequency", + SetFrequencyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(ESP8266PWM), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), + } + ), +) +async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) cg.add(var.set_frequency(template_)) - yield var + return var diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ce7422d05b..bbf64a3cd1 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,50 +1,62 @@ from esphome import pins import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_DOMAIN, CONF_ID, CONF_MANUAL_IP, CONF_STATIC_IP, CONF_TYPE, \ - CONF_USE_ADDRESS, ESP_PLATFORM_ESP32, CONF_GATEWAY, CONF_SUBNET, CONF_DNS1, CONF_DNS2 +from esphome.const import ( + CONF_DOMAIN, + CONF_ID, + CONF_MANUAL_IP, + CONF_STATIC_IP, + CONF_TYPE, + CONF_USE_ADDRESS, + 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] -AUTO_LOAD = ['network'] +CONFLICTS_WITH = ["wifi"] +DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["network"] -ethernet_ns = cg.esphome_ns.namespace('ethernet') -CONF_PHY_ADDR = 'phy_addr' -CONF_MDC_PIN = 'mdc_pin' -CONF_MDIO_PIN = 'mdio_pin' -CONF_CLK_MODE = 'clk_mode' -CONF_POWER_PIN = 'power_pin' +ethernet_ns = cg.esphome_ns.namespace("ethernet") +CONF_PHY_ADDR = "phy_addr" +CONF_MDC_PIN = "mdc_pin" +CONF_MDIO_PIN = "mdio_pin" +CONF_CLK_MODE = "clk_mode" +CONF_POWER_PIN = "power_pin" -EthernetType = ethernet_ns.enum('EthernetType') +EthernetType = ethernet_ns.enum("EthernetType") ETHERNET_TYPES = { - 'LAN8720': EthernetType.ETHERNET_TYPE_LAN8720, - 'TLK110': EthernetType.ETHERNET_TYPE_TLK110, + "LAN8720": EthernetType.ETHERNET_TYPE_LAN8720, + "TLK110": EthernetType.ETHERNET_TYPE_TLK110, } -eth_clock_mode_t = cg.global_ns.enum('eth_clock_mode_t') +eth_clock_mode_t = cg.global_ns.enum("eth_clock_mode_t") CLK_MODES = { - 'GPIO0_IN': eth_clock_mode_t.ETH_CLOCK_GPIO0_IN, - 'GPIO0_OUT': eth_clock_mode_t.ETH_CLOCK_GPIO0_OUT, - 'GPIO16_OUT': eth_clock_mode_t.ETH_CLOCK_GPIO16_OUT, - 'GPIO17_OUT': eth_clock_mode_t.ETH_CLOCK_GPIO17_OUT, + "GPIO0_IN": eth_clock_mode_t.ETH_CLOCK_GPIO0_IN, + "GPIO0_OUT": eth_clock_mode_t.ETH_CLOCK_GPIO0_OUT, + "GPIO16_OUT": eth_clock_mode_t.ETH_CLOCK_GPIO16_OUT, + "GPIO17_OUT": eth_clock_mode_t.ETH_CLOCK_GPIO17_OUT, } -MANUAL_IP_SCHEMA = cv.Schema({ - cv.Required(CONF_STATIC_IP): cv.ipv4, - cv.Required(CONF_GATEWAY): cv.ipv4, - cv.Required(CONF_SUBNET): cv.ipv4, - cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, - cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, -}) +MANUAL_IP_SCHEMA = cv.Schema( + { + cv.Required(CONF_STATIC_IP): cv.ipv4, + cv.Required(CONF_GATEWAY): cv.ipv4, + cv.Required(CONF_SUBNET): cv.ipv4, + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, + } +) -EthernetComponent = ethernet_ns.class_('EthernetComponent', cg.Component) -IPAddress = cg.global_ns.class_('IPAddress') -ManualIP = ethernet_ns.struct('ManualIP') +EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component) +ManualIP = ethernet_ns.struct("ManualIP") -def validate(config): +def _validate(config): if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) @@ -54,37 +66,47 @@ def validate(config): return config -CONFIG_SCHEMA = cv.All(cv.Schema({ - 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.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_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"), -}).extend(cv.COMPONENT_SCHEMA), validate) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EthernetComponent), + cv.Required(CONF_TYPE): cv.enum(ETHERNET_TYPES, upper=True), + cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum( + 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_DOMAIN, default=".local"): cv.domain_name, + cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional("enable_mdns"): cv.invalid( + "This option has been removed. Please use the [disabled] option under the " + "new mdns component instead." + ), + } + ).extend(cv.COMPONENT_SCHEMA), + _validate, + cv.only_with_arduino, +) def manual_ip(config): return cg.StructInitializer( ManualIP, - ('static_ip', IPAddress(*config[CONF_STATIC_IP].args)), - ('gateway', IPAddress(*config[CONF_GATEWAY].args)), - ('subnet', IPAddress(*config[CONF_SUBNET].args)), - ('dns1', IPAddress(*config[CONF_DNS1].args)), - ('dns2', IPAddress(*config[CONF_DNS2].args)), + ("static_ip", IPAddress(*config[CONF_STATIC_IP].args)), + ("gateway", IPAddress(*config[CONF_GATEWAY].args)), + ("subnet", IPAddress(*config[CONF_SUBNET].args)), + ("dns1", IPAddress(*config[CONF_DNS1].args)), + ("dns2", IPAddress(*config[CONF_DNS2].args)), ) @coroutine_with_priority(60.0) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) @@ -94,10 +116,13 @@ def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_POWER_PIN in config: - pin = yield cg.gpio_pin_expression(config[CONF_POWER_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_POWER_PIN]) cg.add(var.set_power_pin(pin)) if CONF_MANUAL_IP in config: cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) - cg.add_define('USE_ETHERNET') + cg.add_define("USE_ETHERNET") + + 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 d5548fc377..384a31ed2f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -3,22 +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 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 *TAG = "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() { @@ -31,36 +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; + } + } - network_setup_mdns(); + 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->last_connected_ > 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:"); @@ -72,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) { @@ -84,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: @@ -107,63 +162,16 @@ 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; + err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); + if (err != ERR_OK) { + ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); } - 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()); - tcpip_adapter_ip_info_t info; if (this->manual_ip_.has_value()) { info.ip.addr = static_cast(this->manual_ip_->static_ip); @@ -176,7 +184,9 @@ void EthernetComponent::start_connect_() { } err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_ETH); - ESPHL_ERROR_CHECK(err, "DHCPC stop error"); + if (err != ESP_ERR_TCPIP_ADAPTER_DHCP_ALREADY_STOPPED) { + ESPHL_ERROR_CHECK(err, "DHCPC stop error"); + } err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_ETH, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); @@ -203,35 +213,42 @@ 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()); - ip_addr_t dns_ip = dns_getserver(0); - ESP_LOGCONFIG(TAG, " DNS1: %s", IPAddress(dns_ip.u_addr.ip4.addr).toString().c_str()); - dns_ip = dns_getserver(1); - ESP_LOGCONFIG(TAG, " DNS2: %s", IPAddress(dns_ip.u_addr.ip4.addr).toString().c_str()); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 4) + const ip_addr_t *dns_ip1 = dns_getserver(0); + const ip_addr_t *dns_ip2 = dns_getserver(1); +#else + ip_addr_t tmp_ip1 = dns_getserver(0); + const ip_addr_t *dns_ip1 = &tmp_ip1; + ip_addr_t tmp_ip2 = dns_getserver(1); + const ip_addr_t *dns_ip2 = &tmp_ip2; +#endif + 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; } @@ -239,7 +256,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"; @@ -251,4 +268,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/__init__.py b/esphome/components/exposure_notifications/__init__.py new file mode 100644 index 0000000000..9c28552267 --- /dev/null +++ b/esphome/components/exposure_notifications/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_TRIGGER_ID + +CODEOWNERS = ["@OttoWinter"] +DEPENDENCIES = ["esp32_ble_tracker"] + +exposure_notifications_ns = cg.esphome_ns.namespace("exposure_notifications") +ExposureNotification = exposure_notifications_ns.struct("ExposureNotification") +ExposureNotificationTrigger = exposure_notifications_ns.class_( + "ExposureNotificationTrigger", + esp32_ble_tracker.ESPBTDeviceListener, + automation.Trigger.template(ExposureNotification), +) + +CONF_ON_EXPOSURE_NOTIFICATION = "on_exposure_notification" + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ON_EXPOSURE_NOTIFICATION): automation.validate_automation( + cv.Schema( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ExposureNotificationTrigger + ), + } + ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + ), + } +) + + +async def to_code(config): + for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await automation.build_automation(trigger, [(ExposureNotification, "x")], conf) + await esp32_ble_tracker.register_ble_device(trigger, conf) diff --git a/esphome/components/exposure_notifications/exposure_notifications.cpp b/esphome/components/exposure_notifications/exposure_notifications.cpp new file mode 100644 index 0000000000..3083cf429c --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.cpp @@ -0,0 +1,49 @@ +#include "exposure_notifications.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace exposure_notifications { + +using namespace esp32_ble_tracker; + +static const char *const TAG = "exposure_notifications"; + +bool ExposureNotificationTrigger::parse_device(const ESPBTDevice &device) { + // See also https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf + if (device.get_service_uuids().size() != 1) + return false; + + // Exposure notifications have Service UUID FD 6F + ESPBTUUID uuid = device.get_service_uuids()[0]; + // constant service identifier + const ESPBTUUID expected_uuid = ESPBTUUID::from_uint16(0xFD6F); + if (uuid != expected_uuid) + return false; + if (device.get_service_datas().size() != 1) + return false; + + // The service data should be 20 bytes + // First 16 bytes are the rolling proximity identifier (RPI) + // Then 4 bytes of encrypted metadata follow which can be used to get the transmit power level. + ServiceData service_data = device.get_service_datas()[0]; + if (service_data.uuid != expected_uuid) + return false; + auto data = service_data.data; + if (data.size() != 20) + return false; + ExposureNotification notification{}; + memcpy(¬ification.address[0], device.address(), 6); + memcpy(¬ification.rolling_proximity_identifier[0], &data[0], 16); + memcpy(¬ification.associated_encrypted_metadata[0], &data[16], 4); + notification.rssi = device.get_rssi(); + this->trigger(notification); + return true; +} + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/exposure_notifications/exposure_notifications.h b/esphome/components/exposure_notifications/exposure_notifications.h new file mode 100644 index 0000000000..f7383c28d9 --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace exposure_notifications { + +struct ExposureNotification { + std::array address; + int rssi; + std::array rolling_proximity_identifier; + std::array associated_encrypted_metadata; +}; + +class ExposureNotificationTrigger : public Trigger, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py new file mode 100644 index 0000000000..d0153f6104 --- /dev/null +++ b/esphome/components/external_components/__init__.py @@ -0,0 +1,165 @@ +import re +import logging +from pathlib import Path + +import esphome.config_validation as cv +from esphome.const import ( + CONF_COMPONENTS, + CONF_REF, + CONF_REFRESH, + CONF_SOURCE, + CONF_URL, + CONF_TYPE, + CONF_EXTERNAL_COMPONENTS, + CONF_PATH, + CONF_USERNAME, + CONF_PASSWORD, +) +from esphome.core import CORE +from esphome import git, loader + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = CONF_EXTERNAL_COMPONENTS + +TYPE_GIT = "git" +TYPE_LOCAL = "local" + + +GIT_SCHEMA = { + cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_REF): cv.git_ref, + cv.Optional(CONF_USERNAME): cv.string, + cv.Optional(CONF_PASSWORD): cv.string, +} +LOCAL_SCHEMA = { + cv.Required(CONF_PATH): cv.directory, +} + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + try: + return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except cv.Invalid: + pass + # Regex for GitHub repo name with optional branch/tag + # Note: git allows other branch/tag names as well, but never seen them used before + m = re.match( + r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))", + value, + ) + if m is None: + raise cv.Invalid( + "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" + ) + if m.group(4): + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: "https://github.com/esphome/esphome.git", + CONF_REF: f"pull/{m.group(4)}/head", + } + else: + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + } + if m.group(3): + conf[CONF_REF] = m.group(3) + + return SOURCE_SCHEMA(conf) + + +SOURCE_SCHEMA = cv.Any( + validate_source_shorthand, + cv.typed_schema( + { + TYPE_GIT: cv.Schema(GIT_SCHEMA), + TYPE_LOCAL: cv.Schema(LOCAL_SCHEMA), + } + ), +) + + +CONFIG_SCHEMA = cv.ensure_list( + { + cv.Required(CONF_SOURCE): SOURCE_SCHEMA, + 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) + ), + } +) + + +async def to_code(config): + pass + + +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, + username=config.get(CONF_USERNAME), + password=config.get(CONF_PASSWORD), + ) + + 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" + ) + + return components_dir + + +def _process_single_config(config: dict): + conf = config[CONF_SOURCE] + if conf[CONF_TYPE] == TYPE_GIT: + with cv.prepend_path([CONF_SOURCE]): + components_dir = _process_git_config( + config[CONF_SOURCE], config[CONF_REFRESH] + ) + elif conf[CONF_TYPE] == TYPE_LOCAL: + components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) + else: + raise NotImplementedError() + + if config[CONF_COMPONENTS] == "all": + num_components = len(list(components_dir.glob("*/__init__.py"))) + if num_components > 100: + # Prevent accidentally including all components from an esphome fork/branch + # In this case force the user to manually specify which components they want to include + raise cv.Invalid( + "This source is an ESPHome fork or branch. Please manually specify the components you want to import using the 'components' key", + [CONF_COMPONENTS], + ) + allowed_components = None + else: + for i, name in enumerate(config[CONF_COMPONENTS]): + expected = components_dir / name / "__init__.py" + if not expected.is_file(): + raise cv.Invalid( + f"Could not find __init__.py file for component {name}. Please check the component is defined by this source (search path: {expected})", + [CONF_COMPONENTS, i], + ) + allowed_components = config[CONF_COMPONENTS] + + loader.install_meta_finder(components_dir, allowed_components=allowed_components) + + +def do_external_components_pass(config: dict) -> None: + conf = config.get(DOMAIN) + if conf is None: + return + with cv.prepend_path(DOMAIN): + conf = CONFIG_SCHEMA(conf) + for i, c in enumerate(conf): + with cv.prepend_path(i): + _process_single_config(c) diff --git a/esphome/components/ezo/__init__.py b/esphome/components/ezo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp new file mode 100644 index 0000000000..2ee5782ff6 --- /dev/null +++ b/esphome/components/ezo/ezo.cpp @@ -0,0 +1,92 @@ +#include "ezo.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ezo { + +static const char *const TAG = "ezo.sensor"; + +static const uint16_t EZO_STATE_WAIT = 1; +static const uint16_t EZO_STATE_SEND_TEMP = 2; +static const uint16_t EZO_STATE_WAIT_TEMP = 4; + +void EZOSensor::dump_config() { + LOG_SENSOR("", "EZO", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) + ESP_LOGE(TAG, "Communication with EZO circuit failed!"); + LOG_UPDATE_INTERVAL(this); +} + +void EZOSensor::update() { + if (this->state_ & EZO_STATE_WAIT) { + ESP_LOGE(TAG, "update overrun, still waiting for previous response"); + return; + } + uint8_t c = 'R'; + this->write(&c, 1); + this->state_ |= EZO_STATE_WAIT; + this->start_time_ = millis(); + this->wait_time_ = 900; +} + +void EZOSensor::loop() { + uint8_t buf[21]; + 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(buf, len); + this->state_ = EZO_STATE_WAIT | EZO_STATE_WAIT_TEMP; + this->start_time_ = millis(); + this->wait_time_ = 300; + } + return; + } + if (millis() - this->start_time_ < this->wait_time_) + return; + buf[0] = 0; + if (!this->read_bytes_raw(buf, 20)) { + ESP_LOGE(TAG, "read error"); + this->state_ = 0; + return; + } + switch (buf[0]) { + case 1: + break; + case 2: + ESP_LOGE(TAG, "device returned a syntax error"); + break; + case 254: + return; // keep waiting + case 255: + ESP_LOGE(TAG, "device returned no data"); + break; + default: + ESP_LOGE(TAG, "device returned an unknown response: %d", buf[0]); + break; + } + if (this->state_ & EZO_STATE_WAIT_TEMP) { + this->state_ = 0; + return; + } + this->state_ &= ~EZO_STATE_WAIT; + if (buf[0] != 1) + return; + + // some sensors return multiple comma-separated values, terminate string after first one + for (size_t i = 1; i < sizeof(buf) - 1; i++) + if (buf[i] == ',') + buf[i] = '\0'; + + float val = parse_number((char *) &buf[1]).value_or(0); + this->publish_state(val); +} + +void EZOSensor::set_tempcomp_value(float temp) { + this->tempcomp_ = temp; + this->state_ |= EZO_STATE_SEND_TEMP; +} + +} // namespace ezo +} // namespace esphome diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h new file mode 100644 index 0000000000..d46d193ae7 --- /dev/null +++ b/esphome/components/ezo/ezo.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ezo { + +/// This class implements support for the EZO circuits in i2c mode +class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void loop() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void set_tempcomp_value(float temp); + + protected: + uint32_t start_time_ = 0; + uint32_t wait_time_ = 0; + uint16_t state_ = 0; + float tempcomp_; +}; + +} // namespace ezo +} // namespace esphome diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py new file mode 100644 index 0000000000..09b36b7135 --- /dev/null +++ b/esphome/components/ezo/sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID + +CODEOWNERS = ["@ssieb"] + +DEPENDENCIES = ["i2c"] + +ezo_ns = cg.esphome_ns.namespace("ezo") + +EZOSensor = ezo_ns.class_( + "EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EZOSensor), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(None)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 50fdb1c2c9..52bec3b5b6 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -3,112 +3,233 @@ import esphome.config_validation as cv from esphome import automation 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_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_NAME -from esphome.core import CORE, coroutine, coroutine_with_priority +from esphome.const import ( + CONF_ID, + 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_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) -MakeFan = cg.Application.struct('MakeFan') +fan_ns = cg.esphome_ns.namespace("fan") +FanState = fan_ns.class_("FanState", cg.EntityBase, cg.Component) +MakeFan = cg.Application.struct("MakeFan") -# Actions -TurnOnAction = fan_ns.class_('TurnOnAction', automation.Action) -TurnOffAction = fan_ns.class_('TurnOffAction', automation.Action) -ToggleAction = fan_ns.class_('ToggleAction', automation.Action) - -FanSpeed = fan_ns.enum('FanSpeed') -FAN_SPEEDS = { - 'OFF': FanSpeed.FAN_SPEED_OFF, - 'LOW': FanSpeed.FAN_SPEED_LOW, - 'MEDIUM': FanSpeed.FAN_SPEED_MEDIUM, - 'HIGH': FanSpeed.FAN_SPEED_HIGH, +FanDirection = fan_ns.enum("FanDirection") +FAN_DIRECTION_ENUM = { + "FORWARD": FanDirection.FAN_DIRECTION_FORWARD, + "REVERSE": FanDirection.FAN_DIRECTION_REVERSE, } -FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(FanState), - cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTFanComponent), - cv.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.All(cv.requires_component('mqtt'), - cv.publish_topic), - cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), - cv.subscribe_topic), -}) +# 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()) + +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), + cv.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_SPEED_LEVEL_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SPEED_LEVEL_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), + } + ), + cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), + } + ), + } +) -@coroutine -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])) +async def setup_fan_core_(var, config): + await setup_entity(var, config) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) - yield mqtt.register_mqtt_component(mqtt_, config) + await mqtt.register_mqtt_component(mqtt_, config) if CONF_OSCILLATION_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_oscillation_state_topic(config[CONF_OSCILLATION_STATE_TOPIC])) + cg.add( + mqtt_.set_custom_oscillation_state_topic( + config[CONF_OSCILLATION_STATE_TOPIC] + ) + ) if CONF_OSCILLATION_COMMAND_TOPIC in config: - cg.add(mqtt_.set_custom_oscillation_command_topic( - config[CONF_OSCILLATION_COMMAND_TOPIC])) + cg.add( + mqtt_.set_custom_oscillation_command_topic( + config[CONF_OSCILLATION_COMMAND_TOPIC] + ) + ) + if CONF_SPEED_LEVEL_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_speed_level_state_topic( + config[CONF_SPEED_LEVEL_STATE_TOPIC] + ) + ) + if CONF_SPEED_LEVEL_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_speed_level_command_topic( + config[CONF_SPEED_LEVEL_COMMAND_TOPIC] + ) + ) if CONF_SPEED_STATE_TOPIC in config: cg.add(mqtt_.set_custom_speed_state_topic(config[CONF_SPEED_STATE_TOPIC])) if CONF_SPEED_COMMAND_TOPIC in config: - cg.add(mqtt_.set_custom_speed_command_topic(config[CONF_SPEED_COMMAND_TOPIC])) + cg.add( + mqtt_.set_custom_speed_command_topic(config[CONF_SPEED_COMMAND_TOPIC]) + ) + + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TURN_OFF, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_SPEED_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) -@coroutine -def register_fan(var, config): +async def register_fan(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_fan(var)) - yield cg.register_component(var, config) - yield setup_fan_core_(var, config) + await cg.register_component(var, config) + await setup_fan_core_(var, config) -@coroutine -def create_fan_state(config): +async def create_fan_state(config): var = cg.new_Pvariable(config[CONF_ID]) - yield register_fan(var, config) - yield var + await register_fan(var, config) + return var -FAN_ACTION_SCHEMA = maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(FanState), -}) +FAN_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } +) -@automation.register_action('fan.toggle', ToggleAction, FAN_ACTION_SCHEMA) -def fan_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) +@automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA) +async def fan_toggle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('fan.turn_off', TurnOffAction, FAN_ACTION_SCHEMA) -def fan_turn_off_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) +@automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA) +async def fan_turn_off_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('fan.turn_on', TurnOnAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(FanState), - cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), - cv.Optional(CONF_SPEED): cv.templatable(cv.enum(FAN_SPEEDS, upper=True)), -})) -def fan_turn_on_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "fan.turn_on", + TurnOnAction, + maybe_simple_id( + { + 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) + ), + } + ), +) +async def fan_turn_on_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_OSCILLATING in config: - template_ = yield cg.templatable(config[CONF_OSCILLATING], args, bool) + template_ = await cg.templatable(config[CONF_OSCILLATING], args, bool) cg.add(var.set_oscillating(template_)) if CONF_SPEED in config: - template_ = yield cg.templatable(config[CONF_SPEED], args, FanSpeed) + template_ = await cg.templatable(config[CONF_SPEED], args, int) cg.add(var.set_speed(template_)) - yield var + 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) -def to_code(config): - cg.add_define('USE_FAN') +async def to_code(config): + cg.add_define("USE_FAN") cg.add_global(fan_ns.using) diff --git a/esphome/components/fan/automation.cpp b/esphome/components/fan/automation.cpp index 3c9ca9b6ed..79e583fc57 100644 --- a/esphome/components/fan/automation.cpp +++ b/esphome/components/fan/automation.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace fan { -static const char *TAG = "fan.automation"; +static const char *const TAG = "fan.automation"; } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index dfa72a3ea6..608f772b75 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -12,7 +12,8 @@ template class TurnOnAction : public Action { explicit TurnOnAction(FanState *state) : state_(state) {} TEMPLATABLE_VALUE(bool, oscillating) - TEMPLATABLE_VALUE(FanSpeed, speed) + TEMPLATABLE_VALUE(int, speed) + TEMPLATABLE_VALUE(FanDirection, direction) void play(Ts... x) override { auto call = this->state_->turn_on(); @@ -22,10 +23,12 @@ 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(); } - protected: FanState *state_; }; @@ -35,7 +38,6 @@ template class TurnOffAction : public Action { void play(Ts... x) override { this->state_->turn_off().perform(); } - protected: FanState *state_; }; @@ -45,9 +47,115 @@ template class ToggleAction : public Action { void play(Ts... x) override { this->state_->toggle().perform(); } + 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) { + state->add_on_state_callback([this, state]() { + auto is_on = state->state; + auto should_trigger = is_on && !this->last_on_; + this->last_on_ = is_on; + if (should_trigger) { + this->trigger(); + } + }); + this->last_on_ = state->state; + } + + protected: + bool last_on_; +}; + +class FanTurnOffTrigger : public Trigger<> { + public: + FanTurnOffTrigger(FanState *state) { + state->add_on_state_callback([this, state]() { + auto is_on = state->state; + auto should_trigger = !is_on && this->last_on_; + this->last_on_ = is_on; + if (should_trigger) { + this->trigger(); + } + }); + this->last_on_ = state->state; + } + + protected: + 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 new file mode 100644 index 0000000000..34883617e6 --- /dev/null +++ b/esphome/components/fan/fan_helpers.cpp @@ -0,0 +1,23 @@ +#include +#include "fan_helpers.h" + +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 = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); + return static_cast(legacy_level - 1); +} + +int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { + const auto enum_level = static_cast(speed) + 1; + const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); + return static_cast(speed_level); +} + +} // namespace fan +} // namespace esphome diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h new file mode 100644 index 0000000000..009505601e --- /dev/null +++ b/esphome/components/fan/fan_helpers.h @@ -0,0 +1,19 @@ +#pragma once +#include "fan_state.h" + +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 af170a755c..6ff4d3a833 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -1,17 +1,18 @@ #include "fan_state.h" +#include "fan_helpers.h" #include "esphome/core/log.h" namespace esphome { namespace fan { -static const char *TAG = "fan"; +static const char *const TAG = "fan"; const FanTraits &FanState::get_traits() const { return this->traits_; } 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); } @@ -20,12 +21,13 @@ FanStateCall FanState::make_call() { return FanStateCall(this); } struct FanStateRTCState { bool state; - FanSpeed speed; + int speed; bool oscillating; + FanDirection direction; }; 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; @@ -34,9 +36,10 @@ void FanState::setup() { call.set_state(recovered.state); call.set_speed(recovered.speed); call.set_oscillating(recovered.oscillating); + 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 { @@ -46,34 +49,34 @@ void FanStateCall::perform() const { if (this->oscillating_.has_value()) { this->state_->oscillating = *this->oscillating_; } + if (this->direction_.has_value()) { + this->state_->direction = *this->direction_; + } if (this->speed_.has_value()) { - switch (*this->speed_) { - case FAN_SPEED_LOW: - case FAN_SPEED_MEDIUM: - case FAN_SPEED_HIGH: - this->state_->speed = *this->speed_; - break; - default: - // protect from invalid input - break; - } + const int speed_count = this->state_->get_traits().supported_speed_count(); + this->state_->speed = clamp(*this->speed_, 1, speed_count); } FanStateRTCState saved{}; saved.state = this->state_->state; saved.speed = this->state_->speed; saved.oscillating = this->state_->oscillating; + saved.direction = this->state_->direction; this->state_->rtc_.save(&saved); this->state_->state_callback_.call(); } -FanStateCall &FanStateCall::set_speed(const char *speed) { - if (strcasecmp(speed, "low") == 0) { - this->set_speed(FAN_SPEED_LOW); - } else if (strcasecmp(speed, "medium") == 0) { - this->set_speed(FAN_SPEED_MEDIUM); - } else if (strcasecmp(speed, "high") == 0) { - this->set_speed(FAN_SPEED_HIGH); + +// 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) { + this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count)); + } else if (strcasecmp(legacy_speed, "medium") == 0) { + this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count)); + } else if (strcasecmp(legacy_speed, "high") == 0) { + this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count)); } return *this; } diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 4e937c68bd..c5a6f59ac4 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -1,20 +1,25 @@ #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" #include "fan_traits.h" namespace esphome { namespace fan { -/// Simple enum to represent the speed of a fan. -enum FanSpeed { +/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon +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. }; +/// Simple enum to represent the direction of a fan +enum FanDirection { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1 }; + class FanState; class FanStateCall { @@ -37,26 +42,32 @@ class FanStateCall { this->oscillating_ = oscillating; return *this; } - FanStateCall &set_speed(FanSpeed speed) { + FanStateCall &set_speed(int speed) { this->speed_ = speed; return *this; } - FanStateCall &set_speed(optional speed) { - this->speed_ = speed; + 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; + return *this; + } + FanStateCall &set_direction(optional direction) { + this->direction_ = direction; return *this; } - FanStateCall &set_speed(const char *speed); void perform() const; protected: FanState *const state_; optional binary_state_; - optional oscillating_{}; - optional speed_{}; + optional oscillating_; + optional speed_; + optional direction_{}; }; -class FanState : public Nameable, public Component { +class FanState : public EntityBase, public Component { public: FanState() = default; /// Construct the fan state with name. @@ -74,8 +85,10 @@ class FanState : public Nameable, public Component { bool state{false}; /// The current oscillation state of the fan. bool oscillating{false}; - /// The current fan speed. - FanSpeed speed{FAN_SPEED_HIGH}; + /// The current fan speed level + int speed{}; + /// The current direction of the fan + FanDirection direction{FAN_DIRECTION_FORWARD}; FanStateCall turn_on(); FanStateCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 67b0e33e88..e69d8e2e53 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -6,7 +6,8 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed) : oscillation_(false), speed_(speed) {} + FanTraits(bool oscillation, bool speed, bool direction, int speed_count) + : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -14,12 +15,22 @@ class FanTraits { void set_oscillation(bool oscillation) { this->oscillation_ = oscillation; } /// Return if this fan supports speed modes. bool supports_speed() const { return this->speed_; } - /// Set whether this fan supports speed modes. + /// Set whether this fan supports speed levels. void set_speed(bool speed) { this->speed_ = speed; } + /// Return how many speed levels the fan has + int supported_speed_count() const { return this->speed_count_; } + /// Set how many speed levels this fan has. + void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; } + /// Return if this fan supports changing direction + bool supports_direction() const { return this->direction_; } + /// Set whether this fan supports changing direction + void set_direction(bool direction) { this->direction_ = direction; } protected: bool oscillation_{false}; bool speed_{false}; + bool direction_{false}; + int speed_count_{}; }; } // namespace fan diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index ffa49f43c2..62de036e62 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -1,39 +1,48 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light -from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MAX_REFRESH_RATE -from esphome.core import coroutine +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_NUM_LEDS, + CONF_RGB_ORDER, + CONF_MAX_REFRESH_RATE, +) -fastled_base_ns = cg.esphome_ns.namespace('fastled_base') -FastLEDLightOutput = fastled_base_ns.class_('FastLEDLightOutput', light.AddressableLight) +CODEOWNERS = ["@OttoWinter"] +fastled_base_ns = cg.esphome_ns.namespace("fastled_base") +FastLEDLightOutput = fastled_base_ns.class_( + "FastLEDLightOutput", light.AddressableLight +) RGB_ORDERS = [ - 'RGB', - 'RBG', - 'GRB', - 'GBR', - 'BRG', - 'BGR', + "RGB", + "RBG", + "GRB", + "GBR", + "BRG", + "BGR", ] -BASE_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(FastLEDLightOutput), - - cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, - cv.Optional(CONF_RGB_ORDER): cv.one_of(*RGB_ORDERS, upper=True), - cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, -}).extend(cv.COMPONENT_SCHEMA) +BASE_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(FastLEDLightOutput), + cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, + cv.Optional(CONF_RGB_ORDER): cv.one_of(*RGB_ORDERS, upper=True), + cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, + } +).extend(cv.COMPONENT_SCHEMA) -@coroutine -def new_fastled_light(config): +async def new_fastled_light(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) if CONF_MAX_REFRESH_RATE in config: cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) - yield light.register_light(var, config) + await light.register_light(var, config) # https://github.com/FastLED/FastLED/blob/master/library.json - cg.add_library('FastLED', '3.3.3') - yield var + # 3.3.3 has an issue on ESP32 with RMT and fastled_clockless: + # https://github.com/esphome/issues/issues/1375 + 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 8420748dd5..486364d0c0 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -1,16 +1,18 @@ +#ifdef USE_ARDUINO + #include "fastled_light.h" #include "esphome/core/log.h" namespace esphome { namespace fastled_base { -static const char *TAG = "fastled"; +static const char *const TAG = "fastled"; 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 0729941e31..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; @@ -38,37 +40,37 @@ class FastLEDLightOutput : public light::AddressableLight { return *this->controller_; } - template + 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); } } } @@ -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 30fa910e59..acf9488ae3 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -4,54 +4,62 @@ from esphome import pins from esphome.components import fastled_base from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER -AUTO_LOAD = ['fastled_base'] +AUTO_LOAD = ["fastled_base"] CHIPSETS = [ - 'NEOPIXEL', - 'TM1829', - 'TM1809', - 'TM1804', - 'TM1803', - 'UCS1903', - 'UCS1903B', - 'UCS1904', - 'UCS2903', - 'WS2812', - 'WS2852', - 'WS2812B', - 'SK6812', - 'SK6822', - 'APA106', - 'PL9823', - 'WS2811', - 'WS2813', - 'APA104', - 'WS2811_400', - 'GW6205', - 'GW6205_400', - 'LPD1886', - 'LPD1886_8BIT', + "NEOPIXEL", + "TM1829", + "TM1809", + "TM1804", + "TM1803", + "UCS1903", + "UCS1903B", + "UCS1904", + "UCS2903", + "WS2812", + "WS2852", + "WS2812B", + "SK6812", + "SK6822", + "APA106", + "PL9823", + "WS2811", + "WS2813", + "APA104", + "WS2811_400", + "GW6205", + "GW6205_400", + "LPD1886", + "LPD1886_8BIT", + "SM16703", ] -def validate(value): - if value[CONF_CHIPSET] == 'NEOPIXEL' and CONF_RGB_ORDER in value: +def _validate(value): + if value[CONF_CHIPSET] == "NEOPIXEL" and CONF_RGB_ORDER in value: raise cv.Invalid("NEOPIXEL doesn't support RGB order") return value -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, -}), validate) +CONFIG_SCHEMA = cv.All( + fastled_base.BASE_SCHEMA.extend( + { + cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + } + ), + _validate, + cv.only_with_arduino, +) -def to_code(config): - var = yield fastled_base.new_fastled_light(config) +async def to_code(config): + var = await fastled_base.new_fastled_light(config) rgb_order = None if CONF_RGB_ORDER in config: rgb_order = cg.RawExpression(config[CONF_RGB_ORDER]) - template_args = cg.TemplateArguments(cg.RawExpression(config[CONF_CHIPSET]), - config[CONF_PIN], rgb_order) + template_args = cg.TemplateArguments( + cg.RawExpression(config[CONF_CHIPSET]), config[CONF_PIN], rgb_order + ) cg.add(var.add_leds(template_args, config[CONF_NUM_LEDS])) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index 959c8a1b19..a729fc015a 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -2,34 +2,61 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import fastled_base -from esphome.const import CONF_CHIPSET, CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_NUM_LEDS, CONF_RGB_ORDER +from esphome.const import ( + CONF_CHIPSET, + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_DATA_RATE, + CONF_NUM_LEDS, + CONF_RGB_ORDER, +) -AUTO_LOAD = ['fastled_base'] +AUTO_LOAD = ["fastled_base"] CHIPSETS = [ - 'LPD8806', - 'WS2801', - 'WS2803', - 'SM16716', - 'P9813', - 'APA102', - 'SK9822', - 'DOTSTAR', + "LPD8806", + "WS2801", + "WS2803", + "SM16716", + "P9813", + "APA102", + "SK9822", + "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, -}) +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, +) -def to_code(config): - var = yield fastled_base.new_fastled_light(config) +async def to_code(config): + var = await fastled_base.new_fastled_light(config) - rgb_order = None - if CONF_RGB_ORDER in config: - rgb_order = cg.RawExpression(config[CONF_RGB_ORDER]) - template_args = cg.TemplateArguments(cg.RawExpression(config[CONF_CHIPSET]), - config[CONF_DATA_PIN], config[CONF_CLOCK_PIN], rgb_order) + rgb_order = cg.RawExpression( + config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB" + ) + data_rate = None + + if CONF_DATA_RATE in config: + data_rate_khz = int(config[CONF_DATA_RATE] / 1000) + if data_rate_khz < 1000: + data_rate = cg.RawExpression(f"DATA_RATE_KHZ({data_rate_khz})") + else: + data_rate_mhz = int(data_rate_khz / 1000) + data_rate = cg.RawExpression(f"DATA_RATE_MHZ({data_rate_mhz})") + template_args = cg.TemplateArguments( + cg.RawExpression(config[CONF_CHIPSET]), + config[CONF_DATA_PIN], + config[CONF_CLOCK_PIN], + rgb_order, + data_rate, + ) cg.add(var.add_leds(template_args, config[CONF_NUM_LEDS])) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py new file mode 100644 index 0000000000..757a633e09 --- /dev/null +++ b/esphome/components/fingerprint_grow/__init__.py @@ -0,0 +1,295 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome import pins +from esphome.components import uart +from esphome.const import ( + CONF_COLOR, + CONF_COUNT, + CONF_FINGER_ID, + CONF_ID, + CONF_NEW_PASSWORD, + CONF_NUM_SCANS, + CONF_ON_ENROLLMENT_DONE, + CONF_ON_ENROLLMENT_FAILED, + CONF_ON_ENROLLMENT_SCAN, + CONF_ON_FINGER_SCAN_MATCHED, + CONF_ON_FINGER_SCAN_UNMATCHED, + CONF_PASSWORD, + CONF_SENSING_PIN, + CONF_SPEED, + CONF_STATE, + CONF_TRIGGER_ID, +) + +CODEOWNERS = ["@OnFreund", "@loongyh"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "sensor"] +MULTI_CONF = True + +CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id" + +fingerprint_grow_ns = cg.esphome_ns.namespace("fingerprint_grow") +FingerprintGrowComponent = fingerprint_grow_ns.class_( + "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice +) + +FingerScanMatchedTrigger = fingerprint_grow_ns.class_( + "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) +) + +FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( + "FingerScanUnmatchedTrigger", automation.Trigger.template() +) + +EnrollmentScanTrigger = fingerprint_grow_ns.class_( + "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) +) + +EnrollmentDoneTrigger = fingerprint_grow_ns.class_( + "EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16) +) + +EnrollmentFailedTrigger = fingerprint_grow_ns.class_( + "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16) +) + +EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action) +CancelEnrollmentAction = fingerprint_grow_ns.class_( + "CancelEnrollmentAction", automation.Action +) +DeleteAction = fingerprint_grow_ns.class_("DeleteAction", automation.Action) +DeleteAllAction = fingerprint_grow_ns.class_("DeleteAllAction", automation.Action) +LEDControlAction = fingerprint_grow_ns.class_("LEDControlAction", automation.Action) +AuraLEDControlAction = fingerprint_grow_ns.class_( + "AuraLEDControlAction", automation.Action +) + +AuraLEDState = fingerprint_grow_ns.enum("GrowAuraLEDState", True) +AURA_LED_STATES = { + "BREATHING": AuraLEDState.BREATHING, + "FLASHING": AuraLEDState.FLASHING, + "ALWAYS_ON": AuraLEDState.ALWAYS_ON, + "ALWAYS_OFF": AuraLEDState.ALWAYS_OFF, + "GRADUAL_ON": AuraLEDState.GRADUAL_ON, + "GRADUAL_OFF": AuraLEDState.GRADUAL_OFF, +} +validate_aura_led_states = cv.enum(AURA_LED_STATES, upper=True) +AuraLEDColor = fingerprint_grow_ns.enum("GrowAuraLEDColor", True) +AURA_LED_COLORS = { + "RED": AuraLEDColor.RED, + "BLUE": AuraLEDColor.BLUE, + "PURPLE": AuraLEDColor.PURPLE, +} +validate_aura_led_colors = cv.enum(AURA_LED_COLORS, upper=True) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FingerprintGrowComponent), + cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_PASSWORD): cv.uint32_t, + cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, + cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanMatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanUnmatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentScanTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentDoneTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentFailedTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("500ms")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_PASSWORD in config: + password = config[CONF_PASSWORD] + cg.add(var.set_password(password)) + await uart.register_uart_device(var, config) + + if CONF_NEW_PASSWORD in config: + new_password = config[CONF_NEW_PASSWORD] + cg.add(var.set_new_password(new_password)) + + if CONF_SENSING_PIN in config: + sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN]) + cg.add(var.set_sensing_pin(sensing_pin)) + + for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf + ) + + for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf + ) + + for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + + for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + + +@automation.register_action( + "fingerprint_grow.enroll", + EnrollmentAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t), + cv.Optional(CONF_NUM_SCANS): cv.templatable(cv.uint8_t), + }, + key=CONF_FINGER_ID, + ), +) +async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_FINGER_ID], args, cg.uint16) + cg.add(var.set_finger_id(template_)) + if CONF_NUM_SCANS in config: + template_ = await cg.templatable(config[CONF_NUM_SCANS], args, cg.uint8) + cg.add(var.set_num_scans(template_)) + return var + + +@automation.register_action( + "fingerprint_grow.cancel_enroll", + CancelEnrollmentAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + } + ), +) +async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "fingerprint_grow.delete", + DeleteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t), + }, + key=CONF_FINGER_ID, + ), +) +async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_FINGER_ID], args, cg.uint16) + cg.add(var.set_finger_id(template_)) + return var + + +@automation.register_action( + "fingerprint_grow.delete_all", + DeleteAllAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + } + ), +) +async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + }, + key=CONF_STATE, +) + + +@automation.register_action( + "fingerprint_grow.led_control", + LEDControlAction, + FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA, +) +async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) + return var + + +@automation.register_action( + "fingerprint_grow.aura_led_control", + AuraLEDControlAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_STATE): cv.templatable(validate_aura_led_states), + cv.Required(CONF_SPEED): cv.templatable(cv.uint8_t), + cv.Required(CONF_COLOR): cv.templatable(validate_aura_led_colors), + cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t), + } + ), +) +async def fingerprint_grow_aura_led_control_to_code( + config, action_id, template_arg, args +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + for key in [CONF_STATE, CONF_SPEED, CONF_COLOR, CONF_COUNT]: + template_ = await cg.templatable(config[key], args, cg.uint8) + cg.add(getattr(var, f"set_{key}")(template_)) + return var diff --git a/esphome/components/fingerprint_grow/binary_sensor.py b/esphome/components/fingerprint_grow/binary_sensor.py new file mode 100644 index 0000000000..f432ef92cc --- /dev/null +++ b/esphome/components/fingerprint_grow/binary_sensor.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ICON, ICON_KEY_PLUS +from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent + +DEPENDENCIES = ["fingerprint_grow"] + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), + cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_FINGERPRINT_GROW_ID]) + var = await binary_sensor.new_binary_sensor(config) + cg.add(hub.set_enrolling_binary_sensor(var)) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp new file mode 100644 index 0000000000..be17e29de3 --- /dev/null +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -0,0 +1,435 @@ +#include "fingerprint_grow.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace fingerprint_grow { + +static const char *const TAG = "fingerprint_grow"; + +// Based on Adafruit's library: https://github.com/adafruit/Adafruit-Fingerprint-Sensor-Library + +void FingerprintGrowComponent::update() { + if (this->enrollment_image_ > this->enrollment_buffers_) { + this->finish_enrollment(this->save_fingerprint_()); + return; + } + + if (this->sensing_pin_ != nullptr) { + if (this->sensing_pin_->digital_read()) { + ESP_LOGV(TAG, "No touch sensing"); + this->waiting_removal_ = false; + return; + } + } + + if (this->waiting_removal_) { + if (this->scan_image_(1) == NO_FINGER) { + ESP_LOGD(TAG, "Finger removed"); + this->waiting_removal_ = false; + } + return; + } + + if (this->enrollment_image_ == 0) { + this->scan_and_match_(); + return; + } + + uint8_t result = this->scan_image_(this->enrollment_image_); + if (result == NO_FINGER) { + return; + } + this->waiting_removal_ = true; + if (result != OK) { + this->finish_enrollment(result); + return; + } + this->enrollment_scan_callback_.call(this->enrollment_image_, this->enrollment_slot_); + ++this->enrollment_image_; +} + +void FingerprintGrowComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); + if (this->check_password_()) { + if (this->new_password_ != nullptr) { + if (this->set_password_()) + return; + } else { + if (this->get_parameters_()) + return; + } + } + this->mark_failed(); +} + +void FingerprintGrowComponent::enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers) { + ESP_LOGI(TAG, "Starting enrollment in slot %d", finger_id); + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(true); + } + this->enrollment_slot_ = finger_id; + this->enrollment_buffers_ = num_buffers; + this->enrollment_image_ = 1; +} + +void FingerprintGrowComponent::finish_enrollment(uint8_t result) { + if (result == OK) { + this->enrollment_done_callback_.call(this->enrollment_slot_); + this->get_fingerprint_count_(); + } else { + this->enrollment_failed_callback_.call(this->enrollment_slot_); + } + this->enrollment_image_ = 0; + this->enrollment_slot_ = 0; + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(false); + } + ESP_LOGI(TAG, "Finished enrollment"); +} + +void FingerprintGrowComponent::scan_and_match_() { + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "Scan and match"); + } else { + ESP_LOGV(TAG, "Scan and match"); + } + if (this->scan_image_(1) == OK) { + this->waiting_removal_ = true; + this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t)(this->capacity_ >> 8), (uint8_t)(this->capacity_ & 0xFF)}; + switch (this->send_command_()) { + case OK: { + ESP_LOGD(TAG, "Fingerprint matched"); + uint16_t finger_id = ((uint16_t) this->data_[1] << 8) | this->data_[2]; + uint16_t confidence = ((uint16_t) this->data_[3] << 8) | this->data_[4]; + if (this->last_finger_id_sensor_ != nullptr) { + this->last_finger_id_sensor_->publish_state(finger_id); + } + if (this->last_confidence_sensor_ != nullptr) { + this->last_confidence_sensor_->publish_state(confidence); + } + this->finger_scan_matched_callback_.call(finger_id, confidence); + break; + } + case NOT_FOUND: + ESP_LOGD(TAG, "Fingerprint not matched to any saved slots"); + this->finger_scan_unmatched_callback_.call(); + break; + } + } +} + +uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "Getting image %d", buffer); + } else { + ESP_LOGV(TAG, "Getting image %d", buffer); + } + this->data_ = {GET_IMAGE}; + switch (this->send_command_()) { + case OK: + break; + case NO_FINGER: + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "No finger"); + } else { + ESP_LOGV(TAG, "No finger"); + } + return this->data_[0]; + case IMAGE_FAIL: + ESP_LOGE(TAG, "Imaging error"); + default: + return this->data_[0]; + } + + ESP_LOGD(TAG, "Processing image %d", buffer); + this->data_ = {IMAGE_2_TZ, buffer}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Processed image %d", buffer); + break; + case IMAGE_MESS: + ESP_LOGE(TAG, "Image too messy"); + break; + case FEATURE_FAIL: + case INVALID_IMAGE: + ESP_LOGE(TAG, "Could not find fingerprint features"); + break; + } + return this->data_[0]; +} + +uint8_t FingerprintGrowComponent::save_fingerprint_() { + ESP_LOGI(TAG, "Creating model"); + this->data_ = {REG_MODEL}; + switch (this->send_command_()) { + case OK: + break; + case ENROLL_MISMATCH: + ESP_LOGE(TAG, "Scans do not match"); + default: + return this->data_[0]; + } + + ESP_LOGI(TAG, "Storing model"); + this->data_ = {STORE, 0x01, (uint8_t)(this->enrollment_slot_ >> 8), (uint8_t)(this->enrollment_slot_ & 0xFF)}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Stored model"); + break; + case BAD_LOCATION: + ESP_LOGE(TAG, "Invalid slot"); + break; + case FLASH_ERR: + ESP_LOGE(TAG, "Error writing to flash"); + break; + } + return this->data_[0]; +} + +bool FingerprintGrowComponent::check_password_() { + ESP_LOGD(TAG, "Checking password"); + this->data_ = {VERIFY_PASSWORD, (uint8_t)(this->password_ >> 24), (uint8_t)(this->password_ >> 16), + (uint8_t)(this->password_ >> 8), (uint8_t)(this->password_ & 0xFF)}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "Password verified"); + return true; + case PASSWORD_FAIL: + ESP_LOGE(TAG, "Wrong password"); + break; + } + return false; +} + +bool FingerprintGrowComponent::set_password_() { + ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_); + this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16), + (uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)}; + if (this->send_command_() == OK) { + ESP_LOGI(TAG, "New password successfully set"); + ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); + ESP_LOGW(TAG, "!!!Forgetting the password will render your device unusable!!!"); + return true; + } + return false; +} + +bool FingerprintGrowComponent::get_parameters_() { + ESP_LOGD(TAG, "Getting parameters"); + this->data_ = {READ_SYS_PARAM}; + if (this->send_command_() == OK) { + ESP_LOGD(TAG, "Got parameters"); + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); + } + this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6]; + if (this->capacity_sensor_ != nullptr) { + this->capacity_sensor_->publish_state(this->capacity_); + } + if (this->security_level_sensor_ != nullptr) { + this->security_level_sensor_->publish_state(((uint16_t) this->data_[7] << 8) | this->data_[8]); + } + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(false); + } + this->get_fingerprint_count_(); + return true; + } + return false; +} + +void FingerprintGrowComponent::get_fingerprint_count_() { + ESP_LOGD(TAG, "Getting fingerprint count"); + this->data_ = {TEMPLATE_COUNT}; + if (this->send_command_() == OK) { + ESP_LOGD(TAG, "Got fingerprint count"); + if (this->fingerprint_count_sensor_ != nullptr) + this->fingerprint_count_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); + } +} + +void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { + ESP_LOGI(TAG, "Deleting fingerprint in slot %d", finger_id); + this->data_ = {DELETE, (uint8_t)(finger_id >> 8), (uint8_t)(finger_id & 0xFF), 0x00, 0x01}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Deleted fingerprint"); + this->get_fingerprint_count_(); + break; + case DELETE_FAIL: + ESP_LOGE(TAG, "Reader failed to delete fingerprint"); + break; + } +} + +void FingerprintGrowComponent::delete_all_fingerprints() { + ESP_LOGI(TAG, "Deleting all stored fingerprints"); + this->data_ = {EMPTY}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Deleted all fingerprints"); + this->get_fingerprint_count_(); + break; + case DB_CLEAR_FAIL: + ESP_LOGE(TAG, "Reader failed to clear fingerprint library"); + break; + } +} + +void FingerprintGrowComponent::led_control(bool state) { + ESP_LOGD(TAG, "Setting LED"); + if (state) + this->data_ = {LED_ON}; + else + this->data_ = {LED_OFF}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "LED set"); + break; + case PACKET_RCV_ERR: + case TIMEOUT: + break; + default: + ESP_LOGE(TAG, "Try aura_led_control instead"); + break; + } +} + +void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count) { + const uint32_t now = millis(); + const uint32_t elapsed = now - this->last_aura_led_control_; + if (elapsed < this->last_aura_led_duration_) { + delay(this->last_aura_led_duration_ - elapsed); + } + ESP_LOGD(TAG, "Setting Aura LED"); + this->data_ = {AURA_CONFIG, state, speed, color, count}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "Aura LED set"); + this->last_aura_led_control_ = millis(); + this->last_aura_led_duration_ = 10 * speed * count; + break; + case PACKET_RCV_ERR: + case TIMEOUT: + break; + default: + ESP_LOGE(TAG, "Try led_control instead"); + break; + } +} + +uint8_t FingerprintGrowComponent::send_command_() { + this->write((uint8_t)(START_CODE >> 8)); + this->write((uint8_t)(START_CODE & 0xFF)); + this->write(this->address_[0]); + this->write(this->address_[1]); + this->write(this->address_[2]); + this->write(this->address_[3]); + this->write(COMMAND); + + uint16_t wire_length = this->data_.size() + 2; + this->write((uint8_t)(wire_length >> 8)); + this->write((uint8_t)(wire_length & 0xFF)); + + uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; + for (auto data : this->data_) { + this->write(data); + sum += data; + } + + this->write((uint8_t)(sum >> 8)); + this->write((uint8_t)(sum & 0xFF)); + + this->data_.clear(); + + uint8_t byte; + uint16_t idx = 0, length = 0; + + for (uint16_t timer = 0; timer < 1000; timer++) { + if (this->available() == 0) { + delay(1); + continue; + } + byte = this->read(); + switch (idx) { + case 0: + if (byte != (uint8_t)(START_CODE >> 8)) + continue; + break; + case 1: + if (byte != (uint8_t)(START_CODE & 0xFF)) { + idx = 0; + continue; + } + break; + case 2: + case 3: + case 4: + case 5: + if (byte != this->address_[idx - 2]) { + idx = 0; + continue; + } + break; + case 6: + if (byte != ACK) { + idx = 0; + continue; + } + break; + case 7: + length = (uint16_t) byte << 8; + break; + case 8: + length |= byte; + break; + default: + this->data_.push_back(byte); + if ((idx - 8) == length) { + switch (this->data_[0]) { + case OK: + case NO_FINGER: + case IMAGE_FAIL: + case IMAGE_MESS: + case FEATURE_FAIL: + case NO_MATCH: + case NOT_FOUND: + case ENROLL_MISMATCH: + case BAD_LOCATION: + case DELETE_FAIL: + case DB_CLEAR_FAIL: + case PASSWORD_FAIL: + case INVALID_IMAGE: + case FLASH_ERR: + break; + case PACKET_RCV_ERR: + ESP_LOGE(TAG, "Reader failed to process request"); + break; + default: + ESP_LOGE(TAG, "Unknown response received from reader: %d", this->data_[0]); + break; + } + return this->data_[0]; + } + break; + } + idx++; + } + ESP_LOGE(TAG, "No response received from reader"); + this->data_[0] = TIMEOUT; + return TIMEOUT; +} + +void FingerprintGrowComponent::dump_config() { + ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); + LOG_SENSOR(" ", "Status", this->status_sensor_); + LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); + LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); + LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); + LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); +} + +} // namespace fingerprint_grow +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h new file mode 100644 index 0000000000..e7d734777a --- /dev/null +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -0,0 +1,276 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace fingerprint_grow { + +static const uint16_t START_CODE = 0xEF01; + +enum GrowPacketType { + COMMAND = 0x01, + DATA = 0x02, + ACK = 0x07, + END_DATA = 0x08, +}; + +enum GrowCommand { + GET_IMAGE = 0x01, + IMAGE_2_TZ = 0x02, + SEARCH = 0x04, + REG_MODEL = 0x05, + STORE = 0x06, + LOAD = 0x07, + UPLOAD = 0x08, + DELETE = 0x0C, + EMPTY = 0x0D, + READ_SYS_PARAM = 0x0F, + SET_PASSWORD = 0x12, + VERIFY_PASSWORD = 0x13, + HI_SPEED_SEARCH = 0x1B, + TEMPLATE_COUNT = 0x1D, + AURA_CONFIG = 0x35, + LED_ON = 0x50, + LED_OFF = 0x51, +}; + +enum GrowResponse { + OK = 0x00, + PACKET_RCV_ERR = 0x01, + NO_FINGER = 0x02, + IMAGE_FAIL = 0x03, + IMAGE_MESS = 0x06, + FEATURE_FAIL = 0x07, + NO_MATCH = 0x08, + NOT_FOUND = 0x09, + ENROLL_MISMATCH = 0x0A, + BAD_LOCATION = 0x0B, + DB_RANGE_FAIL = 0x0C, + UPLOAD_FEATURE_FAIL = 0x0D, + PACKET_RESPONSE_FAIL = 0x0E, + UPLOAD_FAIL = 0x0F, + DELETE_FAIL = 0x10, + DB_CLEAR_FAIL = 0x11, + PASSWORD_FAIL = 0x13, + INVALID_IMAGE = 0x15, + FLASH_ERR = 0x18, + INVALID_REG = 0x1A, + BAD_PACKET = 0xFE, + TIMEOUT = 0xFF, +}; + +enum GrowAuraLEDState { + BREATHING = 0x01, + FLASHING = 0x02, + ALWAYS_ON = 0x03, + ALWAYS_OFF = 0x04, + GRADUAL_ON = 0x05, + GRADUAL_OFF = 0x06, +}; + +enum GrowAuraLEDColor { + RED = 0x01, + BLUE = 0x02, + PURPLE = 0x03, +}; + +class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevice { + public: + void update() override; + void setup() override; + void dump_config() override; + + void set_address(uint32_t address) { + this->address_[0] = (uint8_t)(address >> 24); + this->address_[1] = (uint8_t)(address >> 16); + this->address_[2] = (uint8_t)(address >> 8); + this->address_[3] = (uint8_t)(address & 0xFF); + } + void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } + void set_password(uint32_t password) { this->password_ = password; } + void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; } + void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { + this->fingerprint_count_sensor_ = fingerprint_count_sensor; + } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_capacity_sensor(sensor::Sensor *capacity_sensor) { this->capacity_sensor_ = capacity_sensor; } + void set_security_level_sensor(sensor::Sensor *security_level_sensor) { + this->security_level_sensor_ = security_level_sensor; + } + void set_last_finger_id_sensor(sensor::Sensor *last_finger_id_sensor) { + this->last_finger_id_sensor_ = last_finger_id_sensor; + } + void set_last_confidence_sensor(sensor::Sensor *last_confidence_sensor) { + this->last_confidence_sensor_ = last_confidence_sensor; + } + void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { + this->enrolling_binary_sensor_ = enrolling_binary_sensor; + } + void add_on_finger_scan_matched_callback(std::function callback) { + this->finger_scan_matched_callback_.add(std::move(callback)); + } + void add_on_finger_scan_unmatched_callback(std::function callback) { + this->finger_scan_unmatched_callback_.add(std::move(callback)); + } + void add_on_enrollment_scan_callback(std::function callback) { + this->enrollment_scan_callback_.add(std::move(callback)); + } + void add_on_enrollment_done_callback(std::function callback) { + this->enrollment_done_callback_.add(std::move(callback)); + } + + void add_on_enrollment_failed_callback(std::function callback) { + this->enrollment_failed_callback_.add(std::move(callback)); + } + + void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers); + void finish_enrollment(uint8_t result); + void delete_fingerprint(uint16_t finger_id); + void delete_all_fingerprints(); + + void led_control(bool state); + void aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count); + + protected: + void scan_and_match_(); + uint8_t scan_image_(uint8_t buffer); + uint8_t save_fingerprint_(); + bool check_password_(); + bool set_password_(); + bool get_parameters_(); + void get_fingerprint_count_(); + uint8_t send_command_(); + + std::vector data_ = {}; + uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + uint16_t capacity_ = 64; + uint32_t password_ = 0x0; + uint32_t *new_password_{nullptr}; + GPIOPin *sensing_pin_{nullptr}; + uint8_t enrollment_image_ = 0; + uint16_t enrollment_slot_ = 0; + uint8_t enrollment_buffers_ = 5; + bool waiting_removal_ = false; + uint32_t last_aura_led_control_ = 0; + uint16_t last_aura_led_duration_ = 0; + sensor::Sensor *fingerprint_count_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + sensor::Sensor *capacity_sensor_{nullptr}; + sensor::Sensor *security_level_sensor_{nullptr}; + sensor::Sensor *last_finger_id_sensor_{nullptr}; + sensor::Sensor *last_confidence_sensor_{nullptr}; + binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + CallbackManager finger_scan_matched_callback_; + CallbackManager finger_scan_unmatched_callback_; + CallbackManager enrollment_scan_callback_; + CallbackManager enrollment_done_callback_; + CallbackManager enrollment_failed_callback_; +}; + +class FingerScanMatchedTrigger : public Trigger { + public: + explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_matched_callback( + [this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); }); + } +}; + +class FingerScanUnmatchedTrigger : public Trigger<> { + public: + explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); }); + } +}; + +class EnrollmentScanTrigger : public Trigger { + public: + explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_scan_callback( + [this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); }); + } +}; + +class EnrollmentDoneTrigger : public Trigger { + public: + explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); + } +}; + +class EnrollmentFailedTrigger : public Trigger { + public: + explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); + } +}; + +template class EnrollmentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, finger_id) + TEMPLATABLE_VALUE(uint8_t, num_scans) + + void play(Ts... x) override { + auto finger_id = this->finger_id_.value(x...); + auto num_scans = this->num_scans_.value(x...); + if (num_scans) { + this->parent_->enroll_fingerprint(finger_id, num_scans); + } else { + this->parent_->enroll_fingerprint(finger_id, 2); + } + } +}; + +template +class CancelEnrollmentAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->finish_enrollment(1); } +}; + +template class DeleteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, finger_id) + + void play(Ts... x) override { + auto finger_id = this->finger_id_.value(x...); + this->parent_->delete_fingerprint(finger_id); + } +}; + +template class DeleteAllAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->delete_all_fingerprints(); } +}; + +template class LEDControlAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + auto state = this->state_.value(x...); + this->parent_->led_control(state); + } +}; + +template class AuraLEDControlAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, state) + TEMPLATABLE_VALUE(uint8_t, speed) + TEMPLATABLE_VALUE(uint8_t, color) + TEMPLATABLE_VALUE(uint8_t, count) + + void play(Ts... x) override { + auto state = this->state_.value(x...); + auto speed = this->speed_.value(x...); + auto color = this->color_.value(x...); + auto count = this->count_.value(x...); + + this->parent_->aura_led_control(state, speed, color, count); + } +}; + +} // namespace fingerprint_grow +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py new file mode 100644 index 0000000000..4ae670743d --- /dev/null +++ b/esphome/components/fingerprint_grow/sensor.py @@ -0,0 +1,80 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_CAPACITY, + CONF_FINGERPRINT_COUNT, + CONF_LAST_CONFIDENCE, + CONF_LAST_FINGER_ID, + CONF_SECURITY_LEVEL, + CONF_STATUS, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_ACCOUNT_CHECK, + ICON_DATABASE, + ICON_FINGERPRINT, + ICON_SECURITY, + STATE_CLASS_NONE, +) +from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent + +DEPENDENCIES = ["fingerprint_grow"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), + cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( + icon=ICON_FINGERPRINT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_CAPACITY): sensor.sensor_schema( + icon=ICON_DATABASE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( + icon=ICON_SECURITY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( + icon=ICON_ACCOUNT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( + icon=ICON_ACCOUNT_CHECK, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_FINGERPRINT_GROW_ID]) + + for key in [ + CONF_FINGERPRINT_COUNT, + CONF_STATUS, + CONF_CAPACITY, + CONF_SECURITY_LEVEL, + CONF_LAST_FINGER_ID, + CONF_LAST_CONFIDENCE, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 5b19dc74e0..6af5be45d4 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -4,14 +4,15 @@ 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'] +DEPENDENCIES = ["display"] MULTI_CONF = True -Font = display.display_ns.class_('Font') -Glyph = display.display_ns.class_('Glyph') +Font = display.display_ns.class_("Font") +Glyph = display.display_ns.class_("Glyph") +GlyphData = display.display_ns.struct("GlyphData") def validate_glyphs(value): @@ -20,8 +21,8 @@ def validate_glyphs(value): value = cv.Schema([cv.string])(list(value)) def comparator(x, y): - x_ = x.encode('utf-8') - y_ = y.encode('utf-8') + x_ = x.encode("utf-8") + y_ = y.encode("utf-8") for c in range(min(len(x_), len(y_))): if x_[c] < y_[c]: @@ -42,42 +43,54 @@ def validate_glyphs(value): def validate_pillow_installed(value): try: import PIL - except ImportError: - raise cv.Invalid("Please install the pillow python package to use this feature. " - "(pip install pillow)") + except ImportError as err: + raise cv.Invalid( + "Please install the pillow python package to use this feature. " + "(pip install pillow)" + ) from err - if PIL.__version__[0] < '4': - raise cv.Invalid("Please update your pillow installation to at least 4.0.x. " - "(pip install -U pillow)") + if PIL.__version__[0] < "4": + raise cv.Invalid( + "Please update your pillow installation to at least 4.0.x. " + "(pip install -U pillow)" + ) return 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)) - if not value.endswith('.ttf'): - raise cv.Invalid("Only truetype (.ttf) files are supported. Please make sure you're " - "using the correct format or rename the extension to .ttf") + if value.endswith(".zip"): # for Google Fonts downloads + raise cv.Invalid( + f"Please unzip the font archive '{value}' first and then use the .ttf files inside." + ) + if not value.endswith(".ttf"): + raise cv.Invalid( + "Only truetype (.ttf) files are supported. Please make sure you're " + "using the correct format or rename the extension to .ttf" + ) return cv.file_(value) -DEFAULT_GLYPHS = ' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' -CONF_RAW_DATA_ID = 'raw_data_id' +DEFAULT_GLYPHS = ( + ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' +) +CONF_RAW_GLYPH_ID = "raw_glyph_id" -FONT_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(Font), - cv.Required(CONF_FILE): validate_truetype_file, - cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, - cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), -}) +FONT_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(Font), + cv.Required(CONF_FILE): validate_truetype_file, + cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, + cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), + } +) CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA) -def to_code(config): +async def to_code(config): from PIL import ImageFont path = CORE.relative_config_path(config[CONF_FILE]) @@ -91,11 +104,11 @@ def to_code(config): glyph_args = {} data = [] for glyph in config[CONF_GLYPHS]: - mask = font.getmask(glyph, mode='1') + mask = font.getmask(glyph, mode="1") _, (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)): @@ -108,8 +121,25 @@ def to_code(config): rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - glyphs = [] + glyph_initializer = [] for glyph in config[CONF_GLYPHS]: - glyphs.append(Glyph(glyph, prog_arr, *glyph_args[glyph])) + glyph_initializer.append( + cg.StructInitializer( + GlyphData, + ("a_char", glyph), + ( + "data", + cg.RawExpression(f"{str(prog_arr)} + {str(glyph_args[glyph][0])}"), + ), + ("offset_x", glyph_args[glyph][1]), + ("offset_y", glyph_args[glyph][2]), + ("width", glyph_args[glyph][3]), + ("height", glyph_args[glyph][4]), + ) + ) - cg.new_Pvariable(config[CONF_ID], glyphs, ascent, ascent + descent) + glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) + + cg.new_Pvariable( + config[CONF_ID], glyphs, len(glyph_initializer), ascent, ascent + descent + ) diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py index a6774c397a..427721f2db 100644 --- a/esphome/components/fujitsu_general/climate.py +++ b/esphome/components/fujitsu_general/climate.py @@ -3,16 +3,20 @@ import esphome.config_validation as cv from esphome.components import climate_ir from esphome.const import CONF_ID -AUTO_LOAD = ['climate_ir'] +AUTO_LOAD = ["climate_ir"] -fujitsu_general_ns = cg.esphome_ns.namespace('fujitsu_general') -FujitsuGeneralClimate = fujitsu_general_ns.class_('FujitsuGeneralClimate', climate_ir.ClimateIR) +fujitsu_general_ns = cg.esphome_ns.namespace("fujitsu_general") +FujitsuGeneralClimate = fujitsu_general_ns.class_( + "FujitsuGeneralClimate", climate_ir.ClimateIR +) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), -}) +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield climate_ir.register_climate_ir(var, config) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 261d8be258..9e58f672c7 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -3,182 +3,208 @@ namespace esphome { namespace fujitsu_general { -static const char *TAG = "fujitsu_general.climate"; +// bytes' bits are reversed for fujitsu, so nibbles are ordered 1, 0, 3, 2, 5, 4, etc... -// Control packet -const uint16_t FUJITSU_GENERAL_STATE_LENGTH = 16; +#define SET_NIBBLE(message, nibble, value) \ + ((message)[(nibble) / 2] |= ((value) &0b00001111) << (((nibble) % 2) ? 0 : 4)) +#define GET_NIBBLE(message, nibble) (((message)[(nibble) / 2] >> (((nibble) % 2) ? 0 : 4)) & 0b00001111) -const uint8_t FUJITSU_GENERAL_BASE_BYTE0 = 0x14; -const uint8_t FUJITSU_GENERAL_BASE_BYTE1 = 0x63; -const uint8_t FUJITSU_GENERAL_BASE_BYTE2 = 0x00; -const uint8_t FUJITSU_GENERAL_BASE_BYTE3 = 0x10; -const uint8_t FUJITSU_GENERAL_BASE_BYTE4 = 0x10; -const uint8_t FUJITSU_GENERAL_BASE_BYTE5 = 0xFE; -const uint8_t FUJITSU_GENERAL_BASE_BYTE6 = 0x09; -const uint8_t FUJITSU_GENERAL_BASE_BYTE7 = 0x30; +static const char *const TAG = "fujitsu_general.climate"; -// Temperature and POWER ON -const uint8_t FUJITSU_GENERAL_POWER_ON_MASK_BYTE8 = 0b00000001; -const uint8_t FUJITSU_GENERAL_BASE_BYTE8 = 0x40; +// Common header +const uint8_t FUJITSU_GENERAL_COMMON_LENGTH = 6; +const uint8_t FUJITSU_GENERAL_COMMON_BYTE0 = 0x14; +const uint8_t FUJITSU_GENERAL_COMMON_BYTE1 = 0x63; +const uint8_t FUJITSU_GENERAL_COMMON_BYTE2 = 0x00; +const uint8_t FUJITSU_GENERAL_COMMON_BYTE3 = 0x10; +const uint8_t FUJITSU_GENERAL_COMMON_BYTE4 = 0x10; +const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_BYTE = 5; + +// State message - temp & fan etc. +const uint8_t FUJITSU_GENERAL_STATE_MESSAGE_LENGTH = 16; +const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_STATE = 0xFE; + +// Util messages - off & eco etc. +const uint8_t FUJITSU_GENERAL_UTIL_MESSAGE_LENGTH = 7; +const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_OFF = 0x02; +const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_ECONOMY = 0x09; +const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_NUDGE = 0x6C; + +// State header +const uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE0 = 0x09; +const uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE1 = 0x30; + +// State footer +const uint8_t FUJITSU_GENERAL_STATE_FOOTER_BYTE0 = 0x20; + +// Temperature +const uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16; + +// Power on +const uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17; +const uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00; +const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01; // Mode -const uint8_t FUJITSU_GENERAL_MODE_AUTO_BYTE9 = 0x00; -const uint8_t FUJITSU_GENERAL_MODE_HEAT_BYTE9 = 0x04; -const uint8_t FUJITSU_GENERAL_MODE_COOL_BYTE9 = 0x01; -const uint8_t FUJITSU_GENERAL_MODE_DRY_BYTE9 = 0x02; -const uint8_t FUJITSU_GENERAL_MODE_FAN_BYTE9 = 0x03; -const uint8_t FUJITSU_GENERAL_MODE_10C_BYTE9 = 0x0B; -const uint8_t FUJITSU_GENERAL_BASE_BYTE9 = 0x01; +const uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19; +const uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00; +const uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01; +const uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02; +const uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03; +const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04; +// const uint8_t FUJITSU_GENERAL_MODE_10C = 0x0B; -// Fan speed and swing -const uint8_t FUJITSU_GENERAL_FAN_AUTO_BYTE10 = 0x00; -const uint8_t FUJITSU_GENERAL_FAN_HIGH_BYTE10 = 0x01; -const uint8_t FUJITSU_GENERAL_FAN_MEDIUM_BYTE10 = 0x02; -const uint8_t FUJITSU_GENERAL_FAN_LOW_BYTE10 = 0x03; -const uint8_t FUJITSU_GENERAL_FAN_SILENT_BYTE10 = 0x04; -const uint8_t FUJITSU_GENERAL_SWING_MASK_BYTE10 = 0b00010000; -const uint8_t FUJITSU_GENERAL_BASE_BYTE10 = 0x00; +// Swing +const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20; +const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00; +const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01; +const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02; +const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03; -const uint8_t FUJITSU_GENERAL_BASE_BYTE11 = 0x00; -const uint8_t FUJITSU_GENERAL_BASE_BYTE12 = 0x00; -const uint8_t FUJITSU_GENERAL_BASE_BYTE13 = 0x00; +// Fan +const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21; +const uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00; +const uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01; +const uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02; +const uint8_t FUJITSU_GENERAL_FAN_LOW = 0x03; +const uint8_t FUJITSU_GENERAL_FAN_SILENT = 0x04; -// Outdoor Unit Low Noise -const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; -const uint8_t FUJITSU_GENERAL_BASE_BYTE14 = 0x20; - -// CRC -const uint8_t FUJITSU_GENERAL_BASE_BYTE15 = 0x6F; - -// Power off packet is specific -const uint16_t FUJITSU_GENERAL_OFF_LENGTH = 7; - -const uint8_t FUJITSU_GENERAL_OFF_BYTE0 = FUJITSU_GENERAL_BASE_BYTE0; -const uint8_t FUJITSU_GENERAL_OFF_BYTE1 = FUJITSU_GENERAL_BASE_BYTE1; -const uint8_t FUJITSU_GENERAL_OFF_BYTE2 = FUJITSU_GENERAL_BASE_BYTE2; -const uint8_t FUJITSU_GENERAL_OFF_BYTE3 = FUJITSU_GENERAL_BASE_BYTE3; -const uint8_t FUJITSU_GENERAL_OFF_BYTE4 = FUJITSU_GENERAL_BASE_BYTE4; -const uint8_t FUJITSU_GENERAL_OFF_BYTE5 = 0x02; -const uint8_t FUJITSU_GENERAL_OFF_BYTE6 = 0xFD; - -const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius -const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius +// TODO Outdoor Unit Low Noise +// const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; +// const uint8_t FUJITSU_GENERAL_STATE_BYTE14 = 0x20; const uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300; const uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600; + const uint16_t FUJITSU_GENERAL_BIT_MARK = 420; const uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200; const uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420; + const uint16_t FUJITSU_GENERAL_TRL_MARK = 420; const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000; const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000; -FujitsuGeneralClimate::FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1) {} - void FujitsuGeneralClimate::transmit_state() { if (this->mode == climate::CLIMATE_MODE_OFF) { this->transmit_off_(); return; } - uint8_t remote_state[FUJITSU_GENERAL_STATE_LENGTH] = {0}; - remote_state[0] = FUJITSU_GENERAL_BASE_BYTE0; - remote_state[1] = FUJITSU_GENERAL_BASE_BYTE1; - remote_state[2] = FUJITSU_GENERAL_BASE_BYTE2; - remote_state[3] = FUJITSU_GENERAL_BASE_BYTE3; - remote_state[4] = FUJITSU_GENERAL_BASE_BYTE4; - remote_state[5] = FUJITSU_GENERAL_BASE_BYTE5; - remote_state[6] = FUJITSU_GENERAL_BASE_BYTE6; - remote_state[7] = FUJITSU_GENERAL_BASE_BYTE7; - remote_state[8] = FUJITSU_GENERAL_BASE_BYTE8; - remote_state[9] = FUJITSU_GENERAL_BASE_BYTE9; - remote_state[10] = FUJITSU_GENERAL_BASE_BYTE10; - remote_state[11] = FUJITSU_GENERAL_BASE_BYTE11; - remote_state[12] = FUJITSU_GENERAL_BASE_BYTE12; - remote_state[13] = FUJITSU_GENERAL_BASE_BYTE13; - remote_state[14] = FUJITSU_GENERAL_BASE_BYTE14; - remote_state[15] = FUJITSU_GENERAL_BASE_BYTE15; + ESP_LOGV(TAG, "Transmit state"); + + uint8_t remote_state[FUJITSU_GENERAL_STATE_MESSAGE_LENGTH] = {0}; + + // Common message header + remote_state[0] = FUJITSU_GENERAL_COMMON_BYTE0; + remote_state[1] = FUJITSU_GENERAL_COMMON_BYTE1; + remote_state[2] = FUJITSU_GENERAL_COMMON_BYTE2; + remote_state[3] = FUJITSU_GENERAL_COMMON_BYTE3; + remote_state[4] = FUJITSU_GENERAL_COMMON_BYTE4; + remote_state[5] = FUJITSU_GENERAL_MESSAGE_TYPE_STATE; + remote_state[6] = FUJITSU_GENERAL_STATE_HEADER_BYTE0; + remote_state[7] = FUJITSU_GENERAL_STATE_HEADER_BYTE1; + + // unknown, does not appear to change with any remote settings + remote_state[14] = FUJITSU_GENERAL_STATE_FOOTER_BYTE0; // Set temperature - uint8_t safecelsius = std::max((uint8_t) this->target_temperature, FUJITSU_GENERAL_TEMP_MIN); - safecelsius = std::min(safecelsius, FUJITSU_GENERAL_TEMP_MAX); - remote_state[8] = (byte) safecelsius - 16; - remote_state[8] = remote_state[8] << 4; + uint8_t temperature_clamped = + (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); - // If not powered - set power on flag + // Set power on if (!this->power_) { - remote_state[8] = (byte) remote_state[8] | FUJITSU_GENERAL_POWER_ON_MASK_BYTE8; + SET_NIBBLE(remote_state, FUJITSU_GENERAL_POWER_ON_NIBBLE, FUJITSU_GENERAL_POWER_ON); } // Set mode switch (this->mode) { case climate::CLIMATE_MODE_COOL: - remote_state[9] = FUJITSU_GENERAL_MODE_COOL_BYTE9; + SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_COOL); break; case climate::CLIMATE_MODE_HEAT: - remote_state[9] = FUJITSU_GENERAL_MODE_HEAT_BYTE9; + SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_HEAT); break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_DRY: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_DRY); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN); + break; + case climate::CLIMATE_MODE_HEAT_COOL: default: - remote_state[9] = FUJITSU_GENERAL_MODE_AUTO_BYTE9; + SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO); break; - // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY, CLIMATE_MODE_10C are missing in esphome + // TODO: CLIMATE_MODE_10C is missing from esphome } - // TODO: missing support for fan speed - remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10; + // Set fan + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_HIGH: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH); + break; + case climate::CLIMATE_FAN_MEDIUM: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_MEDIUM); + break; + case climate::CLIMATE_FAN_LOW: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); + break; + case climate::CLIMATE_FAN_AUTO: + default: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); + break; + // TODO Quiet / Silent + } - // TODO: missing support for swing - // remote_state[10] = (byte) remote_state[10] | FUJITSU_GENERAL_SWING_MASK_BYTE10; + // Set swing + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_VERTICAL); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_HORIZONTAL); + break; + case climate::CLIMATE_SWING_BOTH: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_BOTH); + break; + case climate::CLIMATE_SWING_OFF: + default: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_NONE); + break; + } // TODO: missing support for outdoor unit low noise // remote_state[14] = (byte) remote_state[14] | FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14; - // CRC - remote_state[15] = 0; - for (int i = 7; i < 15; i++) { - remote_state[15] += (byte) remote_state[i]; // Addiction - } - remote_state[15] = 0x100 - remote_state[15]; // mod 256 + remote_state[FUJITSU_GENERAL_STATE_MESSAGE_LENGTH - 1] = this->checksum_state_(remote_state); - auto transmit = this->transmitter_->transmit(); - auto data = transmit.get_data(); - - data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); - - // Header - data->mark(FUJITSU_GENERAL_HEADER_MARK); - data->space(FUJITSU_GENERAL_HEADER_SPACE); - // Data - for (uint8_t i : remote_state) { - // Send all Bits from Byte Data in Reverse Order - for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask - data->mark(FUJITSU_GENERAL_BIT_MARK); - bool bit = i & mask; - data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); - // Next bits - } - } - // Footer - data->mark(FUJITSU_GENERAL_TRL_MARK); - data->space(FUJITSU_GENERAL_TRL_SPACE); - - transmit.perform(); + this->transmit_(remote_state, FUJITSU_GENERAL_STATE_MESSAGE_LENGTH); this->power_ = true; } void FujitsuGeneralClimate::transmit_off_() { - uint8_t remote_state[FUJITSU_GENERAL_OFF_LENGTH] = {0}; + ESP_LOGV(TAG, "Transmit off"); - remote_state[0] = FUJITSU_GENERAL_OFF_BYTE0; - remote_state[1] = FUJITSU_GENERAL_OFF_BYTE1; - remote_state[2] = FUJITSU_GENERAL_OFF_BYTE2; - remote_state[3] = FUJITSU_GENERAL_OFF_BYTE3; - remote_state[4] = FUJITSU_GENERAL_OFF_BYTE4; - remote_state[5] = FUJITSU_GENERAL_OFF_BYTE5; - remote_state[6] = FUJITSU_GENERAL_OFF_BYTE6; + uint8_t remote_state[FUJITSU_GENERAL_UTIL_MESSAGE_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_COMMON_BYTE0; + remote_state[1] = FUJITSU_GENERAL_COMMON_BYTE1; + remote_state[2] = FUJITSU_GENERAL_COMMON_BYTE2; + remote_state[3] = FUJITSU_GENERAL_COMMON_BYTE3; + remote_state[4] = FUJITSU_GENERAL_COMMON_BYTE4; + remote_state[5] = FUJITSU_GENERAL_MESSAGE_TYPE_OFF; + remote_state[6] = this->checksum_util_(remote_state); + + this->transmit_(remote_state, FUJITSU_GENERAL_UTIL_MESSAGE_LENGTH); + + this->power_ = false; +} + +void FujitsuGeneralClimate::transmit_(uint8_t const *message, uint8_t length) { + ESP_LOGV(TAG, "Transmit message length %d", length); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -190,22 +216,185 @@ void FujitsuGeneralClimate::transmit_off_() { data->space(FUJITSU_GENERAL_HEADER_SPACE); // Data - for (uint8_t i : remote_state) { - // Send all Bits from Byte Data in Reverse Order - for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + for (uint8_t i = 0; i < length; ++i) { + const uint8_t byte = message[i]; + for (uint8_t mask = 0b00000001; mask > 0; mask <<= 1) { // write from right to left data->mark(FUJITSU_GENERAL_BIT_MARK); - bool bit = i & mask; + bool bit = byte & mask; data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); - // Next bits } } + // Footer data->mark(FUJITSU_GENERAL_TRL_MARK); data->space(FUJITSU_GENERAL_TRL_SPACE); transmit.perform(); +} - this->power_ = false; +uint8_t FujitsuGeneralClimate::checksum_state_(uint8_t const *message) { + uint8_t checksum = 0; + for (uint8_t i = 7; i < FUJITSU_GENERAL_STATE_MESSAGE_LENGTH - 1; ++i) { + checksum += message[i]; + } + return 256 - checksum; +} + +uint8_t FujitsuGeneralClimate::checksum_util_(uint8_t const *message) { return 255 - message[5]; } + +bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { + ESP_LOGV(TAG, "Received IR message"); + + // Validate header + if (!data.expect_item(FUJITSU_GENERAL_HEADER_MARK, FUJITSU_GENERAL_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t recv_message[FUJITSU_GENERAL_STATE_MESSAGE_LENGTH] = {0}; + + // Read header + for (uint8_t byte = 0; byte < FUJITSU_GENERAL_COMMON_LENGTH; ++byte) { + // Read bit + for (uint8_t bit = 0; bit < 8; ++bit) { + if (data.expect_item(FUJITSU_GENERAL_BIT_MARK, FUJITSU_GENERAL_ONE_SPACE)) { + recv_message[byte] |= 1 << bit; // read from right to left + } else if (!data.expect_item(FUJITSU_GENERAL_BIT_MARK, FUJITSU_GENERAL_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", byte, bit); + return false; + } + } + } + + const uint8_t recv_message_type = recv_message[FUJITSU_GENERAL_MESSAGE_TYPE_BYTE]; + uint8_t recv_message_length; + + switch (recv_message_type) { + case FUJITSU_GENERAL_MESSAGE_TYPE_STATE: + ESP_LOGV(TAG, "Received state message"); + recv_message_length = FUJITSU_GENERAL_STATE_MESSAGE_LENGTH; + break; + case FUJITSU_GENERAL_MESSAGE_TYPE_OFF: + case FUJITSU_GENERAL_MESSAGE_TYPE_ECONOMY: + case FUJITSU_GENERAL_MESSAGE_TYPE_NUDGE: + ESP_LOGV(TAG, "Received util message"); + recv_message_length = FUJITSU_GENERAL_UTIL_MESSAGE_LENGTH; + break; + default: + ESP_LOGV(TAG, "Unknown message type %X", recv_message_type); + return false; + } + + // Read message body + for (uint8_t byte = FUJITSU_GENERAL_COMMON_LENGTH; byte < recv_message_length; ++byte) { + for (uint8_t bit = 0; bit < 8; ++bit) { + if (data.expect_item(FUJITSU_GENERAL_BIT_MARK, FUJITSU_GENERAL_ONE_SPACE)) { + recv_message[byte] |= 1 << bit; // read from right to left + } else if (!data.expect_item(FUJITSU_GENERAL_BIT_MARK, FUJITSU_GENERAL_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", byte, bit); + return false; + } + } + } + + for (uint8_t byte = 0; byte < recv_message_length; ++byte) { + ESP_LOGVV(TAG, "%02X", recv_message[byte]); + } + + const uint8_t recv_checksum = recv_message[recv_message_length - 1]; + uint8_t calculated_checksum; + if (recv_message_type == FUJITSU_GENERAL_MESSAGE_TYPE_STATE) { + calculated_checksum = this->checksum_state_(recv_message); + } else { + calculated_checksum = this->checksum_util_(recv_message); + } + + if (recv_checksum != calculated_checksum) { + ESP_LOGV(TAG, "Checksum fail - expected %X - got %X", calculated_checksum, recv_checksum); + return false; + } + + if (recv_message_type == FUJITSU_GENERAL_MESSAGE_TYPE_STATE) { + const uint8_t recv_tempertature = GET_NIBBLE(recv_message, FUJITSU_GENERAL_TEMPERATURE_NIBBLE); + const uint8_t offset_temperature = recv_tempertature + FUJITSU_GENERAL_TEMP_MIN; + this->target_temperature = offset_temperature; + ESP_LOGV(TAG, "Received temperature %d", offset_temperature); + + const uint8_t recv_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_MODE_NIBBLE); + ESP_LOGV(TAG, "Received mode %X", recv_mode); + switch (recv_mode) { + case FUJITSU_GENERAL_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case FUJITSU_GENERAL_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case FUJITSU_GENERAL_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case FUJITSU_GENERAL_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case FUJITSU_GENERAL_MODE_AUTO: + default: + // TODO: CLIMATE_MODE_10C is missing from esphome + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + } + + const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); + ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); + switch (recv_fan_mode) { + // TODO No Quiet / Silent in ESPH + case FUJITSU_GENERAL_FAN_SILENT: + case FUJITSU_GENERAL_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case FUJITSU_GENERAL_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case FUJITSU_GENERAL_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case FUJITSU_GENERAL_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + const uint8_t recv_swing_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_SWING_NIBBLE); + ESP_LOGV(TAG, "Received swing mode %X", recv_swing_mode); + switch (recv_swing_mode) { + case FUJITSU_GENERAL_SWING_VERTICAL: + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + break; + case FUJITSU_GENERAL_SWING_HORIZONTAL: + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + break; + case FUJITSU_GENERAL_SWING_BOTH: + this->swing_mode = climate::CLIMATE_SWING_BOTH; + break; + case FUJITSU_GENERAL_SWING_NONE: + default: + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + + this->power_ = true; + } + + else if (recv_message_type == FUJITSU_GENERAL_MESSAGE_TYPE_OFF) { + ESP_LOGV(TAG, "Received off message"); + this->mode = climate::CLIMATE_MODE_OFF; + this->power_ = false; + } + + else { + ESP_LOGV(TAG, "Received unsupprted message type %X", recv_message_type); + return false; + } + + this->publish_state(); + return true; } } // namespace fujitsu_general diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 80db81a167..8dc7a3e484 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/log.h" #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/components/climate_ir/climate_ir.h" @@ -7,9 +8,53 @@ namespace esphome { namespace fujitsu_general { +const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH +const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius + +// clang-format off +/** + * ``` + * turn + * on temp mode fan swing + * * | | | | | | * + * + * temperatures 1 1248 124 124 1 + * auto auto 18 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000100 00000000 00000000 00000000 00000000 00000000 00000100 11110001 + * auto auto 19 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10001100 00000000 00000000 00000000 00000000 00000000 00000100 11111110 + * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 + * + * on flag: + * on at 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000000 00100000 00000000 00000000 00000000 00000000 00000100 11010101 + * down to 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000000 00100000 00000000 00000000 00000000 00000000 00000100 00110101 + * + * mode options: + * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 + * cool auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 10000000 00000000 00000000 00000000 00000000 00000100 01110011 + * dry auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 01000000 00000000 00000000 00000000 00000000 00000100 10110011 + * fan (auto) (30) 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 11000000 00000000 00000000 00000000 00000000 00000100 00110011 + * heat auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 00000000 00000000 00000000 00000000 00000100 11010011 + * + * fan options: + * heat 30 high 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 10000000 00000000 00000000 00000000 00000100 01010011 + * heat 30 med 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 01000000 00000000 00000000 00000000 00000100 01010011 + * heat 30 low 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 11000000 00000000 00000000 00000000 00000100 10010011 + * heat 30 quiet 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 + * + * swing options: + * heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101 + * heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 + * ``` + */ +// clang-format on + class FujitsuGeneralClimate : public climate_ir::ClimateIR { public: - FujitsuGeneralClimate(); + FujitsuGeneralClimate() + : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_BOTH}) {} protected: /// Transmit via IR the state of this climate controller. @@ -17,6 +62,19 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { /// Transmit via IR power off command. void transmit_off_(); + /// Parse incoming message + bool on_receive(remote_base::RemoteReceiveData data) override; + + /// Transmit message as IR pulses + void transmit_(uint8_t const *message, uint8_t length); + + /// Calculate checksum for a state message + uint8_t checksum_state_(uint8_t const *message); + + /// Calculate cecksum for a util message + uint8_t checksum_util_(uint8_t const *message); + + // true if currently on - fujitsus transmit an on flag on when the remote moves from off to on bool power_{false}; }; diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e59a7e6acb..97a7ba3d54 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -2,54 +2,74 @@ import hashlib from esphome import config_validation as cv, automation from esphome import codegen as cg -from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE +from esphome.const import ( + CONF_ID, + CONF_INITIAL_VALUE, + CONF_RESTORE_VALUE, + CONF_TYPE, + CONF_VALUE, +) from esphome.core import coroutine_with_priority -globals_ns = cg.esphome_ns.namespace('globals') -GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) -GlobalVarSetAction = globals_ns.class_('GlobalVarSetAction', automation.Action) +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 -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(GlobalsComponent), - cv.Required(CONF_TYPE): cv.string_strict, - cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, - cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(GlobalsComponent), + cv.Required(CONF_TYPE): cv.string_strict, + cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) # Run with low priority so that namespaces are registered first @coroutine_with_priority(-100.0) -def to_code(config): +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) - yield cg.register_component(glob, config) + 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('globals.set', GlobalVarSetAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(GlobalsComponent), - cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), -})) -def globals_set_to_code(config, action_id, template_arg, args): - full_id, paren = yield cg.get_variable_with_full_id(config[CONF_ID]) +@automation.register_action( + "globals.set", + GlobalVarSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GlobalsComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), + } + ), +) +async def globals_set_to_code(config, action_id, template_arg, args): + full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) - templ = yield cg.templatable(config[CONF_VALUE], args, None, - to_exp=cg.RawExpression) + templ = await cg.templatable( + config[CONF_VALUE], args, None, to_exp=cg.RawExpression + ) cg.add(var.set_value(templ)) - yield var + return var 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/__init__.py b/esphome/components/gpio/__init__.py index ccb920e654..07ebb64cd2 100644 --- a/esphome/components/gpio/__init__.py +++ b/esphome/components/gpio/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg -gpio_ns = cg.esphome_ns.namespace('gpio') +CODEOWNERS = ["@esphome/core"] +gpio_ns = cg.esphome_ns.namespace("gpio") diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index e269de5a71..4d91b81a44 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -5,18 +5,22 @@ from esphome.components import binary_sensor from esphome.const import CONF_ID, CONF_PIN from .. import gpio_ns -GPIOBinarySensor = gpio_ns.class_('GPIOBinarySensor', binary_sensor.BinarySensor, cg.Component) +GPIOBinarySensor = gpio_ns.class_( + "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component +) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(GPIOBinarySensor), - cv.Required(CONF_PIN): pins.gpio_input_pin_schema -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GPIOBinarySensor), + cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index f95778af4c..cf4b088580 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace gpio { -static const char *TAG = "gpio.binary_sensor"; +static const char *const TAG = "gpio.binary_sensor"; void GPIOBinarySensor::setup() { this->pin_->setup(); 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/__init__.py b/esphome/components/gpio/output/__init__.py index bab23c824b..2fa9f4dc78 100644 --- a/esphome/components/gpio/output/__init__.py +++ b/esphome/components/gpio/output/__init__.py @@ -5,19 +5,20 @@ from esphome.components import output from esphome.const import CONF_ID, CONF_PIN from .. import gpio_ns -GPIOBinaryOutput = gpio_ns.class_('GPIOBinaryOutput', output.BinaryOutput, - cg.Component) +GPIOBinaryOutput = gpio_ns.class_("GPIOBinaryOutput", output.BinaryOutput, cg.Component) -CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(GPIOBinaryOutput), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(GPIOBinaryOutput), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield output.register_output(var, config) - yield cg.register_component(var, config) + await output.register_output(var, config) + await cg.register_component(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/gpio/output/gpio_binary_output.cpp b/esphome/components/gpio/output/gpio_binary_output.cpp index b64492f497..a7dd9ab188 100644 --- a/esphome/components/gpio/output/gpio_binary_output.cpp +++ b/esphome/components/gpio/output/gpio_binary_output.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace gpio { -static const char *TAG = "gpio.output"; +static const char *const TAG = "gpio.output"; void GPIOBinaryOutput::dump_config() { ESP_LOGCONFIG(TAG, "GPIO Binary Output:"); 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/__init__.py b/esphome/components/gpio/switch/__init__.py index f75bc71009..a03e16a2c1 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -5,33 +5,40 @@ from esphome.components import switch from esphome.const import CONF_ID, CONF_INTERLOCK, CONF_PIN, CONF_RESTORE_MODE from .. import gpio_ns -GPIOSwitch = gpio_ns.class_('GPIOSwitch', switch.Switch, cg.Component) -GPIOSwitchRestoreMode = gpio_ns.enum('GPIOSwitchRestoreMode') +GPIOSwitch = gpio_ns.class_("GPIOSwitch", switch.Switch, cg.Component) +GPIOSwitchRestoreMode = gpio_ns.enum("GPIOSwitchRestoreMode") RESTORE_MODES = { - 'RESTORE_DEFAULT_OFF': GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_OFF, - 'RESTORE_DEFAULT_ON': GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_ON, - 'ALWAYS_OFF': GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_OFF, - 'ALWAYS_ON': GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON, + "RESTORE_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_OFF, + "RESTORE_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_ON, + "ALWAYS_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_OFF, + "ALWAYS_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON, + "RESTORE_INVERTED_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + "RESTORE_INVERTED_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON, } -CONF_INTERLOCK_WAIT_TIME = 'interlock_wait_time' -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(GPIOSwitch), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_RESTORE_MODE, default='RESTORE_DEFAULT_OFF'): - cv.enum(RESTORE_MODES, upper=True, space='_'), - cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), - cv.Optional(CONF_INTERLOCK_WAIT_TIME, default='0ms'): cv.positive_time_period_milliseconds, -}).extend(cv.COMPONENT_SCHEMA) +CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time" +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GPIOSwitch), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional( + CONF_INTERLOCK_WAIT_TIME, default="0ms" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield switch.register_switch(var, config) + await cg.register_component(var, config) + await switch.register_switch(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) @@ -39,7 +46,7 @@ def to_code(config): if CONF_INTERLOCK in config: interlock = [] for it in config[CONF_INTERLOCK]: - lock = yield cg.get_variable(it) + lock = await cg.get_variable(it) interlock.append(lock) cg.add(var.set_interlock(interlock)) cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME])) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d87e5a61e6..56e0087eae 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace gpio { -static const char *TAG = "switch.gpio"; +static const char *const TAG = "switch.gpio"; float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -18,6 +18,12 @@ void GPIOSwitch::setup() { case GPIO_SWITCH_RESTORE_DEFAULT_ON: initial_state = this->get_initial_state().value_or(true); break; + case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF: + initial_state = !this->get_initial_state().value_or(true); + break; + case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON: + initial_state = !this->get_initial_state().value_or(false); + break; case GPIO_SWITCH_ALWAYS_OFF: initial_state = false; break; @@ -41,22 +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 = LOG_STR("Restore inverted (Defaults to ON)"); + break; + case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_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 dc0dd9bc95..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 { @@ -11,6 +12,8 @@ enum GPIOSwitchRestoreMode { GPIO_SWITCH_RESTORE_DEFAULT_ON, GPIO_SWITCH_ALWAYS_OFF, GPIO_SWITCH_ALWAYS_ON, + GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON, }; class GPIOSwitch : public switch_::Switch, public Component { diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index ddbd29d5f8..e485373175 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -1,25 +1,103 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_SPEED, + CONF_COURSE, + CONF_ALTITUDE, + CONF_SATELLITES, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_DEGREES, + UNIT_KILOMETER_PER_HOUR, + UNIT_METER, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor"] -gps_ns = cg.esphome_ns.namespace('gps') -GPS = gps_ns.class_('GPS', cg.Component, uart.UARTDevice) -GPSListener = gps_ns.class_('GPSListener') +CODEOWNERS = ["@coogle"] -CONF_GPS_ID = 'gps_id' +gps_ns = cg.esphome_ns.namespace("gps") +GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) +GPSListener = gps_ns.class_("GPSListener") + +CONF_GPS_ID = "gps_id" MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(GPS), -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPS), + cv.Optional(CONF_LATITUDE): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, + ), + cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, + ), + cv.Optional(CONF_COURSE): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=2, + state_class=STATE_CLASS_NONE, + ), + cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, + ), + cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("20s")) + .extend(uart.UART_DEVICE_SCHEMA), + cv.only_with_arduino, +) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) + + if CONF_LATITUDE in config: + sens = await sensor.new_sensor(config[CONF_LATITUDE]) + cg.add(var.set_latitude_sensor(sens)) + + if CONF_LONGITUDE in config: + sens = await sensor.new_sensor(config[CONF_LONGITUDE]) + cg.add(var.set_longitude_sensor(sens)) + + if CONF_SPEED in config: + sens = await sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_COURSE in config: + sens = await sensor.new_sensor(config[CONF_COURSE]) + cg.add(var.set_course_sensor(sens)) + + if CONF_ALTITUDE in config: + sens = await sensor.new_sensor(config[CONF_ALTITUDE]) + cg.add(var.set_altitude_sensor(sens)) + + if CONF_SATELLITES in config: + sens = await sensor.new_sensor(config[CONF_SATELLITES]) + 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 + cg.add_library("mikalhart/TinyGPSPlus", "1.0.2") diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 26371565f3..8c924d629c 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -1,41 +1,66 @@ +#ifdef USE_ARDUINO + #include "gps.h" #include "esphome/core/log.h" namespace esphome { namespace gps { -static const char *TAG = "gps"; +static const char *const TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::update() { + if (this->latitude_sensor_ != nullptr) + this->latitude_sensor_->publish_state(this->latitude_); + + if (this->longitude_sensor_ != nullptr) + this->longitude_sensor_->publish_state(this->longitude_); + + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(this->speed_); + + if (this->course_sensor_ != nullptr) + this->course_sensor_->publish_state(this->course_); + + if (this->altitude_sensor_ != nullptr) + this->altitude_sensor_->publish_state(this->altitude_); + + if (this->satellites_sensor_ != nullptr) + this->satellites_sensor_->publish_state(this->satellites_); +} + void GPS::loop() { while (this->available() && !this->has_time_) { if (this->tiny_gps_.encode(this->read())) { if (tiny_gps_.location.isUpdated()) { + this->latitude_ = tiny_gps_.location.lat(); + this->longitude_ = tiny_gps_.location.lng(); + ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %f", tiny_gps_.location.lat()); - ESP_LOGD(TAG, " Lon: %f", tiny_gps_.location.lng()); + ESP_LOGD(TAG, " Lat: %f", this->latitude_); + ESP_LOGD(TAG, " Lon: %f", this->longitude_); } if (tiny_gps_.speed.isUpdated()) { + this->speed_ = tiny_gps_.speed.kmph(); ESP_LOGD(TAG, "Speed:"); - ESP_LOGD(TAG, " %f km/h", tiny_gps_.speed.kmph()); + ESP_LOGD(TAG, " %f km/h", this->speed_); } if (tiny_gps_.course.isUpdated()) { + this->course_ = tiny_gps_.course.deg(); ESP_LOGD(TAG, "Course:"); - ESP_LOGD(TAG, " %f °", tiny_gps_.course.deg()); + ESP_LOGD(TAG, " %f °", this->course_); } if (tiny_gps_.altitude.isUpdated()) { + this->altitude_ = tiny_gps_.altitude.meters(); ESP_LOGD(TAG, "Altitude:"); - ESP_LOGD(TAG, " %f m", tiny_gps_.altitude.meters()); + ESP_LOGD(TAG, " %f m", this->altitude_); } if (tiny_gps_.satellites.isUpdated()) { + this->satellites_ = tiny_gps_.satellites.value(); ESP_LOGD(TAG, "Satellites:"); - ESP_LOGD(TAG, " %d", tiny_gps_.satellites.value()); - } - if (tiny_gps_.satellites.isUpdated()) { - ESP_LOGD(TAG, "HDOP:"); - ESP_LOGD(TAG, " %.2f", tiny_gps_.hdop.hdop()); + ESP_LOGD(TAG, " %d", this->satellites_); } for (auto *listener : this->listeners_) @@ -46,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 84a9248bc6..40cda145ca 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -1,7 +1,10 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" #include namespace esphome { @@ -20,17 +23,41 @@ class GPSListener { GPS *parent_; }; -class GPS : public Component, public uart::UARTDevice { +class GPS : public PollingComponent, public uart::UARTDevice { public: + void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; } + void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; } + void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; } + void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } + void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } + void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } + void register_listener(GPSListener *listener) { listener->parent_ = this; this->listeners_.push_back(listener); } float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + void update() override; + TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: + float latitude_ = -1; + float longitude_ = -1; + float speed_ = -1; + float course_ = -1; + float altitude_ = -1; + int satellites_ = -1; + + sensor::Sensor *latitude_sensor_{nullptr}; + sensor::Sensor *longitude_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *course_sensor_{nullptr}; + sensor::Sensor *altitude_sensor_{nullptr}; + sensor::Sensor *satellites_sensor_{nullptr}; + bool has_time_{false}; TinyGPSPlus tiny_gps_; std::vector listeners_{}; @@ -38,3 +65,5 @@ class GPS : public Component, public uart::UARTDevice { } // namespace gps } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/__init__.py b/esphome/components/gps/time/__init__.py index bf746d19b2..1dae22a2b2 100644 --- a/esphome/components/gps/time/__init__.py +++ b/esphome/components/gps/time/__init__.py @@ -4,20 +4,24 @@ import esphome.codegen as cg from esphome.const import CONF_ID from .. import gps_ns, GPSListener, CONF_GPS_ID, GPS -DEPENDENCIES = ['gps'] +DEPENDENCIES = ["gps"] -GPSTime = gps_ns.class_('GPSTime', time_.RealTimeClock, GPSListener) +GPSTime = gps_ns.class_( + "GPSTime", cg.PollingComponent, time_.RealTimeClock, GPSListener +) -CONFIG_SCHEMA = time_.TIME_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(GPSTime), - cv.GenerateID(CONF_GPS_ID): cv.use_id(GPS), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GPSTime), + cv.GenerateID(CONF_GPS_ID): cv.use_id(GPS), + } +).extend(cv.polling_component_schema("5min")) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield time_.register_time(var, config) - yield cg.register_component(var, config) + await time_.register_time(var, config) + await cg.register_component(var, config) - paren = yield cg.get_variable(config[CONF_GPS_ID]) + paren = await cg.get_variable(config[CONF_GPS_ID]) cg.add(paren.register_listener(var)) diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index 468ad09bac..e46f24ba8e 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -1,10 +1,12 @@ +#ifdef USE_ARDUINO + #include "gps_time.h" #include "esphome/core/log.h" namespace esphome { namespace gps { -static const char *TAG = "gps.time"; +static const char *const TAG = "gps.time"; void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) @@ -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 f6462be3e0..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" @@ -9,13 +11,11 @@ namespace gps { class GPSTime : public time::RealTimeClock, public GPSListener { public: + void update() override { this->from_tiny_gps_(this->get_tiny_gps()); }; void on_update(TinyGPSPlus &tiny_gps) override { if (!this->has_time_) this->from_tiny_gps_(tiny_gps); } - void setup() override { - this->set_interval(5 * 60 * 1000, [this]() { this->from_tiny_gps_(this->get_tiny_gps()); }); - } protected: void from_tiny_gps_(TinyGPSPlus &tiny_gps); @@ -24,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..daff89e0a6 --- /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 (uint32_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 (uint32_t 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 (uint32_t 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 (uint32_t i = 0; i < this->width_; i++) { + float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; + if (!std::isnan(v) && (thick > 0)) { + int16_t x = this->width_ - 1 - i; + uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick; + if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) { + int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2; + for (uint16_t t = 0; t < thick; t++) { + buff->draw_pixel_at(x_offset + x, y_offset + y + t, c); + } + } + } + } + } +} + +/// 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, txth = 0; + int valw = 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 (int 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/growatt_solar/__init__.py b/esphome/components/growatt_solar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp new file mode 100644 index 0000000000..ed7240ab6c --- /dev/null +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -0,0 +1,69 @@ +#include "growatt_solar.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace growatt_solar { + +static const char *const TAG = "growatt_solar"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 33; + +void GrowattSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } + +void GrowattSolar::on_modbus_data(const std::vector &data) { + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { + if (sensor == nullptr) + return; + float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit; + sensor->publish_state(value); + }; + + auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg1, size_t reg2, float unit) -> void { + float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) + + encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) * + unit; + if (sensor != nullptr) + sensor->publish_state(value); + }; + + publish_1_reg_sensor_state(this->inverter_status_, 0, 1); + + publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); +} + +void GrowattSolar::dump_config() { + ESP_LOGCONFIG(TAG, "GROWATT Solar:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); +} + +} // namespace growatt_solar +} // namespace esphome diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h new file mode 100644 index 0000000000..5356ac907a --- /dev/null +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace growatt_solar { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; + +class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { + public: + void update() override; + void on_modbus_data(const std::vector &data) override; + void dump_config() override; + + void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; } + + void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; } + void set_grid_active_power_sensor(sensor::Sensor *sensor) { this->grid_active_power_sensor_ = sensor; } + void set_pv_active_power_sensor(sensor::Sensor *sensor) { this->pv_active_power_sensor_ = sensor; } + + void set_today_production_sensor(sensor::Sensor *sensor) { this->today_production_ = sensor; } + void set_total_energy_production_sensor(sensor::Sensor *sensor) { this->total_energy_production_ = sensor; } + void set_inverter_module_temp_sensor(sensor::Sensor *sensor) { this->inverter_module_temp_ = sensor; } + + void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) { + this->phases_[phase].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor(uint8_t phase, sensor::Sensor *current_sensor) { + this->phases_[phase].current_sensor_ = current_sensor; + } + void set_active_power_sensor(uint8_t phase, sensor::Sensor *active_power_sensor) { + this->phases_[phase].active_power_sensor_ = active_power_sensor; + } + void set_voltage_sensor_pv(uint8_t pv, sensor::Sensor *voltage_sensor) { + this->pvs_[pv].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor_pv(uint8_t pv, sensor::Sensor *current_sensor) { + this->pvs_[pv].current_sensor_ = current_sensor; + } + void set_active_power_sensor_pv(uint8_t pv, sensor::Sensor *active_power_sensor) { + this->pvs_[pv].active_power_sensor_ = active_power_sensor; + } + + protected: + struct GrowattPhase { + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + } phases_[3]; + struct GrowattPV { + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + } pvs_[2]; + + sensor::Sensor *inverter_status_{nullptr}; + + sensor::Sensor *grid_frequency_sensor_{nullptr}; + sensor::Sensor *grid_active_power_sensor_{nullptr}; + + sensor::Sensor *pv_active_power_sensor_{nullptr}; + + sensor::Sensor *today_production_{nullptr}; + sensor::Sensor *total_energy_production_{nullptr}; + sensor::Sensor *inverter_module_temp_{nullptr}; +}; + +} // namespace growatt_solar +} // namespace esphome diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py new file mode 100644 index 0000000000..99936c33ee --- /dev/null +++ b/esphome/components/growatt_solar/sensor.py @@ -0,0 +1,201 @@ +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_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_CELSIUS, + UNIT_HERTZ, + UNIT_VOLT, + 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_STATUS = "inverter_status" +CONF_PV_ACTIVE_POWER = "pv_active_power" +CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" + + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@leeuwte"] + +growatt_solar_ns = cg.esphome_ns.namespace("growatt_solar") +GrowattSolar = growatt_solar_ns.class_( + "GrowattSolar", 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, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + 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, + ), +} + +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(GrowattSolar), + 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_INVERTER_STATUS): sensor.sensor_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_PV_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_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_INVERTER_MODULE_TEMP): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + 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_INVERTER_STATUS in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS]) + cg.add(var.set_inverter_status_sensor(sens)) + + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_grid_frequency_sensor(sens)) + + if CONF_ACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_ACTIVE_POWER]) + cg.add(var.set_grid_active_power_sensor(sens)) + + if CONF_PV_ACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_PV_ACTIVE_POWER]) + cg.add(var.set_pv_active_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_INVERTER_MODULE_TEMP in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_MODULE_TEMP]) + cg.add(var.set_inverter_module_temp_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/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..d7c8d544f9 --- /dev/null +++ b/esphome/components/havells_solar/sensor.py @@ -0,0 +1,291 @@ +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_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_INSULATION_OF_P_TO_GROUND: sensor.sensor_schema( + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + 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, + 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 new file mode 100644 index 0000000000..7eae863ff5 --- /dev/null +++ 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/__init__.py b/esphome/components/hbridge/light/__init__.py new file mode 100644 index 0000000000..fe5c3e9845 --- /dev/null +++ b/esphome/components/hbridge/light/__init__.py @@ -0,0 +1,30 @@ +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"] + +HBridgeLightOutput = hbridge_ns.class_( + "HBridgeLightOutput", cg.PollingComponent, light.LightOutput +) + +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(HBridgeLightOutput), + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await light.register_light(var, config) + + hside = await cg.get_variable(config[CONF_PIN_A]) + cg.add(var.set_pina_pin(hside)) + lside = await cg.get_variable(config[CONF_PIN_B]) + cg.add(var.set_pinb_pin(lside)) diff --git a/esphome/components/hbridge/light/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h new file mode 100644 index 0000000000..c309154852 --- /dev/null +++ b/esphome/components/hbridge/light/hbridge_light_output.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/light/light_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hbridge { + +// Using PollingComponent as the updates are more consistent and reduces flickering +class HBridgeLightOutput : public PollingComponent, public light::LightOutput { + public: + HBridgeLightOutput() : PollingComponent(1) {} + + void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; } + void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + return traits; + } + + void setup() override { this->forward_direction_ = false; } + + void update() override { + // 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->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(0); + this->pinb_pin_->set_level(this->pinb_duty_); + this->forward_direction_ = false; + } + } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void write_state(light::LightState *state) override { + state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); + } + + protected: + output::FloatOutput *pina_pin_; + output::FloatOutput *pinb_pin_; + float pina_duty_ = 0; + float pinb_duty_ = 0; + bool forward_direction_ = false; +}; + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 915c44b155..60e8943e67 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -1,10 +1,11 @@ #include "hdc1080.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace hdc1080 { -static const char *TAG = "hdc1080"; +static const char *const TAG = "hdc1080"; static const uint8_t HDC1080_ADDRESS = 0x40; // 0b1000000 from datasheet static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02; @@ -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 00b8296351..39727f7159 100644 --- a/esphome/components/hdc1080/sensor.py +++ b/esphome/components/hdc1080/sensor.py @@ -1,30 +1,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ - ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_CELSIUS, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -hdc1080_ns = cg.esphome_ns.namespace('hdc1080') -HDC1080Component = hdc1080_ns.class_('HDC1080Component', cg.PollingComponent, i2c.I2CDevice) +hdc1080_ns = cg.esphome_ns.namespace("hdc1080") +HDC1080Component = hdc1080_ns.class_( + "HDC1080Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(HDC1080Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC1080Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) 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..592e03f959 --- /dev/null +++ b/esphome/components/heatpumpir/climate.py @@ -0,0 +1,117 @@ +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, + "greeyac": Protocol.PROTOCOL_GREEYAC, + "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])) + + # PIO isn't updating releases, so referencing the release tag directly. See: + # https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd + cg.add_library("", "", "https://github.com/ToniA/arduino-heatpumpir.git#1.0.18") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp new file mode 100644 index 0000000000..ad3731b955 --- /dev/null +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -0,0 +1,196 @@ +#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_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // 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(); + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + + IRSenderESPHome esp_sender(this->transmitter_); + this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature + 0.5))); + + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; +} + +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(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..18d9b5040f --- /dev/null +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -0,0 +1,117 @@ +#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_GREEYAC, + 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..7546d990ea --- /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(remote_transmitter::RemoteTransmitterComponent *transmitter) + : IRSender(0), transmit_(transmitter->transmit()){}; + void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming) + void space(int space_length) override; + void mark(int mark_length) override; + + protected: + remote_transmitter::RemoteTransmitterComponent::TransmitCall transmit_; +}; + +} // namespace heatpumpir +} // namespace esphome + +#endif diff --git a/esphome/components/hitachi_ac344/__init__.py b/esphome/components/hitachi_ac344/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py new file mode 100644 index 0000000000..94b34eb955 --- /dev/null +++ b/esphome/components/hitachi_ac344/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_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") +HitachiClimate = hitachi_ac344_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_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp new file mode 100644 index 0000000000..7702baf312 --- /dev/null +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -0,0 +1,365 @@ +#include "hitachi_ac344.h" + +namespace esphome { +namespace hitachi_ac344 { + +static const char *const TAG = "climate.hitachi_ac344"; + +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_AC344_POWER_BYTE] == HITACHI_AC344_POWER_ON; } + +void HitachiClimate::set_power_(bool on) { + set_button_(HITACHI_AC344_BUTTON_POWER); + remote_state_[HITACHI_AC344_POWER_BYTE] = on ? HITACHI_AC344_POWER_ON : HITACHI_AC344_POWER_OFF; +} + +uint8_t HitachiClimate::get_mode_() { return remote_state_[HITACHI_AC344_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_AC344_MODE_FAN: + set_temp_(HITACHI_AC344_TEMP_FAN, false); + break; + case HITACHI_AC344_MODE_HEAT: + case HITACHI_AC344_MODE_COOL: + case HITACHI_AC344_MODE_DRY: + break; + default: + new_mode = HITACHI_AC344_MODE_COOL; + } + set_bits(&remote_state_[HITACHI_AC344_MODE_BYTE], 0, 4, new_mode); + if (new_mode != HITACHI_AC344_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_AC344_TEMP_MAX); + temp = std::max(temp, HITACHI_AC344_TEMP_MIN); + set_bits(&remote_state_[HITACHI_AC344_TEMP_BYTE], HITACHI_AC344_TEMP_OFFSET, HITACHI_AC344_TEMP_SIZE, temp); + if (previous_temp_ > temp) + set_button_(HITACHI_AC344_BUTTON_TEMP_DOWN); + else if (previous_temp_ < temp) + set_button_(HITACHI_AC344_BUTTON_TEMP_UP); + if (set_previous) + previous_temp_ = temp; +} + +uint8_t HitachiClimate::get_fan_() { return remote_state_[HITACHI_AC344_FAN_BYTE] >> 4 & 0xF; } + +void HitachiClimate::set_fan_(uint8_t speed) { + uint8_t new_speed = std::max(speed, HITACHI_AC344_FAN_MIN); + uint8_t fan_max = HITACHI_AC344_FAN_MAX; + + // Only 2 x low speeds in Dry mode or Auto + if (get_mode_() == HITACHI_AC344_MODE_DRY && speed == HITACHI_AC344_FAN_AUTO) { + fan_max = HITACHI_AC344_FAN_AUTO; + } else if (get_mode_() == HITACHI_AC344_MODE_DRY) { + fan_max = HITACHI_AC344_FAN_MAX_DRY; + } else if (get_mode_() == HITACHI_AC344_MODE_FAN && speed == HITACHI_AC344_FAN_AUTO) { + // Fan Mode does not have auto. Set to safe low + new_speed = HITACHI_AC344_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_AC344_BUTTON_FAN); + // Set the values + + set_bits(&remote_state_[HITACHI_AC344_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_AC344_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_AC344_BUTTON_SWINGV; // Set the button to SwingV. + else if (button == HITACHI_AC344_BUTTON_SWINGV) // Asked to unset it + // It was set previous, so use Power as a default + button = HITACHI_AC344_BUTTON_POWER; + set_button_(button); +} + +bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC344_BUTTON_SWINGV; } + +void HitachiClimate::set_swing_v_(bool on) { + set_swing_v_toggle_(on); // Set the button value. + set_bit(&remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET, on); +} + +bool HitachiClimate::get_swing_v_() { + return GETBIT8(remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET); +} + +void HitachiClimate::set_swing_h_(uint8_t position) { + if (position > HITACHI_AC344_SWINGH_LEFT_MAX) + return set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE); + set_bits(&remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE, position); + set_button_(HITACHI_AC344_BUTTON_SWINGH); +} + +uint8_t HitachiClimate::get_swing_h_() { + return GETBITS8(remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE); +} + +uint8_t HitachiClimate::get_button_() { return remote_state_[HITACHI_AC344_BUTTON_BYTE]; } + +void HitachiClimate::set_button_(uint8_t button) { remote_state_[HITACHI_AC344_BUTTON_BYTE] = button; } + +void HitachiClimate::transmit_state() { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + set_mode_(HITACHI_AC344_MODE_COOL); + break; + case climate::CLIMATE_MODE_DRY: + set_mode_(HITACHI_AC344_MODE_DRY); + break; + case climate::CLIMATE_MODE_HEAT: + set_mode_(HITACHI_AC344_MODE_HEAT); + break; + case climate::CLIMATE_MODE_HEAT_COOL: + set_mode_(HITACHI_AC344_MODE_AUTO); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + set_mode_(HITACHI_AC344_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_AC344_FAN_LOW); + break; + case climate::CLIMATE_FAN_MEDIUM: + set_fan_(HITACHI_AC344_FAN_MEDIUM); + break; + case climate::CLIMATE_FAN_HIGH: + set_fan_(HITACHI_AC344_FAN_HIGH); + break; + case climate::CLIMATE_FAN_ON: + case climate::CLIMATE_FAN_AUTO: + default: + set_fan_(HITACHI_AC344_FAN_AUTO); + } + + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + set_swing_v_(true); + set_swing_h_(HITACHI_AC344_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_VERTICAL: + set_swing_v_(true); + set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + set_swing_v_(false); + set_swing_h_(HITACHI_AC344_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_OFF: + set_swing_v_(false); + set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE); + break; + } + + // TODO: find change value to set button, now always set to power button + set_button_(HITACHI_AC344_BUTTON_POWER); + + invert_byte_pairs(remote_state_ + 3, HITACHI_AC344_STATE_LENGTH - 3); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(HITACHI_AC344_FREQ); + + uint8_t repeat = 0; + for (uint8_t r = 0; r <= repeat; r++) { + // Header + data->item(HITACHI_AC344_HDR_MARK, HITACHI_AC344_HDR_SPACE); + // Data + for (uint8_t i : remote_state_) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(HITACHI_AC344_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? HITACHI_AC344_ONE_SPACE : HITACHI_AC344_ZERO_SPACE); + } + } + // Footer + data->item(HITACHI_AC344_BIT_MARK, HITACHI_AC344_MIN_GAP); + } + transmit.perform(); + + dump_state_("Sent", remote_state_); +} + +bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) { + uint8_t power = remote_state[HITACHI_AC344_POWER_BYTE]; + ESP_LOGV(TAG, "Power: %02X %02X", remote_state[HITACHI_AC344_POWER_BYTE], power); + uint8_t mode = remote_state[HITACHI_AC344_MODE_BYTE] & 0xF; + ESP_LOGV(TAG, "Mode: %02X %02X", remote_state[HITACHI_AC344_MODE_BYTE], mode); + if (power == HITACHI_AC344_POWER_ON) { + switch (mode) { + case HITACHI_AC344_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case HITACHI_AC344_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case HITACHI_AC344_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case HITACHI_AC344_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case HITACHI_AC344_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 = + GETBITS8(remote_state[HITACHI_AC344_TEMP_BYTE], HITACHI_AC344_TEMP_OFFSET, HITACHI_AC344_TEMP_SIZE); + this->target_temperature = temperature; + ESP_LOGV(TAG, "Temperature: %02X %02u %04f", remote_state[HITACHI_AC344_TEMP_BYTE], temperature, + this->target_temperature); + return true; +} + +bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) { + uint8_t fan_mode = remote_state[HITACHI_AC344_FAN_BYTE] >> 4 & 0xF; + ESP_LOGV(TAG, "Fan: %02X %02X", remote_state[HITACHI_AC344_FAN_BYTE], fan_mode); + switch (fan_mode) { + case HITACHI_AC344_FAN_MIN: + case HITACHI_AC344_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case HITACHI_AC344_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case HITACHI_AC344_FAN_HIGH: + case HITACHI_AC344_FAN_MAX: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case HITACHI_AC344_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 = + GETBITS8(remote_state[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE); + ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC344_SWINGH_BYTE], swing_modeh); + + 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_AC344_HDR_MARK, HITACHI_AC344_HDR_SPACE)) { + ESP_LOGVV(TAG, "Header fail"); + return false; + } + + uint8_t recv_state[HITACHI_AC344_STATE_LENGTH] = {0}; + // Read all bytes. + for (uint8_t pos = 0; pos < HITACHI_AC344_STATE_LENGTH; pos++) { + // Read bit + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(HITACHI_AC344_BIT_MARK, HITACHI_AC344_ONE_SPACE)) + recv_state[pos] |= 1 << bit; + else if (!data.expect_item(HITACHI_AC344_BIT_MARK, HITACHI_AC344_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Byte %d bit %d fail", pos, bit); + return false; + } + } + } + + // Validate footer + if (!data.expect_mark(HITACHI_AC344_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_AC344_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_AC344_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_ac344 +} // namespace esphome diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h new file mode 100644 index 0000000000..c34f033d92 --- /dev/null +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -0,0 +1,121 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace hitachi_ac344 { + +const uint16_t HITACHI_AC344_HDR_MARK = 3300; // ac +const uint16_t HITACHI_AC344_HDR_SPACE = 1700; // ac +const uint16_t HITACHI_AC344_BIT_MARK = 400; +const uint16_t HITACHI_AC344_ONE_SPACE = 1250; +const uint16_t HITACHI_AC344_ZERO_SPACE = 500; +const uint32_t HITACHI_AC344_MIN_GAP = 100000; // just a guess. +const uint16_t HITACHI_AC344_FREQ = 38000; // Hz. + +const uint8_t HITACHI_AC344_BUTTON_BYTE = 11; +const uint8_t HITACHI_AC344_BUTTON_POWER = 0x13; +const uint8_t HITACHI_AC344_BUTTON_SLEEP = 0x31; +const uint8_t HITACHI_AC344_BUTTON_MODE = 0x41; +const uint8_t HITACHI_AC344_BUTTON_FAN = 0x42; +const uint8_t HITACHI_AC344_BUTTON_TEMP_DOWN = 0x43; +const uint8_t HITACHI_AC344_BUTTON_TEMP_UP = 0x44; +const uint8_t HITACHI_AC344_BUTTON_SWINGV = 0x81; +const uint8_t HITACHI_AC344_BUTTON_SWINGH = 0x8C; +const uint8_t HITACHI_AC344_BUTTON_MILDEWPROOF = 0xE2; + +const uint8_t HITACHI_AC344_TEMP_BYTE = 13; +const uint8_t HITACHI_AC344_TEMP_OFFSET = 2; +const uint8_t HITACHI_AC344_TEMP_SIZE = 6; +const uint8_t HITACHI_AC344_TEMP_MIN = 16; // 16C +const uint8_t HITACHI_AC344_TEMP_MAX = 32; // 32C +const uint8_t HITACHI_AC344_TEMP_FAN = 27; // 27C + +const uint8_t HITACHI_AC344_TIMER_BYTE = 15; + +const uint8_t HITACHI_AC344_MODE_BYTE = 25; +const uint8_t HITACHI_AC344_MODE_FAN = 1; +const uint8_t HITACHI_AC344_MODE_COOL = 3; +const uint8_t HITACHI_AC344_MODE_DRY = 5; +const uint8_t HITACHI_AC344_MODE_HEAT = 6; +const uint8_t HITACHI_AC344_MODE_AUTO = 7; + +const uint8_t HITACHI_AC344_FAN_BYTE = HITACHI_AC344_MODE_BYTE; +const uint8_t HITACHI_AC344_FAN_MIN = 1; +const uint8_t HITACHI_AC344_FAN_LOW = 2; +const uint8_t HITACHI_AC344_FAN_MEDIUM = 3; +const uint8_t HITACHI_AC344_FAN_HIGH = 4; +const uint8_t HITACHI_AC344_FAN_AUTO = 5; +const uint8_t HITACHI_AC344_FAN_MAX = 6; +const uint8_t HITACHI_AC344_FAN_MAX_DRY = 2; + +const uint8_t HITACHI_AC344_POWER_BYTE = 27; +const uint8_t HITACHI_AC344_POWER_ON = 0xF1; +const uint8_t HITACHI_AC344_POWER_OFF = 0xE1; + +const uint8_t HITACHI_AC344_SWINGH_BYTE = 35; +const uint8_t HITACHI_AC344_SWINGH_OFFSET = 0; // Mask 0b00000xxx +const uint8_t HITACHI_AC344_SWINGH_SIZE = 3; // Mask 0b00000xxx +const uint8_t HITACHI_AC344_SWINGH_AUTO = 0; // 0b000 +const uint8_t HITACHI_AC344_SWINGH_RIGHT_MAX = 1; // 0b001 +const uint8_t HITACHI_AC344_SWINGH_RIGHT = 2; // 0b010 +const uint8_t HITACHI_AC344_SWINGH_MIDDLE = 3; // 0b011 +const uint8_t HITACHI_AC344_SWINGH_LEFT = 4; // 0b100 +const uint8_t HITACHI_AC344_SWINGH_LEFT_MAX = 5; // 0b101 + +const uint8_t HITACHI_AC344_SWINGV_BYTE = 37; +const uint8_t HITACHI_AC344_SWINGV_OFFSET = 5; // Mask 0b00x00000 + +const uint8_t HITACHI_AC344_MILDEWPROOF_BYTE = HITACHI_AC344_SWINGV_BYTE; +const uint8_t HITACHI_AC344_MILDEWPROOF_OFFSET = 2; // Mask 0b00000x00 + +const uint16_t HITACHI_AC344_STATE_LENGTH = 43; +const uint16_t HITACHI_AC344_BITS = HITACHI_AC344_STATE_LENGTH * 8; + +#define GETBIT8(a, b) ((a) & ((uint8_t) 1 << (b))) +#define 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_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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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_ac344 +} // namespace esphome 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..713bc0be25 --- /dev/null +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -0,0 +1,366 @@ +#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 & 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 2b3b4ec2d9..ecdaa07ab2 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -4,17 +4,34 @@ namespace esphome { namespace hlw8012 { -static const char *TAG = "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); @@ -79,6 +90,12 @@ void HLW8012Component::update() { this->power_sensor_->publish_state(power); } + if (this->energy_sensor_ != nullptr) { + cf_total_pulses_ += raw_cf; + float energy = cf_total_pulses_ * this->power_multiplier_ / 3600; + this->energy_sensor_->publish_state(energy); + } + if (this->change_mode_at_++ == this->change_mode_every_) { this->current_mode_ = !this->current_mode_; ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE"); diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 4e5dc0f67f..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,15 +26,17 @@ 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; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } protected: uint32_t nth_value_{0}; @@ -37,14 +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 e1f02b8fd2..033cccc3d4 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -2,62 +2,118 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_INITIAL_MODE, CONF_CURRENT, \ - CONF_CURRENT_RESISTOR, CONF_ID, CONF_POWER, CONF_SEL_PIN, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, \ - ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT +from esphome.const import ( + CONF_CHANGE_MODE_EVERY, + CONF_INITIAL_MODE, + CONF_CURRENT, + CONF_CURRENT_RESISTOR, + CONF_ID, + 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, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_WATT_HOURS, +) -AUTO_LOAD = ['pulse_counter'] +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") -hlw8012_ns = cg.esphome_ns.namespace('hlw8012') -HLW8012Component = hlw8012_ns.class_('HLW8012Component', cg.PollingComponent) -HLW8012InitialMode = hlw8012_ns.enum('HLW8012InitialMode') INITIAL_MODES = { CONF_CURRENT: HLW8012InitialMode.HLW8012_INITIAL_MODE_CURRENT, CONF_VOLTAGE: HLW8012InitialMode.HLW8012_INITIAL_MODE_VOLTAGE, } -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), +MODELS = { + "HLW8012": HLW8012SensorModels.HLW8012_SENSOR_MODEL_HLW8012, + "CSE7759": HLW8012SensorModels.HLW8012_SENSOR_MODEL_CSE7759, + "BL0937": HLW8012SensorModels.HLW8012_SENSOR_MODEL_BL0937, +} - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), - - cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, - cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, - cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(cv.uint32_t, cv.Range(min=1)), - cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(*INITIAL_MODES, lower=True), -}).extend(cv.polling_component_schema('60s')) +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), + cv.Required(CONF_CF1_PIN): cv.All(pins.internal_gpio_input_pullup_pin_schema), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=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) + ), + cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of( + *INITIAL_MODES, lower=True + ), + } +).extend(cv.polling_component_schema("60s")) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - sel = yield cg.gpio_pin_expression(config[CONF_SEL_PIN]) + sel = await cg.gpio_pin_expression(config[CONF_SEL_PIN]) cg.add(var.set_sel_pin(sel)) - cf = yield cg.gpio_pin_expression(config[CONF_CF_PIN]) + cf = await cg.gpio_pin_expression(config[CONF_CF_PIN]) cg.add(var.set_cf_pin(cf)) - cf1 = yield cg.gpio_pin_expression(config[CONF_CF1_PIN]) + cf1 = await cg.gpio_pin_expression(config[CONF_CF1_PIN]) cg.add(var.set_cf1_pin(cf1)) if CONF_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) cg.add(var.set_voltage_sensor(sens)) if CONF_CURRENT in config: - sens = yield sensor.new_sensor(config[CONF_CURRENT]) + sens = await sensor.new_sensor(config[CONF_CURRENT]) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: - sens = yield sensor.new_sensor(config[CONF_POWER]) + sens = await sensor.new_sensor(config[CONF_POWER]) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_ENERGY]) + cg.add(var.set_energy_sensor(sens)) cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY])) cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]])) + cg.add(var.set_sensor_model(config[CONF_MODEL])) diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h new file mode 100644 index 0000000000..42d900a262 --- /dev/null +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace esphome { +namespace hm3301 { + +class AbstractAQICalculator { + public: + virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h new file mode 100644 index 0000000000..08d1dc2921 --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator.h @@ -0,0 +1,48 @@ +#pragma once + +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class AQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 6; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, 604}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h new file mode 100644 index 0000000000..55608b6e51 --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -0,0 +1,29 @@ +#pragma once + +#include "caqi_calculator.h" +#include "aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; + +class AQICalculatorFactory { + public: + AbstractAQICalculator *get_calculator(AQICalculatorType type) { + if (type == 0) { + return caqi_calculator_; + } else if (type == 1) { + return aqi_calculator_; + } + + return nullptr; + } + + protected: + CAQICalculator *caqi_calculator_ = new CAQICalculator(); + AQICalculator *aqi_calculator_ = new AQICalculator(); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h new file mode 100644 index 0000000000..1ec61f2416 --- /dev/null +++ b/esphome/components/hm3301/caqi_calculator.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/log.h" +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class CAQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 5; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + if (grid_index == -1) { + return -1; + } + + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 6456ee354a..a2bef2a01d 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,10 +1,10 @@ -#include "hm3301.h" #include "esphome/core/log.h" +#include "hm3301.h" namespace esphome { namespace hm3301 { -static const char *TAG = "hm3301.sensor"; +static const char *const TAG = "hm3301.sensor"; static const uint8_t PM_1_0_VALUE_INDEX = 5; static const uint8_t PM_2_5_VALUE_INDEX = 6; @@ -12,9 +12,8 @@ static const uint8_t PM_10_0_VALUE_INDEX = 7; void HM3301Component::setup() { ESP_LOGCONFIG(TAG, "Setting up HM3301..."); - hm3301_ = new HM330X(); - error_code_ = hm3301_->init(); - if (error_code_ != NO_ERROR) { + if (i2c::ERROR_OK != this->write(&SELECT_COMM_CMD, 1)) { + error_code_ = ERROR_COMM; this->mark_failed(); return; } @@ -30,12 +29,13 @@ void HM3301Component::dump_config() { 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(" ", "AQI", this->aqi_sensor_); } float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } void HM3301Component::update() { - if (!this->read_sensor_value_(data_buffer_)) { + if (this->read(data_buffer_, 29) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Read result failed"); this->status_set_warning(); return; @@ -47,24 +47,43 @@ void HM3301Component::update() { return; } + int16_t pm_1_0_value = -1; if (this->pm_1_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); - this->pm_1_0_sensor_->publish_state(value); + pm_1_0_value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); } + + int16_t pm_2_5_value = -1; if (this->pm_2_5_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); - this->pm_2_5_sensor_->publish_state(value); + pm_2_5_value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); } + + int16_t pm_10_0_value = -1; if (this->pm_10_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); - this->pm_10_0_sensor_->publish_state(value); + pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + } + + int8_t aqi_value = -1; + if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); + } + + if (pm_1_0_value != -1) { + this->pm_1_0_sensor_->publish_state(pm_1_0_value); + } + if (pm_2_5_value != -1) { + this->pm_2_5_sensor_->publish_state(pm_2_5_value); + } + if (pm_10_0_value != -1) { + this->pm_10_0_sensor_->publish_state(pm_10_0_value); + } + if (aqi_value != -1) { + this->aqi_sensor_->publish_state(aqi_value); } this->status_clear_warning(); } -bool HM3301Component::read_sensor_value_(uint8_t *data) { return !hm3301_->read_sensor_value(data, 29); } - bool HM3301Component::validate_checksum_(const uint8_t *data) { uint8_t sum = 0; for (int i = 0; i < 28; i++) { diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 0fbb32612e..e13ffa466e 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -3,12 +3,13 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" - -#include +#include "aqi_calculator_factory.h" namespace esphome { namespace hm3301 { +static const uint8_t SELECT_COMM_CMD = 0X88; + class HM3301Component : public PollingComponent, public i2c::I2CDevice { public: HM3301Component() = default; @@ -16,6 +17,9 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } + + void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } void setup() override; void dump_config() override; @@ -23,17 +27,23 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - HM330X *hm3301_; - - HM330XErrorCode error_code_{NO_ERROR}; + enum { + NO_ERROR = 0, + ERROR_PARAM = -1, + ERROR_COMM = -2, + ERROR_OTHERS = -128, + } error_code_{NO_ERROR}; uint8_t data_buffer_[30]; sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *aqi_sensor_{nullptr}; + + AQICalculatorType aqi_calc_type_; + AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory(); - bool read_sensor_value_(uint8_t *); bool validate_checksum_(const uint8_t *); uint16_t get_sensor_value_(const uint8_t *, uint8_t); }; diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 718d0a20bb..8e9ee4c6fb 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -1,43 +1,110 @@ 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_2_5, CONF_PM_10_0, CONF_PM_1_0, \ - UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON +from esphome.const import ( + CONF_ID, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_PM_1_0, + DEVICE_CLASS_AQI, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -hm3301_ns = cg.esphome_ns.namespace('hm3301') -HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) +hm3301_ns = cg.esphome_ns.namespace("hm3301") +HM3301Component = hm3301_ns.class_( + "HM3301Component", cg.PollingComponent, i2c.I2CDevice +) +AQICalculatorType = hm3301_ns.enum("AQICalculatorType") -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(HM3301Component), +CONF_AQI = "aqi" +CONF_CALCULATION_TYPE = "calculation_type" +UNIT_INDEX = "index" - cv.Optional(CONF_PM_1_0): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - cv.Optional(CONF_PM_2_5): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - cv.Optional(CONF_PM_10_0): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40))) +AQI_CALCULATION_TYPE = { + "CAQI": AQICalculatorType.CAQI_TYPE, + "AQI": AQICalculatorType.AQI_TYPE, +} -def to_code(config): +def _validate(config): + if CONF_AQI in config and CONF_PM_2_5 not in config: + raise cv.Invalid("AQI sensor requires PM 2.5") + if CONF_AQI in config and CONF_PM_10_0 not in config: + raise cv.Invalid("AQI sensor requires PM 10 sensors") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HM3301Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AQI): sensor.sensor_schema( + 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( + AQI_CALCULATION_TYPE, upper=True + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)), + _validate, +) + + +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_PM_1_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + sens = await sensor.new_sensor(config[CONF_PM_1_0]) cg.add(var.set_pm_1_0_sensor(sens)) if CONF_PM_2_5 in config: - sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + sens = await sensor.new_sensor(config[CONF_PM_2_5]) cg.add(var.set_pm_2_5_sensor(sens)) if CONF_PM_10_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) - # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 - cg.add_library('6306', '1.0.3') + if CONF_AQI in config: + sens = await sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi_sensor(sens)) + cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index 9094548bb3..de3903d7e2 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace hmc5883l { -static const char *TAG = "hmc5883l"; +static const char *const TAG = "hmc5883l"; static const uint8_t HMC5883L_ADDRESS = 0x1E; static const uint8_t HMC5883L_REGISTER_CONFIG_A = 0x00; static const uint8_t HMC5883L_REGISTER_CONFIG_B = 0x01; diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index b063284698..9d8701079e 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -1,22 +1,34 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, - UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, - CONF_UPDATE_INTERVAL) +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_OVERSAMPLING, + CONF_RANGE, + ICON_MAGNET, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_MICROTESLA, + UNIT_DEGREES, + ICON_SCREEN_ROTATION, + CONF_UPDATE_INTERVAL, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -hmc5883l_ns = cg.esphome_ns.namespace('hmc5883l') +hmc5883l_ns = cg.esphome_ns.namespace("hmc5883l") -CONF_FIELD_STRENGTH_X = 'field_strength_x' -CONF_FIELD_STRENGTH_Y = 'field_strength_y' -CONF_FIELD_STRENGTH_Z = 'field_strength_z' -CONF_HEADING = 'heading' +CONF_FIELD_STRENGTH_X = "field_strength_x" +CONF_FIELD_STRENGTH_Y = "field_strength_y" +CONF_FIELD_STRENGTH_Z = "field_strength_z" +CONF_HEADING = "heading" -HMC5883LComponent = hmc5883l_ns.class_('HMC5883LComponent', cg.PollingComponent, i2c.I2CDevice) +HMC5883LComponent = hmc5883l_ns.class_( + "HMC5883LComponent", cg.PollingComponent, i2c.I2CDevice +) -HMC5883LOversampling = hmc5883l_ns.enum('HMC5883LOversampling') +HMC5883LOversampling = hmc5883l_ns.enum("HMC5883LOversampling") HMC5883LOversamplings = { 1: HMC5883LOversampling.HMC5883L_OVERSAMPLING_1, 2: HMC5883LOversampling.HMC5883L_OVERSAMPLING_2, @@ -24,7 +36,7 @@ HMC5883LOversamplings = { 8: HMC5883LOversampling.HMC5883L_OVERSAMPLING_8, } -HMC5883LDatarate = hmc5883l_ns.enum('HMC5883LDatarate') +HMC5883LDatarate = hmc5883l_ns.enum("HMC5883LDatarate") HMC5883LDatarates = { 0.75: HMC5883LDatarate.HMC5883L_DATARATE_0_75_HZ, 1.5: HMC5883LDatarate.HMC5883L_DATARATE_1_5_HZ, @@ -35,7 +47,7 @@ HMC5883LDatarates = { 75: HMC5883LDatarate.HMC5883L_DATARATE_75_0_HZ, } -HMC5883LRange = hmc5883l_ns.enum('HMC5883LRange') +HMC5883LRange = hmc5883l_ns.enum("HMC5883LRange") HMC5883L_RANGES = { 88: HMC5883LRange.HMC5883L_RANGE_88_UT, 130: HMC5883LRange.HMC5883L_RANGE_130_UT, @@ -59,53 +71,74 @@ def validate_enum(enum_values, units=None, int=True): value = cv.string(value) for unit in _units: if value.endswith(unit): - value = value[:-len(unit)] + value = value[: -len(unit)] break return enum_bound(value) + return validate_enum_bound -field_strength_schema = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1) -heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) +field_strength_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, +) +heading_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(HMC5883LComponent), - cv.Optional(CONF_ADDRESS): cv.i2c_address, - cv.Optional(CONF_OVERSAMPLING, default='1x'): validate_enum(HMC5883LOversamplings, units="x"), - cv.Optional(CONF_RANGE, default='130µT'): validate_enum(HMC5883L_RANGES, units=["uT", "µT"]), - cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, - cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, - cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, - cv.Optional(CONF_HEADING): heading_schema, -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x1E)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HMC5883LComponent), + cv.Optional(CONF_ADDRESS): cv.i2c_address, + cv.Optional(CONF_OVERSAMPLING, default="1x"): validate_enum( + HMC5883LOversamplings, units="x" + ), + cv.Optional(CONF_RANGE, default="130µT"): validate_enum( + HMC5883L_RANGES, units=["uT", "µT"] + ), + cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, + cv.Optional(CONF_HEADING): heading_schema, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x1E)) +) def auto_data_rate(config): - interval_sec = config[CONF_UPDATE_INTERVAL].seconds - interval_hz = 1.0/interval_sec + interval_msec = config[CONF_UPDATE_INTERVAL].total_milliseconds + interval_hz = 1000.0 / interval_msec for datarate in sorted(HMC5883LDatarates.keys()): if float(datarate) >= interval_hz: return HMC5883LDatarates[datarate] return HMC5883LDatarates[75] -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) cg.add(var.set_datarate(auto_data_rate(config))) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: - sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) + sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) cg.add(var.set_x_sensor(sens)) if CONF_FIELD_STRENGTH_Y in config: - sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y]) + sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y]) cg.add(var.set_y_sensor(sens)) if CONF_FIELD_STRENGTH_Z in config: - sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z]) + sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z]) cg.add(var.set_z_sensor(sens)) if CONF_HEADING in config: - sens = yield sensor.new_sensor(config[CONF_HEADING]) + sens = await sensor.new_sensor(config[CONF_HEADING]) cg.add(var.set_heading_sensor(sens)) diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 9fb3836d49..c151abc250 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg -homeassistant_ns = cg.esphome_ns.namespace('homeassistant') +CODEOWNERS = ["@OttoWinter"] +homeassistant_ns = cg.esphome_ns.namespace("homeassistant") diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index 88e2f2fcb2..4972466aac 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,23 +1,28 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ENTITY_ID, CONF_ID +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID from .. import homeassistant_ns -DEPENDENCIES = ['api'] -HomeassistantBinarySensor = homeassistant_ns.class_('HomeassistantBinarySensor', - binary_sensor.BinarySensor, - cg.Component) +DEPENDENCIES = ["api"] +HomeassistantBinarySensor = homeassistant_ns.class_( + "HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component +) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(HomeassistantBinarySensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HomeassistantBinarySensor), + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_ATTRIBUTE): cv.string, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) + if CONF_ATTRIBUTE in config: + cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp index 203f6d8a24..cea02f072a 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp @@ -5,32 +5,41 @@ namespace esphome { namespace homeassistant { -static const char *TAG = "homeassistant.binary_sensor"; +static const char *const TAG = "homeassistant.binary_sensor"; void HomeassistantBinarySensor::setup() { - api::global_api_server->subscribe_home_assistant_state(this->entity_id_, [this](std::string state) { - auto val = parse_on_off(state.c_str()); - switch (val) { - case PARSE_NONE: - case PARSE_TOGGLE: - ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); - break; - case PARSE_ON: - case PARSE_OFF: - bool new_state = val == PARSE_ON; - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); - if (this->initial_) - this->publish_initial_state(new_state); - else - this->publish_state(new_state); - break; - } - this->initial_ = false; - }); + api::global_api_server->subscribe_home_assistant_state( + this->entity_id_, this->attribute_, [this](const std::string &state) { + auto val = parse_on_off(state.c_str()); + switch (val) { + case PARSE_NONE: + case PARSE_TOGGLE: + ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); + break; + case PARSE_ON: + case PARSE_OFF: + bool new_state = val == PARSE_ON; + if (this->attribute_.has_value()) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_.c_str(), + this->attribute_.value().c_str(), ONOFF(new_state)); + } else { + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + } + if (this->initial_) + this->publish_initial_state(new_state); + else + this->publish_state(new_state); + break; + } + this->initial_ = false; + }); } void HomeassistantBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Homeassistant Binary Sensor", this); ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + if (this->attribute_.has_value()) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + } } float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h index e468fd00eb..7026496295 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h @@ -9,12 +9,14 @@ namespace homeassistant { class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component { public: void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } + void set_attribute(const std::string &attribute) { attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: std::string entity_id_; + optional attribute_; bool initial_{true}; }; diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 577efca79b..cf29db8bb8 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -1,23 +1,37 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_ENTITY_ID, CONF_ID, ICON_EMPTY, UNIT_EMPTY +from esphome.const import ( + CONF_ATTRIBUTE, + CONF_ENTITY_ID, + CONF_ID, + STATE_CLASS_NONE, +) from .. import homeassistant_ns -DEPENDENCIES = ['api'] +DEPENDENCIES = ["api"] -HomeassistantSensor = homeassistant_ns.class_('HomeassistantSensor', sensor.Sensor, - cg.Component) +HomeassistantSensor = homeassistant_ns.class_( + "HomeassistantSensor", sensor.Sensor, cg.Component +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1).extend({ - cv.GenerateID(): cv.declare_id(HomeassistantSensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, -}) +CONFIG_SCHEMA = sensor.sensor_schema( + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, +).extend( + { + cv.GenerateID(): cv.declare_id(HomeassistantSensor), + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_ATTRIBUTE): cv.string, + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) + if CONF_ATTRIBUTE in config: + cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index 6b1299f70e..f5e73c8854 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -5,24 +5,33 @@ namespace esphome { namespace homeassistant { -static const char *TAG = "homeassistant.sensor"; +static const char *const TAG = "homeassistant.sensor"; void HomeassistantSensor::setup() { - api::global_api_server->subscribe_home_assistant_state(this->entity_id_, [this](std::string state) { - auto val = parse_float(state); - if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); - this->publish_state(NAN); - return; - } + api::global_api_server->subscribe_home_assistant_state( + this->entity_id_, this->attribute_, [this](const std::string &state) { + auto val = parse_number(state); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); + this->publish_state(NAN); + return; + } - ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val); - this->publish_state(*val); - }); + if (this->attribute_.has_value()) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_.c_str(), + this->attribute_.value().c_str(), *val); + } else { + ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val); + } + this->publish_state(*val); + }); } void HomeassistantSensor::dump_config() { LOG_SENSOR("", "Homeassistant Sensor", this); ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + if (this->attribute_.has_value()) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + } } float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.h b/esphome/components/homeassistant/sensor/homeassistant_sensor.h index baca6594c1..53b288d7d4 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.h +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.h @@ -9,12 +9,14 @@ namespace homeassistant { class HomeassistantSensor : public sensor::Sensor, public Component { public: void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } + void set_attribute(const std::string &attribute) { attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: std::string entity_id_; + optional attribute_; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index 2d06473f3c..b63d45b9ce 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,23 +1,29 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ENTITY_ID, CONF_ID +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID from .. import homeassistant_ns -DEPENDENCIES = ['api'] +DEPENDENCIES = ["api"] -HomeassistantTextSensor = homeassistant_ns.class_('HomeassistantTextSensor', - text_sensor.TextSensor, cg.Component) +HomeassistantTextSensor = homeassistant_ns.class_( + "HomeassistantTextSensor", text_sensor.TextSensor, cg.Component +) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(HomeassistantTextSensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, -}) +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HomeassistantTextSensor), + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_ATTRIBUTE): cv.string, + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) + if CONF_ATTRIBUTE in config: + cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp index 67fbf3cd5d..9b933fbbbe 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp @@ -5,18 +5,27 @@ namespace esphome { namespace homeassistant { -static const char *TAG = "homeassistant.text_sensor"; +static const char *const TAG = "homeassistant.text_sensor"; +void HomeassistantTextSensor::setup() { + api::global_api_server->subscribe_home_assistant_state( + this->entity_id_, this->attribute_, [this](const std::string &state) { + if (this->attribute_.has_value()) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_.c_str(), + this->attribute_.value().c_str(), state.c_str()); + } else { + ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str()); + } + this->publish_state(state); + }); +} void HomeassistantTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this); ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + if (this->attribute_.has_value()) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + } } -void HomeassistantTextSensor::setup() { - api::global_api_server->subscribe_home_assistant_state(this->entity_id_, [this](std::string state) { - ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str()); - this->publish_state(state); - }); -} - +float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h index 02a74af1db..ce6b2c2c3f 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h @@ -9,11 +9,14 @@ namespace homeassistant { class HomeassistantTextSensor : public text_sensor::TextSensor, public Component { public: void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void dump_config() override; + void set_attribute(const std::string &attribute) { attribute_ = attribute; } void setup() override; + void dump_config() override; + float get_setup_priority() const override; protected: std::string entity_id_; + optional attribute_; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/time/__init__.py b/esphome/components/homeassistant/time/__init__.py index fd40de68d9..0040988794 100644 --- a/esphome/components/homeassistant/time/__init__.py +++ b/esphome/components/homeassistant/time/__init__.py @@ -4,17 +4,19 @@ import esphome.codegen as cg from esphome.const import CONF_ID from .. import homeassistant_ns -DEPENDENCIES = ['api'] +DEPENDENCIES = ["api"] -HomeassistantTime = homeassistant_ns.class_('HomeassistantTime', time_.RealTimeClock) +HomeassistantTime = homeassistant_ns.class_("HomeassistantTime", time_.RealTimeClock) -CONFIG_SCHEMA = time_.TIME_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(HomeassistantTime), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HomeassistantTime), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield time_.register_time(var, config) - yield cg.register_component(var, config) - cg.add_define('USE_HOMEASSISTANT_TIME') + await time_.register_time(var, config) + await cg.register_component(var, config) + cg.add_define("USE_HOMEASSISTANT_TIME") diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index e9d97690fb..9f5239404a 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -4,23 +4,19 @@ namespace esphome { namespace homeassistant { -static const char *TAG = "homeassistant.time"; +static const char *const TAG = "homeassistant.time"; void HomeassistantTime::dump_config() { ESP_LOGCONFIG(TAG, "Home Assistant Time:"); ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } + float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } -void HomeassistantTime::setup() { - global_homeassistant_time = this; - this->set_interval(15 * 60 * 1000, []() { - // re-request time every 15 minutes - api::global_api_server->request_time(); - }); -} +void HomeassistantTime::setup() { global_homeassistant_time = this; } -HomeassistantTime *global_homeassistant_time = nullptr; +void HomeassistantTime::update() { api::global_api_server->request_time(); } +HomeassistantTime *global_homeassistant_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 8ab09d1185..36e28ea16b 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -10,12 +10,13 @@ namespace homeassistant { class HomeassistantTime : public time::RealTimeClock { public: void setup() override; + void update() override; void dump_config() override; void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); } float get_setup_priority() const override; }; -extern HomeassistantTime *global_homeassistant_time; +extern HomeassistantTime *global_homeassistant_time; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace homeassistant } // namespace esphome 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..bd1c82c96b --- /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 = parse_number(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2)).value_or(0); + 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 e79df12a6c..774d6a0f91 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -3,141 +3,191 @@ import urllib.parse as urlparse import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ - CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_1, CONF_URL -from esphome.core import CORE, Lambda -from esphome.core_config import PLATFORMIO_ESP8266_LUT +from esphome.const import ( + CONF_ID, + CONF_TIMEOUT, + CONF_METHOD, + CONF_TRIGGER_ID, + CONF_URL, + CONF_ESP8266_DISABLE_SSL_SUPPORT, +) +from esphome.core import Lambda, CORE -DEPENDENCIES = ['network'] -AUTO_LOAD = ['json'] +DEPENDENCIES = ["network"] +AUTO_LOAD = ["json"] -http_request_ns = cg.esphome_ns.namespace('http_request') -HttpRequestComponent = http_request_ns.class_('HttpRequestComponent', cg.Component) -HttpRequestSendAction = http_request_ns.class_('HttpRequestSendAction', automation.Action) +http_request_ns = cg.esphome_ns.namespace("http_request") +HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component) +HttpRequestSendAction = http_request_ns.class_( + "HttpRequestSendAction", automation.Action +) +HttpRequestResponseTrigger = http_request_ns.class_( + "HttpRequestResponseTrigger", automation.Trigger +) -CONF_HEADERS = 'headers' -CONF_USERAGENT = 'useragent' -CONF_BODY = 'body' -CONF_JSON = 'json' -CONF_VERIFY_SSL = 'verify_ssl' - - -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 +CONF_HEADERS = "headers" +CONF_USERAGENT = "useragent" +CONF_BODY = "body" +CONF_JSON = "json" +CONF_VERIFY_SSL = "verify_ssl" +CONF_ON_RESPONSE = "on_response" def validate_url(value): value = cv.string(value) try: parsed = list(urlparse.urlparse(value)) - except Exception: - raise cv.Invalid('Invalid URL') + except Exception as err: + raise cv.Invalid("Invalid URL") from err if not parsed[0] or not parsed[1]: - raise cv.Invalid('URL must have a URL scheme and host') + raise cv.Invalid("URL must have a URL scheme and host") - if parsed[0] not in ['http', 'https']: - raise cv.Invalid('Scheme must be http or https') + if parsed[0] not in ["http", "https"]: + raise cv.Invalid("Scheme must be http or https") if not parsed[2]: - parsed[2] = '/' + parsed[2] = "/" return urlparse.urlunparse(parsed) def validate_secure_url(config): url_ = config[CONF_URL] - if config.get(CONF_VERIFY_SSL) and not isinstance(url_, Lambda) \ - and url_.lower().startswith('https:'): - raise cv.Invalid('Currently ESPHome doesn\'t support SSL verification. ' - 'Set \'verify_ssl: false\' to make insecure HTTPS requests.') + if ( + config.get(CONF_VERIFY_SSL) + and not isinstance(url_, Lambda) + and url_.lower().startswith("https:") + ): + raise cv.Invalid( + "Currently ESPHome doesn't support SSL verification. " + "Set 'verify_ssl: false' to make insecure HTTPS requests." + ) return config -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(HttpRequestComponent), - cv.Optional(CONF_USERAGENT, 'ESPHome'): cv.string, - cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds, -}).add_extra(validate_framework).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HttpRequestComponent), + cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string, + 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 + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 5, 1), + esp32_arduino=cv.Version(0, 0, 0), + ), +) -def to_code(config): +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])) - yield cg.register_component(var, config) + 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) + if CORE.is_esp8266: + cg.add_library("ESP8266HTTPClient", None) + + await cg.register_component(var, config) -HTTP_REQUEST_ACTION_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(HttpRequestComponent), - cv.Required(CONF_URL): cv.templatable(validate_url), - cv.Optional(CONF_HEADERS): cv.All(cv.Schema({cv.string: cv.templatable(cv.string)})), - cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, -}).add_extra(validate_secure_url) +HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(HttpRequestComponent), + cv.Required(CONF_URL): cv.templatable(validate_url), + cv.Optional(CONF_HEADERS): cv.All( + cv.Schema({cv.string: cv.templatable(cv.string)}) + ), + cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} + ), + } +).add_extra(validate_secure_url) HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf( - CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({ - cv.Optional(CONF_METHOD, default='GET'): cv.one_of('GET', upper=True), - }) + CONF_URL, + HTTP_REQUEST_ACTION_SCHEMA.extend( + { + cv.Optional(CONF_METHOD, default="GET"): cv.one_of("GET", upper=True), + } + ), ) HTTP_REQUEST_POST_ACTION_SCHEMA = automation.maybe_conf( - CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({ - cv.Optional(CONF_METHOD, default='POST'): cv.one_of('POST', upper=True), - cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string), - cv.Exclusive(CONF_JSON, 'body'): cv.Any( - cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), - ), - }) -) -HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend({ - cv.Required(CONF_METHOD): cv.one_of('GET', 'POST', 'PUT', 'DELETE', 'PATCH', upper=True), - cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string), - cv.Exclusive(CONF_JSON, 'body'): cv.Any( - cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + CONF_URL, + HTTP_REQUEST_ACTION_SCHEMA.extend( + { + cv.Optional(CONF_METHOD, default="POST"): cv.one_of("POST", upper=True), + cv.Exclusive(CONF_BODY, "body"): cv.templatable(cv.string), + cv.Exclusive(CONF_JSON, "body"): cv.Any( + cv.lambda_, + cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + ), + } ), -}) +) +HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend( + { + cv.Required(CONF_METHOD): cv.one_of( + "GET", "POST", "PUT", "DELETE", "PATCH", upper=True + ), + cv.Exclusive(CONF_BODY, "body"): cv.templatable(cv.string), + cv.Exclusive(CONF_JSON, "body"): cv.Any( + cv.lambda_, + cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + ), + } +) -@automation.register_action('http_request.get', HttpRequestSendAction, - HTTP_REQUEST_GET_ACTION_SCHEMA) -@automation.register_action('http_request.post', HttpRequestSendAction, - HTTP_REQUEST_POST_ACTION_SCHEMA) -@automation.register_action('http_request.send', HttpRequestSendAction, - HTTP_REQUEST_SEND_ACTION_SCHEMA) -def http_request_action_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "http_request.get", HttpRequestSendAction, HTTP_REQUEST_GET_ACTION_SCHEMA +) +@automation.register_action( + "http_request.post", HttpRequestSendAction, HTTP_REQUEST_POST_ACTION_SCHEMA +) +@automation.register_action( + "http_request.send", HttpRequestSendAction, HTTP_REQUEST_SEND_ACTION_SCHEMA +) +async def http_request_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_URL], args, cg.std_string) + template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) cg.add(var.set_method(config[CONF_METHOD])) if CONF_BODY in config: - template_ = yield cg.templatable(config[CONF_BODY], args, cg.std_string) + template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string) cg.add(var.set_body(template_)) if CONF_JSON in config: json_ = config[CONF_JSON] if isinstance(json_, Lambda): - args_ = args + [(cg.JsonObjectRef, 'root')] - lambda_ = yield cg.process_lambda(json_, args_, return_type=cg.void) + args_ = args + [(cg.JsonObjectRef, "root")] + lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: for key in json_: - template_ = yield cg.templatable(json_[key], args, cg.std_string) + template_ = await cg.templatable(json_[key], args, cg.std_string) cg.add(var.add_json(key, template_)) for key in config.get(CONF_HEADERS, []): - template_ = yield cg.templatable(config[CONF_HEADERS][key], args, cg.const_char_ptr) + template_ = await cg.templatable( + config[CONF_HEADERS][key], args, cg.const_char_ptr + ) cg.add(var.add_header(key, template_)) - yield var + for conf in config.get(CONF_ON_RESPONSE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_response_trigger(trigger)) + await automation.build_automation(trigger, [(int, "status_code")], conf) + + return var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 390867c948..309977a915 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -1,10 +1,14 @@ +#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 { -static const char *TAG = "http_request"; +static const char *const TAG = "http_request"; void HttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "HTTP Request:"); @@ -12,19 +16,41 @@ void HttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_); } -void HttpRequestComponent::send() { - bool begin_status = false; +void HttpRequestComponent::set_url(std::string url) { + this->url_ = std::move(url); + this->secure_ = this->url_.compare(0, 6, "https:") == 0; + + if (!this->last_url_.empty() && this->url_ != this->last_url_) { + // Close connection if url has been changed + this->client_.setReuse(false); + this->client_.end(); + } this->client_.setReuse(true); - static const String URL = this->url_.c_str(); -#ifdef ARDUINO_ARCH_ESP32 - begin_status = this->client_.begin(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 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) { @@ -43,6 +69,9 @@ void HttpRequestComponent::send() { } int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); + for (auto *trigger : response_triggers) + trigger->process(http_code); + if (http_code < 0) { ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_.c_str(), HTTPClient::errorToString(http_code).c_str()); @@ -60,25 +89,30 @@ void HttpRequestComponent::send() { 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_; } #endif -void HttpRequestComponent::close() { this->client_.end(); } +void HttpRequestComponent::close() { + this->last_url_ = this->url_; + this->client_.end(); +} const char *HttpRequestComponent::get_string() { static const String STR = this->client_.getString(); @@ -87,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 e6c0510b32..9cc027b58d 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,18 +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 "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/json/json_util.h" +#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 { @@ -22,37 +29,39 @@ struct Header { const char *value; }; +class HttpRequestResponseTrigger; + class HttpRequestComponent : public Component { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - void set_url(std::string url) { - this->url_ = url; - this->secure_ = url.compare(0, 6, "https:") == 0; - } + void set_url(std::string url); void set_method(const char *method) { this->method_ = method; } void set_useragent(const char *useragent) { this->useragent_ = useragent; } void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } - void set_body(std::string body) { this->body_ = body; } - void set_headers(std::list
headers) { this->headers_ = headers; } - void send(); + void set_body(const std::string &body) { this->body_ = body; } + void set_headers(std::list
headers) { this->headers_ = std::move(headers); } + void send(const std::vector &response_triggers); void close(); const char *get_string(); protected: HTTPClient client_{}; std::string url_; + std::string last_url_; const char *method_; const char *useragent_{nullptr}; bool secure_; 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 }; @@ -71,6 +80,8 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } + void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } + void play(Ts... x) override { this->parent_->set_url(this->url_.value(x...)); this->parent_->set_method(this->method_.value(x...)); @@ -102,7 +113,7 @@ template class HttpRequestSendAction : public Action { } this->parent_->set_headers(headers); } - this->parent_->send(); + this->parent_->send(this->response_triggers_); this->parent_->close(); } @@ -118,7 +129,15 @@ template class HttpRequestSendAction : public Action { std::map> headers_{}; std::map> json_{}; std::function json_func_{nullptr}; + std::vector response_triggers_; +}; + +class HttpRequestResponseTrigger : public Trigger { + public: + void process(int status_code) { this->trigger(status_code); } }; } // namespace http_request } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index 45926dae36..a38ec73019 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -1,15 +1,16 @@ #include "htu21d.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace htu21d { -static const char *TAG = "htu21d"; +static const char *const TAG = "htu21d"; static const uint8_t HTU21D_ADDRESS = 0x40; static const uint8_t HTU21D_REGISTER_RESET = 0xFE; -static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3; -static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5; +static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3; +static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5; static const uint8_t HTU21D_REGISTER_STATUS = 0xE7; void HTU21DComponent::setup() { @@ -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 20053d27dd..37422f0329 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -1,30 +1,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ - ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT, ICON_WATER_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -htu21d_ns = cg.esphome_ns.namespace('htu21d') -HTU21DComponent = htu21d_ns.class_('HTU21DComponent', cg.PollingComponent, i2c.I2CDevice) +htu21d_ns = cg.esphome_ns.namespace("htu21d") +HTU21DComponent = htu21d_ns.class_( + "HTU21DComponent", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(HTU21DComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HTU21DComponent), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 605f534f91..62adc4ae86 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace hx711 { -static const char *TAG = "hx711"; +static const char *const TAG = "hx711"; void HX711Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up HX711 '%s'...", this->name_.c_str()); 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 2fc333a243..cd06cc770f 100644 --- a/esphome/components/hx711/sensor.py +++ b/esphome/components/hx711/sensor.py @@ -2,35 +2,51 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_CLK_PIN, CONF_GAIN, CONF_ID, ICON_SCALE +from esphome.const import ( + CONF_CLK_PIN, + CONF_GAIN, + CONF_ID, + ICON_SCALE, + STATE_CLASS_MEASUREMENT, +) -hx711_ns = cg.esphome_ns.namespace('hx711') -HX711Sensor = hx711_ns.class_('HX711Sensor', sensor.Sensor, cg.PollingComponent) +hx711_ns = cg.esphome_ns.namespace("hx711") +HX711Sensor = hx711_ns.class_("HX711Sensor", sensor.Sensor, cg.PollingComponent) -CONF_DOUT_PIN = 'dout_pin' +CONF_DOUT_PIN = "dout_pin" -HX711Gain = hx711_ns.enum('HX711Gain') +HX711Gain = hx711_ns.enum("HX711Gain") GAINS = { 128: HX711Gain.HX711_GAIN_128, 32: HX711Gain.HX711_GAIN_32, 64: HX711Gain.HX711_GAIN_64, } -CONFIG_SCHEMA = sensor.sensor_schema('', ICON_SCALE, 0).extend({ - cv.GenerateID(): cv.declare_id(HX711Sensor), - cv.Required(CONF_DOUT_PIN): pins.gpio_input_pin_schema, - cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_GAIN, default=128): cv.enum(GAINS, int=True), -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + icon=ICON_SCALE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(HX711Sensor), + cv.Required(CONF_DOUT_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_GAIN, default=128): cv.enum(GAINS, int=True), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - dout_pin = yield cg.gpio_pin_expression(config[CONF_DOUT_PIN]) + dout_pin = await cg.gpio_pin_expression(config[CONF_DOUT_PIN]) cg.add(var.set_dout_pin(dout_pin)) - sck_pin = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + sck_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_sck_pin(sck_pin)) cg.add(var.set_gain(config[CONF_GAIN])) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 0c71f18019..46f0abacc6 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,36 +1,82 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_SCAN, CONF_SCL, CONF_SDA, CONF_ADDRESS, \ - CONF_I2C_ID -from esphome.core import coroutine, coroutine_with_priority +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_INPUT, + CONF_OUTPUT, + CONF_SCAN, + CONF_SCL, + CONF_SDA, + CONF_ADDRESS, + CONF_I2C_ID, +) +from esphome.core import coroutine_with_priority, CORE -i2c_ns = cg.esphome_ns.namespace('i2c') -I2CComponent = i2c_ns.class_('I2CComponent', cg.Component) -I2CDevice = i2c_ns.class_('I2CDevice') +CODEOWNERS = ["@esphome/core"] +i2c_ns = cg.esphome_ns.namespace("i2c") +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") + +CONF_SDA_PULLUP_ENABLED = "sda_pullup_enabled" +CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled" MULTI_CONF = 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.Optional(CONF_FREQUENCY, default='50kHz'): - cv.All(cv.frequency, cv.Range(min=0, min_included=False)), - cv.Optional(CONF_SCAN, default=True): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA) + + +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(): _bus_declare_type, + cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( + cv.only_with_esp_idf, cv.boolean + ), + cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( + cv.only_with_esp_idf, cv.boolean + ), + cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( + cv.frequency, cv.Range(min=0, min_included=False) + ), + cv.Optional(CONF_SCAN, default=True): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) @coroutine_with_priority(1.0) -def to_code(config): +async def to_code(config): cg.add_global(i2c_ns.using) var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, 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): @@ -41,7 +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.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 @@ -50,14 +100,13 @@ def i2c_device_schema(default_address): return cv.Schema(schema) -@coroutine -def register_i2c_device(var, config): +async def register_i2c_device(var, config): """Register an i2c device with the given config. Sets the i2c bus to use and the i2c address. This is a coroutine, you need to await it with a 'yield' expression! """ - parent = yield cg.get_variable(config[CONF_I2C_ID]) - cg.add(var.set_i2c_parent(parent)) + parent = await cg.get_variable(config[CONF_I2C_ID]) + cg.add(var.set_i2c_bus(parent)) cg.add(var.set_i2c_address(config[CONF_ADDRESS])) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 562bd26771..82ab7bd09a 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,237 +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 *TAG = "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) { - uint8_t status = this->wire_->endTransmission(); - 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; } -bool I2CDevice::read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { // NOLINT - return this->parent_->read_bytes(this->address_, a_register, data, len, conversion); -} -bool I2CDevice::read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion) { // NOLINT - 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 - return this->parent_->write_bytes(this->address_, a_register, data, len); -} -bool I2CDevice::write_byte(uint8_t a_register, uint8_t data) { // NOLINT - 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 - 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 - 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 - 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 - 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 c4ed40e268..50a0b3ae50 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,189 +1,74 @@ #pragma once -#include -#include "esphome/core/component.h" +#include "i2c_bus.h" #include "esphome/core/helpers.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); - - /** 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 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) { return convert_big_endian(i2cshort); } +inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(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); - - /// 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}; } - /** 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) { return this->parent_->read_bytes_raw(this->address_, data, len); } + 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); + } + + 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); + } + + // Compat APIs + + 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; @@ -200,18 +85,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; @@ -220,57 +102,29 @@ 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) { - return this->parent_->write_bytes_raw(this->address_, data, len); + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { + return write_register(a_register, data, len) == ERROR_OK; } - /** 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()); + 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: uint8_t address_{0x00}; - I2CComponent *parent_{nullptr}; + I2CBus *bus_{nullptr}; }; } // namespace i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h new file mode 100644 index 0000000000..71f6b1d15b --- /dev/null +++ b/esphome/components/i2c/i2c_bus.h @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#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; + + protected: + void i2c_scan_() { + for (uint8_t address = 8; address < 120; address++) { + auto err = writev(address, nullptr, 0); + if (err == ERROR_OK) { + scan_results_.emplace_back(address, true); + } else if (err == ERROR_UNKNOWN) { + scan_results_.emplace_back(address, false); + } + } + } + std::vector> scan_results_; + bool scan_{false}; +}; + +} // 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..eac2a47524 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -0,0 +1,263 @@ +#ifdef USE_ARDUINO + +#include "i2c_bus_arduino.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.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; + if (this->scan_) { + ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); + this->i2c_scan_(); + } +} +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, "Results from i2c bus scan:"); + if (scan_results_.empty()) { + ESP_LOGI(TAG, "Found no i2c devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) + ESP_LOGI(TAG, "Found i2c device at address 0x%02X", s.first); + else + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } + } + } +} + +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..f4151e4f37 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -0,0 +1,46 @@ +#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_; + 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..109c3f890d --- /dev/null +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -0,0 +1,324 @@ +#ifdef USE_ESP_IDF + +#include "i2c_bus_esp_idf.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +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; + if (this->scan_) { + ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); + this->i2c_scan_(); + } +} +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, "Results from i2c bus scan:"); + if (scan_results_.empty()) { + ESP_LOGI(TAG, "Found no i2c devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) + ESP_LOGI(TAG, "Found i2c device at address 0x%02X", s.first); + else + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } + } + } +} + +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..d4b0626467 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -0,0 +1,50 @@ +#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_; + 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/__init__.py b/esphome/components/ili9341/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py new file mode 100644 index 0000000000..157e8212bd --- /dev/null +++ b/esphome/components/ili9341/display.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import ( + CONF_DC_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_MODEL, + CONF_PAGES, + CONF_RESET_PIN, +) + +DEPENDENCIES = ["spi"] + +CONF_LED_PIN = "led_pin" + +ili9341_ns = cg.esphome_ns.namespace("ili9341") +ili9341 = ili9341_ns.class_( + "ILI9341Display", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) +ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341) +ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341) + +ILI9341Model = ili9341_ns.enum("ILI9341Model") + +MODELS = { + "M5STACK": ILI9341Model.M5STACK, + "TFT_2.4": ILI9341Model.TFT_24, +} + +ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_") + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ili9341), + cv.Required(CONF_MODEL): ILI9341_MODEL, + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(spi.spi_device_schema(False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + if config[CONF_MODEL] == "M5STACK": + lcd_type = ILI9341M5Stack + if config[CONF_MODEL] == "TFT_2.4": + lcd_type = ILI9341TFT24 + rhs = lcd_type.new() + var = cg.Pvariable(config[CONF_ID], rhs) + + await cg.register_component(var, config) + await display.register_display(var, config) + await spi.register_spi_device(var, config) + cg.add(var.set_model(config[CONF_MODEL])) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_LED_PIN in config: + led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN]) + cg.add(var.set_led_pin(led_pin)) diff --git a/esphome/components/ili9341/ili9341_defines.h b/esphome/components/ili9341/ili9341_defines.h new file mode 100644 index 0000000000..6b3d4c0dcf --- /dev/null +++ b/esphome/components/ili9341/ili9341_defines.h @@ -0,0 +1,83 @@ +#pragma once + +namespace esphome { +namespace ili9341 { + +// Color definitions +// clang-format off +static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top +static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left +static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode +static const uint8_t MADCTL_ML = 0x10; ///< Bit 4 LCD refresh Bottom to top +static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order +static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order +static const uint8_t MADCTL_MH = 0x04; ///< Bit 2 LCD refresh right to left +// clang-format on + +static const uint16_t ILI9341_TFTWIDTH = 320; ///< ILI9341 max TFT width +static const uint16_t ILI9341_TFTHEIGHT = 240; ///< ILI9341 max TFT height + +// All ILI9341 specific commands some are used by init() +static const uint8_t ILI9341_NOP = 0x00; +static const uint8_t ILI9341_SWRESET = 0x01; +static const uint8_t ILI9341_RDDID = 0x04; +static const uint8_t ILI9341_RDDST = 0x09; + +static const uint8_t ILI9341_SLPIN = 0x10; +static const uint8_t ILI9341_SLPOUT = 0x11; +static const uint8_t ILI9341_PTLON = 0x12; +static const uint8_t ILI9341_NORON = 0x13; + +static const uint8_t ILI9341_RDMODE = 0x0A; +static const uint8_t ILI9341_RDMADCTL = 0x0B; +static const uint8_t ILI9341_RDPIXFMT = 0x0C; +static const uint8_t ILI9341_RDIMGFMT = 0x0A; +static const uint8_t ILI9341_RDSELFDIAG = 0x0F; + +static const uint8_t ILI9341_INVOFF = 0x20; +static const uint8_t ILI9341_INVON = 0x21; +static const uint8_t ILI9341_GAMMASET = 0x26; +static const uint8_t ILI9341_DISPOFF = 0x28; +static const uint8_t ILI9341_DISPON = 0x29; + +static const uint8_t ILI9341_CASET = 0x2A; +static const uint8_t ILI9341_PASET = 0x2B; +static const uint8_t ILI9341_RAMWR = 0x2C; +static const uint8_t ILI9341_RAMRD = 0x2E; + +static const uint8_t ILI9341_PTLAR = 0x30; +static const uint8_t ILI9341_VSCRDEF = 0x33; +static const uint8_t ILI9341_MADCTL = 0x36; +static const uint8_t ILI9341_VSCRSADD = 0x37; +static const uint8_t ILI9341_PIXFMT = 0x3A; + +static const uint8_t ILI9341_WRDISBV = 0x51; +static const uint8_t ILI9341_RDDISBV = 0x52; +static const uint8_t ILI9341_WRCTRLD = 0x53; + +static const uint8_t ILI9341_FRMCTR1 = 0xB1; +static const uint8_t ILI9341_FRMCTR2 = 0xB2; +static const uint8_t ILI9341_FRMCTR3 = 0xB3; +static const uint8_t ILI9341_INVCTR = 0xB4; +static const uint8_t ILI9341_DFUNCTR = 0xB6; + +static const uint8_t ILI9341_PWCTR1 = 0xC0; +static const uint8_t ILI9341_PWCTR2 = 0xC1; +static const uint8_t ILI9341_PWCTR3 = 0xC2; +static const uint8_t ILI9341_PWCTR4 = 0xC3; +static const uint8_t ILI9341_PWCTR5 = 0xC4; +static const uint8_t ILI9341_VMCTR1 = 0xC5; +static const uint8_t ILI9341_VMCTR2 = 0xC7; + +static const uint8_t ILI9341_RDID4 = 0xD3; +static const uint8_t ILI9341_RDINDEX = 0xD9; +static const uint8_t ILI9341_RDID1 = 0xDA; +static const uint8_t ILI9341_RDID2 = 0xDB; +static const uint8_t ILI9341_RDID3 = 0xDC; +static const uint8_t ILI9341_RDIDX = 0xDD; // TBC + +static const uint8_t ILI9341_GMCTRP1 = 0xE0; +static const uint8_t ILI9341_GMCTRN1 = 0xE1; + +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp new file mode 100644 index 0000000000..a24f0bbb64 --- /dev/null +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -0,0 +1,276 @@ +#include "ili9341_display.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ili9341 { + +static const char *const TAG = "ili9341"; + +void ILI9341Display::setup_pins_() { + this->init_internal_(this->get_buffer_length_()); + this->dc_pin_->setup(); // OUTPUT + this->dc_pin_->digital_write(false); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); // OUTPUT + this->reset_pin_->digital_write(true); + } + if (this->led_pin_ != nullptr) { + this->led_pin_->setup(); + this->led_pin_->digital_write(true); + } + this->spi_setup(); + + this->reset_(); +} + +void ILI9341Display::dump_config() { + LOG_DISPLAY("", "ili9341", this); + ESP_LOGCONFIG(TAG, " Width: %d, Height: %d, Rotation: %d", this->width_, this->height_, this->rotation_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_PIN(" Backlight Pin: ", this->led_pin_); + LOG_UPDATE_INTERVAL(this); +} + +float ILI9341Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void ILI9341Display::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void ILI9341Display::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void ILI9341Display::data(uint8_t value) { + this->start_data_(); + this->write_byte(value); + this->end_data_(); +} + +void ILI9341Display::send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->command(command_byte); // Send the command byte + this->start_data_(); + this->write_array(data_bytes, num_data_bytes); + this->end_data_(); +} + +uint8_t ILI9341Display::read_command(uint8_t command_byte, uint8_t index) { + uint8_t data = 0x10 + index; + this->send_command(0xD9, &data, 1); // Set Index Register + uint8_t result; + this->start_command_(); + this->write_byte(command_byte); + this->start_data_(); + do { + result = this->read_byte(); + } while (index--); + this->end_data_(); + return result; +} + +void ILI9341Display::update() { + this->do_update_(); + this->display_(); +} + +void ILI9341Display::display_() { + // we will only update the changed window to the display + uint16_t w = this->x_high_ - this->x_low_ + 1; + uint16_t h = this->y_high_ - this->y_low_ + 1; + + set_addr_window_(this->x_low_, this->y_low_, w, h); + this->start_data_(); + uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); + for (uint16_t row = 0; row < h; row++) { + uint32_t pos = start_pos + (row * width_); + uint32_t rem = w; + + while (rem > 0) { + uint32_t sz = buffer_to_transfer_(pos, rem); + this->write_array(transfer_buffer_, 2 * sz); + pos += sz; + rem -= sz; + } + } + this->end_data_(); + + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) { + int r = color_8bit >> 5; + int g = (color_8bit >> 2) & 0x07; + int b = color_8bit & 0x03; + uint16_t color = (r * 0x04) << 11; + color |= (g * 0x09) << 5; + color |= (b * 0x0A); + + return color; +} + +uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) { + // convert 16bit color to 8 bit buffer + uint8_t r = color_16bit >> 11; + uint8_t g = (color_16bit >> 5) & 0x3F; + uint8_t b = color_16bit & 0x1F; + + return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5)); +} + +void ILI9341Display::fill(Color color) { + auto color565 = display::ColorUtil::color_to_565(color); + memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_()); + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->get_width_internal() - 1; + this->y_high_ = this->get_height_internal() - 1; +} + +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_(); + + 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) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + // low and high watermark may speed up drawing from buffer + this->x_low_ = (x < this->x_low_) ? x : this->x_low_; + this->y_low_ = (y < this->y_low_) ? y : this->y_low_; + this->x_high_ = (x > this->x_high_) ? x : this->x_high_; + this->y_high_ = (y > this->y_high_) ? y : this->y_high_; + + uint32_t pos = (y * width_) + x; + auto color565 = display::ColorUtil::color_to_565(color); + buffer_[pos] = convert_to_8bit_color_(color565); +} + +// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color +// values per bit is huge +uint32_t ILI9341Display::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } + +void ILI9341Display::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} + +void ILI9341Display::end_command_() { this->disable(); } +void ILI9341Display::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} +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 = progmem_read_byte(addr++)) > 0) { + x = progmem_read_byte(addr++); + num_args = x & 0x7F; + send_command(cmd, addr, num_args); + addr += num_args; + if (x & 0x80) + delay(150); // NOLINT + } +} + +void ILI9341Display::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { + uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); + this->command(ILI9341_CASET); // Column address set + this->start_data_(); + this->write_byte(x1 >> 8); + this->write_byte(x1); + this->write_byte(x2 >> 8); + this->write_byte(x2); + this->end_data_(); + this->command(ILI9341_PASET); // Row address set + this->start_data_(); + this->write_byte(y1 >> 8); + this->write_byte(y1); + this->write_byte(y2 >> 8); + this->write_byte(y2); + this->end_data_(); + this->command(ILI9341_RAMWR); // Write to RAM +} + +void ILI9341Display::invert_display_(bool invert) { this->command(invert ? ILI9341_INVON : ILI9341_INVOFF); } + +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); +} + +// 24_TFT display +void ILI9341TFT24::initialize() { + this->init_lcd_(INITCMD_TFT); + this->width_ = 240; + this->height_ = 320; + this->fill_internal_(Color::BLACK); +} + +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h new file mode 100644 index 0000000000..d8c90c9d33 --- /dev/null +++ b/esphome/components/ili9341/ili9341_display.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" +#include "ili9341_defines.h" +#include "ili9341_init.h" + +namespace esphome { +namespace ili9341 { + +enum ILI9341Model { + M5STACK = 0, + TFT_24, +}; + +class ILI9341Display : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + float get_setup_priority() const override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void set_led_pin(GPIOPin *led) { this->led_pin_ = led; } + void set_model(ILI9341Model model) { this->model_ = model; } + + void command(uint8_t value); + void data(uint8_t value); + void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); + uint8_t read_command(uint8_t command_byte, uint8_t index); + virtual void initialize() = 0; + + void update() override; + + void fill(Color color) override; + + void dump_config() override; + void setup() override { + this->setup_pins_(); + this->initialize(); + } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void setup_pins_(); + + void init_lcd_(const uint8_t *init_cmd); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void invert_display_(bool invert); + void reset_(); + void fill_internal_(Color color); + void display_(); + uint16_t convert_to_16bit_color_(uint8_t color_8bit); + uint8_t convert_to_8bit_color_(uint16_t color_16bit); + + ILI9341Model model_; + int16_t width_{320}; ///< Display width as modified by current rotation + int16_t height_{240}; ///< Display height as modified by current rotation + uint16_t x_low_{0}; + uint16_t y_low_{0}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + + uint32_t get_buffer_length_(); + int get_width_internal() override; + int get_height_internal() override; + + void start_command_(); + void end_command_(); + 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_; + GPIOPin *busy_pin_{nullptr}; +}; + +//----------- M5Stack display -------------- +class ILI9341M5Stack : public ILI9341Display { + public: + void initialize() override; +}; + +//----------- ILI9341_24_TFT display -------------- +class ILI9341TFT24 : public ILI9341Display { + public: + void initialize() override; +}; +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_init.h b/esphome/components/ili9341/ili9341_init.h new file mode 100644 index 0000000000..9282895e2e --- /dev/null +++ b/esphome/components/ili9341/ili9341_init.h @@ -0,0 +1,70 @@ +#pragma once +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ili9341 { + +// clang-format off +static const uint8_t PROGMEM INITCMD_M5STACK[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9341_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9341_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9341_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9341_VMCTR2 , 1, 0x86, // VCM control2 + ILI9341_MADCTL , 1, MADCTL_BGR, // Memory Access Control + ILI9341_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9341_PIXFMT , 1, 0x55, + ILI9341_FRMCTR1 , 2, 0x00, 0x13, + ILI9341_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9341_SLPOUT , 0x80, // Exit Sleep + ILI9341_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_TFT[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9341_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9341_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9341_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9341_VMCTR2 , 1, 0x86, // VCM control2 + ILI9341_MADCTL , 1, 0x48, // Memory Access Control + ILI9341_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9341_PIXFMT , 1, 0x55, + ILI9341_FRMCTR1 , 2, 0x00, 0x18, + ILI9341_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9341_SLPOUT , 0x80, // Exit Sleep + ILI9341_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +// clang-format on +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index d0a41a7379..a721263dff 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -4,28 +4,47 @@ 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_RESIZE +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__) -DEPENDENCIES = ['display'] +DEPENDENCIES = ["display"] MULTI_CONF = True -Image_ = display.display_ns.class_('Image') +ImageType = display.display_ns.enum("ImageType") +IMAGE_TYPE = { + "BINARY": ImageType.IMAGE_TYPE_BINARY, + "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, + "RGB24": ImageType.IMAGE_TYPE_RGB24, +} -CONF_RAW_DATA_ID = 'raw_data_id' +Image_ = display.display_ns.class_("Image") -IMAGE_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.file_, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), -}) +IMAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(Image_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( + "NONE", "FLOYDSTEINBERG", upper=True + ), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) -def to_code(config): +async def to_code(config): from PIL import Image path = CORE.relative_config_path(config[CONF_FILE]) @@ -34,23 +53,54 @@ def to_code(config): except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + width, height = image.size + if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) + width, height = image.size + else: + if width > 500 or height > 500: + _LOGGER.warning( + "The image you requested is very big. Please consider using" + " the resize parameter." + ) - image = image.convert('1', dither=Image.NONE) - width, height = image.size - if width > 500 or height > 500: - _LOGGER.warning("The image you requested is very big. Please consider using the resize " - "parameter") - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) + dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG + if config[CONF_TYPE] == "GRAYSCALE": + image = image.convert("L", dither=dither) + pixels = list(image.getdata()) + data = [0 for _ in range(height * width)] + pos = 0 + for pix in pixels: + data[pos] = pix + pos += 1 + + elif config[CONF_TYPE] == "RGB24": + image = image.convert("RGB") + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 3)] + pos = 0 + for pix in pixels: + data[pos] = pix[0] + pos += 1 + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] + pos += 1 + + elif config[CONF_TYPE] == "BINARY": + image = image.convert("1", dither=dither) + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range(height * width8 // 8)] + for y in range(height): + for x in range(width): + if image.getpixel((x, y)): + continue + pos = x + y * width8 + data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable(config[CONF_ID], prog_arr, width, height) + cg.new_Pvariable( + config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] + ) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py new file mode 100644 index 0000000000..ed7c382a2f --- /dev/null +++ b/esphome/components/improv_serial/__init__.py @@ -0,0 +1,33 @@ +from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv + +CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["logger", "wifi"] + +improv_serial_ns = cg.esphome_ns.namespace("improv_serial") + +ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ImprovSerialComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +def validate_logger_baud_rate(config): + logger_conf = fv.full_config.get()[CONF_LOGGER] + if logger_conf[CONF_BAUD_RATE] == 0: + raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") + return config + + +FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add_library("esphome/Improv", "1.0.0") diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp new file mode 100644 index 0000000000..b4d1d88370 --- /dev/null +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -0,0 +1,266 @@ +#include "improv_serial_component.h" + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/version.h" + +#include "esphome/components/logger/logger.h" + +namespace esphome { +namespace improv_serial { + +static const char *const TAG = "improv_serial"; + +void ImprovSerialComponent::setup() { + global_improv_serial_component = this; +#ifdef USE_ARDUINO + this->hw_serial_ = logger::global_logger->get_hw_serial(); +#endif +#ifdef USE_ESP_IDF + this->uart_num_ = logger::global_logger->get_uart_num(); +#endif + + if (wifi::global_wifi_component->has_sta()) { + this->state_ = improv::STATE_PROVISIONED; + } +} + +void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } + +int ImprovSerialComponent::available_() { +#ifdef USE_ARDUINO + return this->hw_serial_->available(); +#endif +#ifdef USE_ESP_IDF + size_t available; + uart_get_buffered_data_len(this->uart_num_, &available); + return available; +#endif +} + +uint8_t ImprovSerialComponent::read_byte_() { + uint8_t data; +#ifdef USE_ARDUINO + this->hw_serial_->readBytes(&data, 1); +#endif +#ifdef USE_ESP_IDF + uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_RATE_MS); +#endif + return data; +} + +void ImprovSerialComponent::write_data_(std::vector &data) { + data.push_back('\n'); +#ifdef USE_ARDUINO + this->hw_serial_->write(data.data(), data.size()); +#endif +#ifdef USE_ESP_IDF + uart_write_bytes(this->uart_num_, data.data(), data.size()); +#endif +} + +void ImprovSerialComponent::loop() { + const uint32_t now = millis(); + if (now - this->last_read_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_read_byte_ = now; + } + + while (this->available_()) { + uint8_t byte = this->read_byte_(); + if (this->parse_improv_serial_byte_(byte)) { + this->last_read_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } + + if (this->state_ == improv::STATE_PROVISIONING) { + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); + this->send_response_(url); + } + } +} + +std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { + std::vector urls; +#ifdef USE_WEBSERVER + auto ip = wifi::global_wifi_component->wifi_sta_ip(); + std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); + urls.push_back(webserver_url); +#endif + std::vector data = improv::build_rpc_response(command, urls, false); + return data; +} + +std::vector ImprovSerialComponent::build_version_info_() { + std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; + std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); + return data; +}; + +bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + ESP_LOGD(TAG, "Improv Serial byte: 0x%02X", byte); + const uint8_t *raw = &this->rx_buffer_[0]; + if (at == 0) + return byte == 'I'; + if (at == 1) + return byte == 'M'; + if (at == 2) + return byte == 'P'; + if (at == 3) + return byte == 'R'; + if (at == 4) + return byte == 'O'; + if (at == 5) + return byte == 'V'; + + if (at == 6) + return byte == IMPROV_SERIAL_VERSION; + + if (at == 7) + return true; + uint8_t type = raw[7]; + + if (at == 8) + return true; + uint8_t data_len = raw[8]; + + if (at < 8 + data_len) + return true; + + if (at == 8 + data_len) + return true; + + if (at == 8 + data_len + 1) { + uint8_t checksum = 0x00; + for (size_t i = 0; i < at; i++) + checksum += raw[i]; + + if (checksum != byte) { + ESP_LOGW(TAG, "Error decoding Improv payload"); + this->set_error_(improv::ERROR_INVALID_RPC); + return false; + } + + if (type == TYPE_RPC) { + this->set_error_(improv::ERROR_NONE); + auto command = improv::parse_improv_data(&raw[9], data_len, false); + return this->parse_improv_payload_(command); + } + } + + // If we got here then the command coming is is improv, but not an RPC command + return false; +} + +bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) { + switch (command.command) { + case improv::WIFI_SETTINGS: { + wifi::WiFiAP sta{}; + sta.set_ssid(command.ssid); + sta.set_password(command.password); + this->connecting_sta_ = sta; + + wifi::global_wifi_component->set_sta(sta); + wifi::global_wifi_component->start_scanning(); + this->set_state_(improv::STATE_PROVISIONING); + ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + command.password.c_str()); + + auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); + this->set_timeout("wifi-connect-timeout", 30000, f); + return true; + } + case improv::GET_CURRENT_STATE: + this->set_state_(this->state_); + if (this->state_ == improv::STATE_PROVISIONED) { + std::vector url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE); + this->send_response_(url); + } + return true; + case improv::GET_DEVICE_INFO: { + std::vector info = this->build_version_info_(); + this->send_response_(info); + return true; + } + default: { + ESP_LOGW(TAG, "Unknown Improv payload"); + this->set_error_(improv::ERROR_UNKNOWN_RPC); + return false; + } + } +} + +void ImprovSerialComponent::set_state_(improv::State state) { + this->state_ = state; + + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = IMPROV_SERIAL_VERSION; + data[7] = TYPE_CURRENT_STATE; + data[8] = 1; + data[9] = state; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + + this->write_data_(data); +} + +void ImprovSerialComponent::set_error_(improv::Error error) { + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = IMPROV_SERIAL_VERSION; + data[7] = TYPE_ERROR_STATE; + data[8] = 1; + data[9] = error; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + this->write_data_(data); +} + +void ImprovSerialComponent::send_response_(std::vector &response) { + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(9); + data[6] = IMPROV_SERIAL_VERSION; + data[7] = TYPE_RPC_RESPONSE; + data[8] = response.size(); + data.insert(data.end(), response.begin(), response.end()); + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data.push_back(checksum); + + this->write_data_(data); +} + +void ImprovSerialComponent::on_wifi_connect_timeout_() { + this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); + this->set_state_(improv::STATE_AUTHORIZED); + ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + wifi::global_wifi_component->clear_sta(); +} + +ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace improv_serial +} // namespace esphome diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h new file mode 100644 index 0000000000..304afdaf75 --- /dev/null +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/components/wifi/wifi_component.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include + +#ifdef USE_ARDUINO +#include +#endif +#ifdef USE_ESP_IDF +#include +#endif + +namespace esphome { +namespace improv_serial { + +enum ImprovSerialType : uint8_t { + TYPE_CURRENT_STATE = 0x01, + TYPE_ERROR_STATE = 0x02, + TYPE_RPC = 0x03, + TYPE_RPC_RESPONSE = 0x04 +}; + +static const uint8_t IMPROV_SERIAL_VERSION = 1; + +class ImprovSerialComponent : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + bool parse_improv_serial_byte_(uint8_t byte); + bool parse_improv_payload_(improv::ImprovCommand &command); + + void set_state_(improv::State state); + void set_error_(improv::Error error); + void send_response_(std::vector &response); + void on_wifi_connect_timeout_(); + + std::vector build_rpc_settings_response_(improv::Command command); + std::vector build_version_info_(); + + int available_(); + uint8_t read_byte_(); + void write_data_(std::vector &data); + +#ifdef USE_ARDUINO + HardwareSerial *hw_serial_{nullptr}; +#endif +#ifdef USE_ESP_IDF + uart_port_t uart_num_; +#endif + + std::vector rx_buffer_; + uint32_t last_read_byte_{0}; + wifi::WiFiAP connecting_sta_; + improv::State state_{improv::STATE_AUTHORIZED}; +}; + +extern ImprovSerialComponent + *global_improv_serial_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace improv_serial +} // namespace esphome diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 1150f7c661..609f3d0f08 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -1,10 +1,11 @@ #include "ina219.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ina219 { -static const char *TAG = "ina219"; +static const char *const TAG = "ina219"; // | A0 | A1 | Address | // | GND | GND | 0x40 | @@ -120,7 +121,7 @@ void INA219Component::setup() { } this->calibration_lsb_ = lsb; - auto calibration = uint32_t(0.04096f / (0.0001 * lsb * this->shunt_resistance_ohm_)); + auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_)); ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); @@ -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 6a61e16226..020be9bc6e 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -1,49 +1,96 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_BUS_VOLTAGE, CONF_CURRENT, CONF_ID, \ - CONF_MAX_CURRENT, CONF_MAX_VOLTAGE, CONF_POWER, CONF_SHUNT_RESISTANCE, \ - CONF_SHUNT_VOLTAGE, ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT +from esphome.const import ( + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_ID, + CONF_MAX_CURRENT, + CONF_MAX_VOLTAGE, + CONF_POWER, + CONF_SHUNT_RESISTANCE, + CONF_SHUNT_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -ina219_ns = cg.esphome_ns.namespace('ina219') -INA219Component = ina219_ns.class_('INA219Component', cg.PollingComponent, i2c.I2CDevice) +ina219_ns = cg.esphome_ns.namespace("ina219") +INA219Component = ina219_ns.class_( + "INA219Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(INA219Component), - cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 3), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), - cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(cv.resistance, - cv.Range(min=0.0, max=32.0)), - cv.Optional(CONF_MAX_VOLTAGE, default=32.0): cv.All(cv.voltage, cv.Range(min=0.0, max=32.0)), - cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(cv.current, cv.Range(min=0.0)), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(INA219Component), + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( + cv.resistance, cv.Range(min=0.0, max=32.0) + ), + cv.Optional(CONF_MAX_VOLTAGE, default=32.0): cv.All( + cv.voltage, cv.Range(min=0.0, max=32.0) + ), + cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All( + cv.current, cv.Range(min=0.0) + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) cg.add(var.set_max_voltage_v(config[CONF_MAX_VOLTAGE])) if CONF_BUS_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) cg.add(var.set_bus_voltage_sensor(sens)) if CONF_SHUNT_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) cg.add(var.set_shunt_voltage_sensor(sens)) if CONF_CURRENT in config: - sens = yield sensor.new_sensor(config[CONF_CURRENT]) + sens = await sensor.new_sensor(config[CONF_CURRENT]) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: - sens = yield sensor.new_sensor(config[CONF_POWER]) + sens = await sensor.new_sensor(config[CONF_POWER]) cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index cbb06d73b6..2e30a5ac01 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -1,10 +1,11 @@ #include "ina226.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ina226 { -static const char *TAG = "ina226"; +static const char *const TAG = "ina226"; // | A0 | A1 | Address | // | GND | GND | 0x40 | @@ -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 066363b3d4..ee4036ce7e 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -1,47 +1,92 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_BUS_VOLTAGE, CONF_CURRENT, CONF_ID, \ - CONF_MAX_CURRENT, CONF_POWER, CONF_SHUNT_RESISTANCE, \ - CONF_SHUNT_VOLTAGE, ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT +from esphome.const import ( + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_ID, + CONF_MAX_CURRENT, + CONF_POWER, + CONF_SHUNT_RESISTANCE, + CONF_SHUNT_VOLTAGE, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -ina226_ns = cg.esphome_ns.namespace('ina226') -INA226Component = ina226_ns.class_('INA226Component', cg.PollingComponent, i2c.I2CDevice) +ina226_ns = cg.esphome_ns.namespace("ina226") +INA226Component = ina226_ns.class_( + "INA226Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(INA226Component), - cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 3), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), - cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(cv.resistance, cv.Range(min=0.0)), - cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(cv.current, cv.Range(min=0.0)), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(INA226Component), + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( + cv.resistance, cv.Range(min=0.0) + ), + cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All( + cv.current, cv.Range(min=0.0) + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) if CONF_BUS_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) cg.add(var.set_bus_voltage_sensor(sens)) if CONF_SHUNT_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) cg.add(var.set_shunt_voltage_sensor(sens)) if CONF_CURRENT in config: - sens = yield sensor.new_sensor(config[CONF_CURRENT]) + sens = await sensor.new_sensor(config[CONF_CURRENT]) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: - sens = yield sensor.new_sensor(config[CONF_POWER]) + sens = await sensor.new_sensor(config[CONF_POWER]) cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 3bd568f37d..3f8e2d06df 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -1,10 +1,11 @@ #include "ina3221.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ina3221 { -static const char *TAG = "ina3221"; +static const char *const TAG = "ina3221"; static const uint8_t INA3221_REGISTER_CONFIG = 0x00; static const uint8_t INA3221_REGISTER_CHANNEL1_SHUNT_VOLTAGE = 0x01; @@ -42,7 +43,7 @@ void INA3221Component::setup() { config |= 0b0001000000000000; } // 0b0000xxx000000000 << 9 Averaging Mode (0 -> 1 sample, 111 -> 1024 samples) - config |= 0b0000111000000000; + config |= 0b0000000000000000; // 0b0000000xxx000000 << 6 Bus Voltage Conversion time (100 -> 1.1ms, 111 -> 8.244 ms) config |= 0b0000000111000000; // 0b0000000000xxx000 << 3 Shunt Voltage Conversion time (same as above) @@ -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,11 +97,11 @@ 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; } - const float shunt_voltage_v = int16_t(raw) * 40.0f / 1000000.0f; + const float shunt_voltage_v = int16_t(raw) * 40.0f / 8.0f / 1000000.0f; if (channel.shunt_voltage_sensor_ != nullptr) channel.shunt_voltage_sensor_->publish_state(shunt_voltage_v); current_a = shunt_voltage_v / channel.shunt_resistance_; diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 1c26533cc4..9c42ecbb9d 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -1,40 +1,83 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_BUS_VOLTAGE, CONF_CURRENT, CONF_ID, CONF_POWER, \ - CONF_SHUNT_RESISTANCE, CONF_SHUNT_VOLTAGE, ICON_FLASH, \ - UNIT_VOLT, UNIT_AMPERE, UNIT_WATT +from esphome.const import ( + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_ID, + CONF_POWER, + CONF_SHUNT_RESISTANCE, + CONF_SHUNT_VOLTAGE, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -CONF_CHANNEL_1 = 'channel_1' -CONF_CHANNEL_2 = 'channel_2' -CONF_CHANNEL_3 = 'channel_3' +CONF_CHANNEL_1 = "channel_1" +CONF_CHANNEL_2 = "channel_2" +CONF_CHANNEL_3 = "channel_3" -ina3221_ns = cg.esphome_ns.namespace('ina3221') -INA3221Component = ina3221_ns.class_('INA3221Component', cg.PollingComponent, i2c.I2CDevice) +ina3221_ns = cg.esphome_ns.namespace("ina3221") +INA3221Component = ina3221_ns.class_( + "INA3221Component", cg.PollingComponent, i2c.I2CDevice +) -INA3221_CHANNEL_SCHEMA = cv.Schema({ - cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), - cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(cv.resistance, - cv.Range(min=0.0, max=32.0)), -}) +INA3221_CHANNEL_SCHEMA = cv.Schema( + { + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( + cv.resistance, cv.Range(min=0.0, max=32.0) + ), + } +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(INA3221Component), - cv.Optional(CONF_CHANNEL_1): INA3221_CHANNEL_SCHEMA, - cv.Optional(CONF_CHANNEL_2): INA3221_CHANNEL_SCHEMA, - cv.Optional(CONF_CHANNEL_3): INA3221_CHANNEL_SCHEMA, -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(INA3221Component), + cv.Optional(CONF_CHANNEL_1): INA3221_CHANNEL_SCHEMA, + cv.Optional(CONF_CHANNEL_2): INA3221_CHANNEL_SCHEMA, + cv.Optional(CONF_CHANNEL_3): INA3221_CHANNEL_SCHEMA, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) for i, channel in enumerate([CONF_CHANNEL_1, CONF_CHANNEL_2, CONF_CHANNEL_3]): if channel not in config: @@ -43,14 +86,14 @@ def to_code(config): if CONF_SHUNT_RESISTANCE in conf: cg.add(var.set_shunt_resistance(i, conf[CONF_SHUNT_RESISTANCE])) if CONF_BUS_VOLTAGE in conf: - sens = yield sensor.new_sensor(conf[CONF_BUS_VOLTAGE]) + sens = await sensor.new_sensor(conf[CONF_BUS_VOLTAGE]) cg.add(var.set_bus_voltage_sensor(i, sens)) if CONF_SHUNT_VOLTAGE in conf: - sens = yield sensor.new_sensor(conf[CONF_SHUNT_VOLTAGE]) + sens = await sensor.new_sensor(conf[CONF_SHUNT_VOLTAGE]) cg.add(var.set_shunt_voltage_sensor(i, sens)) if CONF_CURRENT in conf: - sens = yield sensor.new_sensor(conf[CONF_CURRENT]) + sens = await sensor.new_sensor(conf[CONF_CURRENT]) cg.add(var.set_current_sensor(i, sens)) if CONF_POWER in conf: - sens = yield sensor.new_sensor(conf[CONF_POWER]) + sens = await sensor.new_sensor(conf[CONF_POWER]) cg.add(var.set_power_sensor(i, sens)) diff --git a/esphome/components/inkbird_ibsth1_mini/__init__.py b/esphome/components/inkbird_ibsth1_mini/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp new file mode 100644 index 0000000000..76013e28ff --- /dev/null +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -0,0 +1,110 @@ +#include "inkbird_ibsth1_mini.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace inkbird_ibsth1_mini { + +static const char *const TAG = "inkbird_ibsth1_mini"; + +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 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 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 + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + if (device.get_address_type() != BLE_ADDR_TYPE_PUBLIC) { + ESP_LOGVV(TAG, "parse_device(): address is not public"); + return false; + } + if (!device.get_service_datas().empty()) { + ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty"); + return false; + } + 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 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 (mnf_data.data.size() != 7) { + ESP_LOGVV(TAG, "parse_device(): manufacturer data element length is expected to be of length 7"); + return false; + } + if (mnf_data.data[6] != 8) { + ESP_LOGVV(TAG, "parse_device(): unexpected data"); + return false; + } + + // 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 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) + + // 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); + } + if (this->battery_level_ != nullptr) { + this->battery_level_->publish_state(battery_level); + } + + return true; +} + +} // namespace inkbird_ibsth1_mini +} // namespace esphome + +#endif diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h new file mode 100644 index 0000000000..bdca2d0cac --- /dev/null +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -0,0 +1,36 @@ +#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 inkbird_ibsth1_mini { + +class InkbirdIbstH1Mini : 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_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}; +}; + +} // namespace inkbird_ibsth1_mini +} // namespace esphome + +#endif diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py new file mode 100644 index 0000000000..aa11fb3172 --- /dev/null +++ b/esphome/components/inkbird_ibsth1_mini/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_HUMIDITY, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + CONF_ID, +) + +CODEOWNERS = ["@fkirill"] +DEPENDENCIES = ["esp32_ble_tracker"] + +CONF_EXTERNAL_TEMPERATURE = "external_temperature" + +inkbird_ibsth1_mini_ns = cg.esphome_ns.namespace("inkbird_ibsth1_mini") +InkbirdIbstH1Mini = inkbird_ibsth1_mini_ns.class_( + "InkbirdIbstH1Mini", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(InkbirdIbstH1Mini), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + 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)) + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/inkplate6/__init__.py b/esphome/components/inkplate6/__init__.py new file mode 100644 index 0000000000..b1de57df8f --- /dev/null +++ b/esphome/components/inkplate6/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py new file mode 100644 index 0000000000..e4c71ea717 --- /dev/null +++ b/esphome/components/inkplate6/display.py @@ -0,0 +1,170 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, i2c +from esphome.const import ( + CONF_FULL_UPDATE_EVERY, + CONF_ID, + CONF_LAMBDA, + CONF_PAGES, + CONF_WAKEUP_PIN, +) + +DEPENDENCIES = ["i2c", "esp32"] + +CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" +CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" +CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" +CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" +CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" +CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" +CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" +CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" + +CONF_CL_PIN = "cl_pin" +CONF_CKV_PIN = "ckv_pin" +CONF_GREYSCALE = "greyscale" +CONF_GMOD_PIN = "gmod_pin" +CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" +CONF_LE_PIN = "le_pin" +CONF_OE_PIN = "oe_pin" +CONF_PARTIAL_UPDATING = "partial_updating" +CONF_POWERUP_PIN = "powerup_pin" +CONF_SPH_PIN = "sph_pin" +CONF_SPV_PIN = "spv_pin" +CONF_VCOM_PIN = "vcom_pin" + + +inkplate6_ns = cg.esphome_ns.namespace("inkplate6") +Inkplate6 = inkplate6_ns.class_( + "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer +) + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Inkplate6), + cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, + cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, + cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, + # Control pins + cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, + # Data pins + cv.Optional( + CONF_DISPLAY_DATA_0_PIN, default=4 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_1_PIN, default=5 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_2_PIN, default=18 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_3_PIN, default=19 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_4_PIN, default=23 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_5_PIN, default=25 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_6_PIN, default=26 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_7_PIN, default=27 + ): pins.internal_gpio_output_pin_schema, + } + ) + .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, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await display.register_display(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + cg.add(var.set_greyscale(config[CONF_GREYSCALE])) + cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + + ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) + cg.add(var.set_ckv_pin(ckv)) + + gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) + cg.add(var.set_gmod_pin(gmod)) + + gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) + cg.add(var.set_gpio0_enable_pin(gpio0_enable)) + + oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe)) + + powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) + cg.add(var.set_powerup_pin(powerup)) + + sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) + cg.add(var.set_sph_pin(sph)) + + spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) + cg.add(var.set_spv_pin(spv)) + + vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) + cg.add(var.set_vcom_pin(vcom)) + + wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) + cg.add(var.set_wakeup_pin(wakeup)) + + cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) + cg.add(var.set_cl_pin(cl)) + + le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) + cg.add(var.set_le_pin(le)) + + display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) + cg.add(var.set_display_data_0_pin(display_data_0)) + + display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) + cg.add(var.set_display_data_1_pin(display_data_1)) + + display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) + cg.add(var.set_display_data_2_pin(display_data_2)) + + display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) + cg.add(var.set_display_data_3_pin(display_data_3)) + + display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) + cg.add(var.set_display_data_4_pin(display_data_4)) + + display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) + cg.add(var.set_display_data_5_pin(display_data_5)) + + display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) + cg.add(var.set_display_data_6_pin(display_data_6)) + + display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) + cg.add(var.set_display_data_7_pin(display_data_7)) + + cg.add_build_flag("-DBOARD_HAS_PSRAM") diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp new file mode 100644 index 0000000000..8a05836db9 --- /dev/null +++ b/esphome/components/inkplate6/inkplate.cpp @@ -0,0 +1,623 @@ +#include "inkplate.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include + +namespace esphome { +namespace inkplate6 { + +static const char *const TAG = "inkplate"; + +void Inkplate6::setup() { + this->initialize_(); + + this->vcom_pin_->setup(); + this->powerup_pin_->setup(); + this->wakeup_pin_->setup(); + this->gpio0_enable_pin_->setup(); + this->gpio0_enable_pin_->digital_write(true); + + this->cl_pin_->setup(); + this->le_pin_->setup(); + this->ckv_pin_->setup(); + this->gmod_pin_->setup(); + this->oe_pin_->setup(); + this->sph_pin_->setup(); + this->spv_pin_->setup(); + + this->display_data_0_pin_->setup(); + this->display_data_1_pin_->setup(); + this->display_data_2_pin_->setup(); + this->display_data_3_pin_->setup(); + this->display_data_4_pin_->setup(); + this->display_data_5_pin_->setup(); + this->display_data_6_pin_->setup(); + this->display_data_7_pin_->setup(); + + this->clean(); + this->display(); +} +void Inkplate6::initialize_() { + uint32_t buffer_size = this->get_buffer_length_(); + + if (this->partial_buffer_ != nullptr) { + free(this->partial_buffer_); // NOLINT + } + if (this->partial_buffer_2_ != nullptr) { + free(this->partial_buffer_2_); // NOLINT + } + if (this->buffer_ != nullptr) { + free(this->buffer_); // NOLINT + } + + this->buffer_ = (uint8_t *) ps_malloc(buffer_size); + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer for display!"); + this->mark_failed(); + return; + } + if (!this->greyscale_) { + this->partial_buffer_ = (uint8_t *) ps_malloc(buffer_size); + if (this->partial_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate partial buffer for display!"); + this->mark_failed(); + return; + } + this->partial_buffer_2_ = (uint8_t *) ps_malloc(buffer_size * 2); + if (this->partial_buffer_2_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate partial buffer 2 for display!"); + this->mark_failed(); + return; + } + memset(this->partial_buffer_, 0, buffer_size); + memset(this->partial_buffer_2_, 0, buffer_size * 2); + } + + memset(this->buffer_, 0, buffer_size); +} +float Inkplate6::get_setup_priority() const { return setup_priority::PROCESSOR; } +size_t Inkplate6::get_buffer_length_() { + if (this->greyscale_) { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 2u; + } else { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; + } +} +void Inkplate6::update() { + this->do_update_(); + + if (this->full_update_every_ > 0 && this->partial_updates_ >= this->full_update_every_) { + this->block_partial_ = true; + } + + this->display(); +} +void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + return; + + if (this->greyscale_) { + int x1 = x / 2; + int x_sub = x % 2; + uint32_t pos = (x1 + y * (this->get_width_internal() / 2)); + uint8_t current = this->buffer_[pos]; + + // float px = (0.2126 * (color.red / 255.0)) + (0.7152 * (color.green / 255.0)) + (0.0722 * (color.blue / 255.0)); + // px = pow(px, 1.5); + // uint8_t gs = (uint8_t)(px*7); + + uint8_t gs = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; + this->buffer_[pos] = (pixelMaskGLUT[x_sub] & current) | (x_sub ? gs : gs << 4); + + } else { + int x1 = x / 8; + int x_sub = x % 8; + uint32_t pos = (x1 + y * (this->get_width_internal() / 8)); + uint8_t current = this->partial_buffer_[pos]; + this->partial_buffer_[pos] = (~pixelMaskLUT[x_sub] & current) | (color.is_on() ? 0 : pixelMaskLUT[x_sub]); + } +} +void Inkplate6::dump_config() { + LOG_DISPLAY("", "Inkplate", this); + ESP_LOGCONFIG(TAG, " Greyscale: %s", YESNO(this->greyscale_)); + ESP_LOGCONFIG(TAG, " Partial Updating: %s", YESNO(this->partial_updating_)); + ESP_LOGCONFIG(TAG, " Full Update Every: %d", this->full_update_every_); + // Log pins + LOG_PIN(" CKV Pin: ", this->ckv_pin_); + LOG_PIN(" CL Pin: ", this->cl_pin_); + LOG_PIN(" GPIO0 Enable Pin: ", this->gpio0_enable_pin_); + LOG_PIN(" GMOD Pin: ", this->gmod_pin_); + LOG_PIN(" LE Pin: ", this->le_pin_); + LOG_PIN(" OE Pin: ", this->oe_pin_); + LOG_PIN(" POWERUP Pin: ", this->powerup_pin_); + LOG_PIN(" SPH Pin: ", this->sph_pin_); + LOG_PIN(" SPV Pin: ", this->spv_pin_); + LOG_PIN(" VCOM Pin: ", this->vcom_pin_); + LOG_PIN(" WAKEUP Pin: ", this->wakeup_pin_); + + LOG_PIN(" Data 0 Pin: ", this->display_data_0_pin_); + LOG_PIN(" Data 1 Pin: ", this->display_data_1_pin_); + LOG_PIN(" Data 2 Pin: ", this->display_data_2_pin_); + LOG_PIN(" Data 3 Pin: ", this->display_data_3_pin_); + LOG_PIN(" Data 4 Pin: ", this->display_data_4_pin_); + LOG_PIN(" Data 5 Pin: ", this->display_data_5_pin_); + LOG_PIN(" Data 6 Pin: ", this->display_data_6_pin_); + LOG_PIN(" Data 7 Pin: ", this->display_data_7_pin_); + + LOG_UPDATE_INTERVAL(this); +} +void Inkplate6::eink_off_() { + ESP_LOGV(TAG, "Eink off called"); + if (panel_on_ == 0) + return; + panel_on_ = 0; + this->gmod_pin_->digital_write(false); + this->oe_pin_->digital_write(false); + + GPIO.out &= ~(get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()) | (1 << this->le_pin_->get_pin())); + + this->sph_pin_->digital_write(false); + this->spv_pin_->digital_write(false); + + this->powerup_pin_->digital_write(false); + this->wakeup_pin_->digital_write(false); + this->vcom_pin_->digital_write(false); + + pins_z_state_(); +} +void Inkplate6::eink_on_() { + ESP_LOGV(TAG, "Eink on called"); + if (panel_on_ == 1) + return; + panel_on_ = 1; + pins_as_outputs_(); + this->wakeup_pin_->digital_write(true); + this->powerup_pin_->digital_write(true); + this->vcom_pin_->digital_write(true); + + this->write_byte(0x01, 0x3F); + + delay(40); + + this->write_byte(0x0D, 0x80); + + delay(2); + + this->read_register(0x00, nullptr, 0); + + this->le_pin_->digital_write(false); + this->oe_pin_->digital_write(false); + this->cl_pin_->digital_write(false); + this->sph_pin_->digital_write(true); + this->gmod_pin_->digital_write(true); + this->spv_pin_->digital_write(true); + this->ckv_pin_->digital_write(false); + this->oe_pin_->digital_write(true); +} +void Inkplate6::fill(Color color) { + ESP_LOGV(TAG, "Fill called"); + uint32_t start_time = millis(); + + if (this->greyscale_) { + uint8_t fill = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; + memset(this->buffer_, (fill << 4) | fill, this->get_buffer_length_()); + } else { + uint8_t fill = color.is_on() ? 0x00 : 0xFF; + memset(this->partial_buffer_, fill, this->get_buffer_length_()); + } + + ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); +} +void Inkplate6::display() { + ESP_LOGV(TAG, "Display called"); + uint32_t start_time = millis(); + + if (this->greyscale_) { + this->display3b_(); + } else { + if (this->partial_updating_ && this->partial_update_()) { + ESP_LOGV(TAG, "Display finished (partial) (%ums)", millis() - start_time); + return; + } + this->display1b_(); + } + ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); +} +void Inkplate6::display1b_() { + ESP_LOGV(TAG, "Display1b called"); + uint32_t start_time = millis(); + + memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); + + uint32_t send; + uint8_t data; + uint8_t buffer_value; + const uint8_t *buffer_ptr; + eink_on_(); + clean_fast_(0, 1); + clean_fast_(1, 5); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 11); + + uint32_t clock = (1 << this->cl_pin_->get_pin()); + 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 & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); + hscan_start_(send); + data = LUTB[buffer_value & 0x0F]; + 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 & 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 & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + } + GPIO.out_w1ts = send; + GPIO.out_w1tc = get_data_pin_mask_() | clock; + vscan_end_(); + } + delayMicroseconds(230); + } + 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 & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); + hscan_start_(send); + data = LUT2[buffer_value & 0x0F]; + 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 & 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 & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + } + GPIO.out_w1ts = send; + GPIO.out_w1tc = get_data_pin_mask_() | clock; + vscan_end_(); + } + delayMicroseconds(230); + 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 & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); + hscan_start_(send); + send |= clock; + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + } + GPIO.out_w1ts = clock; + GPIO.out_w1tc = get_data_pin_mask_() | clock; + vscan_end_(); + } + delayMicroseconds(230); + 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 (%ums)", millis() - start_time); +} +void Inkplate6::display3b_() { + ESP_LOGV(TAG, "Display3b called"); + uint32_t start_time = millis(); + + eink_on_(); + clean_fast_(0, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 11); + clean_fast_(2, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 11); + + uint32_t clock = (1 << this->cl_pin_->get_pin()); + for (int k = 0; k < 8; k++) { + const uint8_t *buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; + uint32_t send; + uint8_t pix1; + uint8_t pix2; + uint8_t pix3; + uint8_t pix4; + uint8_t pixel; + uint8_t pixel2; + + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + pix1 = (*buffer_ptr--); + pix2 = (*buffer_ptr--); + pix3 = (*buffer_ptr--); + pix4 = (*buffer_ptr--); + pixel = (waveform3Bit[pix1 & 0x07][k] << 6) | (waveform3Bit[(pix1 >> 4) & 0x07][k] << 4) | + (waveform3Bit[pix2 & 0x07][k] << 2) | (waveform3Bit[(pix2 >> 4) & 0x07][k] << 0); + 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 & 0b00000011) << 4) | (((pixel & 0b00001100) >> 2) << 18) | (((pixel & 0b00010000) >> 4) << 23) | + (((pixel & 0b11100000) >> 5) << 25); + hscan_start_(send); + 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; + + for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { + pix1 = (*buffer_ptr--); + pix2 = (*buffer_ptr--); + pix3 = (*buffer_ptr--); + pix4 = (*buffer_ptr--); + pixel = (waveform3Bit[pix1 & 0x07][k] << 6) | (waveform3Bit[(pix1 >> 4) & 0x07][k] << 4) | + (waveform3Bit[pix2 & 0x07][k] << 2) | (waveform3Bit[(pix2 >> 4) & 0x07][k] << 0); + 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 & 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 & 0b00000011) << 4) | (((pixel2 & 0b00001100) >> 2) << 18) | + (((pixel2 & 0b00010000) >> 4) << 23) | (((pixel2 & 0b11100000) >> 5) << 25) | clock; + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + } + GPIO.out_w1ts = send; + GPIO.out_w1tc = get_data_pin_mask_() | clock; + vscan_end_(); + } + delayMicroseconds(230); + } + clean_fast_(2, 1); + clean_fast_(3, 1); + vscan_start_(); + eink_off_(); + ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); +} +bool Inkplate6::partial_update_() { + ESP_LOGV(TAG, "Partial update called"); + uint32_t start_time = millis(); + if (this->greyscale_) + return false; + if (this->block_partial_) + return false; + + this->partial_updates_++; + + uint16_t pos = this->get_buffer_length_() - 1; + uint32_t send; + uint8_t data; + uint8_t diffw, diffb; + uint32_t n = (this->get_buffer_length_() * 2) - 1; + + for (int i = 0, im = this->get_height_internal(); i < im; i++) { + for (int j = 0, jm = (this->get_width_internal() / 8); j < jm; j++) { + diffw = (this->buffer_[pos] ^ this->partial_buffer_[pos]) & ~(this->partial_buffer_[pos]); + diffb = (this->buffer_[pos] ^ this->partial_buffer_[pos]) & this->partial_buffer_[pos]; + pos--; + this->partial_buffer_2_[n--] = LUTW[diffw >> 4] & LUTB[diffb >> 4]; + this->partial_buffer_2_[n--] = LUTW[diffw & 0x0F] & LUTB[diffb & 0x0F]; + } + } + ESP_LOGV(TAG, "Partial update buffer built after (%ums)", millis() - start_time); + + eink_on_(); + uint32_t clock = (1 << this->cl_pin_->get_pin()); + for (int k = 0; k < 5; k++) { + vscan_start_(); + const uint8_t *data_ptr = &this->partial_buffer_2_[(this->get_buffer_length_() * 2) - 1]; + for (int i = 0; i < this->get_height_internal(); i++) { + data = *(data_ptr--); + 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 & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; + GPIO.out_w1ts = send; + GPIO.out_w1tc = send; + } + GPIO.out_w1ts = send; + GPIO.out_w1tc = get_data_pin_mask_() | clock; + vscan_end_(); + } + delayMicroseconds(230); + ESP_LOGV(TAG, "Partial update loop k=%d (%ums)", k, millis() - start_time); + } + clean_fast_(2, 2); + clean_fast_(3, 1); + vscan_start_(); + eink_off_(); + + memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); + ESP_LOGV(TAG, "Partial update finished (%ums)", millis() - start_time); + return true; +} +void Inkplate6::vscan_start_() { + this->ckv_pin_->digital_write(true); + delayMicroseconds(7); + this->spv_pin_->digital_write(false); + delayMicroseconds(10); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); + delayMicroseconds(8); + this->spv_pin_->digital_write(true); + delayMicroseconds(10); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); + delayMicroseconds(18); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); + delayMicroseconds(18); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); +} +void Inkplate6::vscan_write_() { + this->ckv_pin_->digital_write(false); + this->le_pin_->digital_write(true); + this->le_pin_->digital_write(false); + delayMicroseconds(0); + this->sph_pin_->digital_write(false); + this->cl_pin_->digital_write(true); + this->cl_pin_->digital_write(false); + this->sph_pin_->digital_write(true); + this->ckv_pin_->digital_write(true); +} +void Inkplate6::hscan_start_(uint32_t d) { + this->sph_pin_->digital_write(false); + GPIO.out_w1ts = (d) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + this->sph_pin_->digital_write(true); +} +void Inkplate6::vscan_end_() { + this->ckv_pin_->digital_write(false); + this->le_pin_->digital_write(true); + this->le_pin_->digital_write(false); + delayMicroseconds(1); + this->ckv_pin_->digital_write(true); +} +void Inkplate6::clean() { + ESP_LOGV(TAG, "Clean called"); + uint32_t start_time = millis(); + + eink_on_(); + clean_fast_(0, 1); // White + clean_fast_(0, 8); // White to White + clean_fast_(0, 1); // White to Black + 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 (%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); + uint32_t start_time = millis(); + + eink_on_(); + uint8_t data = 0; + if (c == 0) // White + data = 0b10101010; + else if (c == 1) // Black + data = 0b01010101; + else if (c == 2) // Discharge + data = 0b00000000; + else if (c == 3) // Skip + data = 0b11111111; + + 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++) { + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + hscan_start_(send); + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = clock; + for (int j = 0, jm = this->get_width_internal() / 8; j < jm; j++) { + GPIO.out_w1ts = clock; + GPIO.out_w1tc = clock; + GPIO.out_w1ts = clock; + GPIO.out_w1tc = clock; + } + GPIO.out_w1ts = clock; + GPIO.out_w1tc = get_data_pin_mask_() | clock; + vscan_end_(); + } + delayMicroseconds(230); + ESP_LOGV(TAG, "Clean fast rep loop %d finished (%ums)", k, millis() - start_time); + } + ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); +} +void Inkplate6::pins_z_state_() { + this->ckv_pin_->pin_mode(gpio::FLAG_INPUT); + this->sph_pin_->pin_mode(gpio::FLAG_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(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(gpio::FLAG_OUTPUT); + this->sph_pin_->pin_mode(gpio::FLAG_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(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 // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h new file mode 100644 index 0000000000..56e95e95bb --- /dev/null +++ b/esphome/components/inkplate6/inkplate.h @@ -0,0 +1,161 @@ +#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 USE_ESP32_FRAMEWORK_ARDUINO + +namespace esphome { +namespace inkplate6 { + +class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { + public: + 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}}; + const uint32_t waveform[50] = { + 0x00000008, 0x00000008, 0x00200408, 0x80281888, 0x60a81898, 0x60a8a8a8, 0x60a8a8a8, 0x6068a868, 0x6868a868, + 0x6868a868, 0x68686868, 0x6a686868, 0x5a686868, 0x5a686868, 0x5a586a68, 0x5a5a6a68, 0x5a5a6a68, 0x55566a68, + 0x55565a64, 0x55555654, 0x55555556, 0x55555556, 0x55555556, 0x55555516, 0x55555596, 0x15555595, 0x95955595, + 0x95959595, 0x95949495, 0x94949495, 0x94949495, 0xa4949494, 0x9494a4a4, 0x84a49494, 0x84948484, 0x84848484, + 0x84848484, 0x84848484, 0xa5a48484, 0xa9a4a4a8, 0xa9a8a8a8, 0xa5a9a9a4, 0xa5a5a5a4, 0xa1a5a5a1, 0xa9a9a9a9, + 0xa9a9a9a9, 0xa9a9a9a9, 0xa9a9a9a9, 0x15151515, 0x11111111}; + + void set_greyscale(bool greyscale) { + this->greyscale_ = greyscale; + this->initialize_(); + this->block_partial_ = true; + } + 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(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(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(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; } + void set_spv_pin(GPIOPin *spv) { this->spv_pin_ = spv; } + void set_vcom_pin(GPIOPin *vcom) { this->vcom_pin_ = vcom; } + void set_wakeup_pin(GPIOPin *wakeup) { this->wakeup_pin_ = wakeup; } + + float get_setup_priority() const override; + + void dump_config() override; + + void display(); + void clean(); + void fill(Color color) override; + + void update() override; + + void setup() override; + + uint8_t get_panel_state() { return this->panel_on_; } + bool get_greyscale() { return this->greyscale_; } + bool get_partial_updating() { return this->partial_updating_; } + uint8_t get_temperature() { return this->temperature_; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void display1b_(); + void display3b_(); + void initialize_(); + bool partial_update_(); + void clean_fast_(uint8_t c, uint8_t rep); + + void hscan_start_(uint32_t d); + void vscan_end_(); + void vscan_start_(); + void vscan_write_(); + + void eink_off_(); + void eink_on_(); + + void setup_pins_(); + void pins_z_state_(); + void pins_as_outputs_(); + + int get_width_internal() override { return 800; } + + int get_height_internal() override { return 600; } + + size_t get_buffer_length_(); + + int get_data_pin_mask_() { + int data = 0; + data |= (1 << this->display_data_0_pin_->get_pin()); + data |= (1 << this->display_data_1_pin_->get_pin()); + data |= (1 << this->display_data_2_pin_->get_pin()); + data |= (1 << this->display_data_3_pin_->get_pin()); + data |= (1 << this->display_data_4_pin_->get_pin()); + data |= (1 << this->display_data_5_pin_->get_pin()); + data |= (1 << this->display_data_6_pin_->get_pin()); + data |= (1 << this->display_data_7_pin_->get_pin()); + return data; + } + + uint8_t panel_on_ = 0; + uint8_t temperature_; + + uint8_t *partial_buffer_{nullptr}; + uint8_t *partial_buffer_2_{nullptr}; + + uint32_t full_update_every_; + uint32_t partial_updates_{0}; + + bool block_partial_; + bool greyscale_; + bool partial_updating_; + + 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_; + InternalGPIOPin *cl_pin_; + GPIOPin *gpio0_enable_pin_; + GPIOPin *gmod_pin_; + InternalGPIOPin *le_pin_; + GPIOPin *oe_pin_; + GPIOPin *powerup_pin_; + GPIOPin *sph_pin_; + GPIOPin *spv_pin_; + GPIOPin *vcom_pin_; + GPIOPin *wakeup_pin_; +}; + +} // namespace inkplate6 +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/integration/__init__.py b/esphome/components/integration/__init__.py index e69de29bb2..71a87b6ae5 100644 --- a/esphome/components/integration/__init__.py +++ b/esphome/components/integration/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index f9b5a43870..2a398e5240 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -1,19 +1,24 @@ #include "integration_sensor.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace integration { -static const char *TAG = "integration"; +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_.load(&this->result_); + 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 a354ab7433..26c7c2871a 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -2,56 +2,86 @@ 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_('IntegrationSensor', sensor.Sensor, cg.Component) -ResetAction = integration_ns.class_('ResetAction', automation.Action) +integration_ns = cg.esphome_ns.namespace("integration") +IntegrationSensor = integration_ns.class_( + "IntegrationSensor", sensor.Sensor, cg.Component +) +ResetAction = integration_ns.class_("ResetAction", automation.Action) -IntegrationSensorTime = integration_ns.enum('IntegrationSensorTime') +IntegrationSensorTime = integration_ns.enum("IntegrationSensorTime") INTEGRATION_TIMES = { - 'ms': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_MILLISECOND, - 's': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_SECOND, - 'min': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_MINUTE, - 'h': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_HOUR, - 'd': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_DAY, + "ms": IntegrationSensorTime.INTEGRATION_SENSOR_TIME_MILLISECOND, + "s": IntegrationSensorTime.INTEGRATION_SENSOR_TIME_SECOND, + "min": IntegrationSensorTime.INTEGRATION_SENSOR_TIME_MINUTE, + "h": IntegrationSensorTime.INTEGRATION_SENSOR_TIME_HOUR, + "d": IntegrationSensorTime.INTEGRATION_SENSOR_TIME_DAY, } -IntegrationMethod = integration_ns.enum('IntegrationMethod') +IntegrationMethod = integration_ns.enum("IntegrationMethod") INTEGRATION_METHODS = { - 'trapezoid': IntegrationMethod.INTEGRATION_METHOD_TRAPEZOID, - 'left': IntegrationMethod.INTEGRATION_METHOD_LEFT, - 'right': IntegrationMethod.INTEGRATION_METHOD_RIGHT, + "trapezoid": IntegrationMethod.INTEGRATION_METHOD_TRAPEZOID, + "left": IntegrationMethod.INTEGRATION_METHOD_LEFT, + "right": IntegrationMethod.INTEGRATION_METHOD_RIGHT, } -CONF_TIME_UNIT = 'time_unit' -CONF_INTEGRATION_METHOD = 'integration_method' +CONF_TIME_UNIT = "time_unit" +CONF_INTEGRATION_METHOD = "integration_method" +CONF_MIN_SAVE_INTERVAL = "min_save_interval" -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(IntegrationSensor), - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_TIME_UNIT): cv.enum(INTEGRATION_TIMES, lower=True), - cv.Optional(CONF_INTEGRATION_METHOD, default='trapezoid'): - cv.enum(INTEGRATION_METHODS, lower=True), - cv.Optional(CONF_RESTORE, default=False): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(IntegrationSensor), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_TIME_UNIT): cv.enum(INTEGRATION_TIMES, lower=True), + cv.Optional(CONF_INTEGRATION_METHOD, default="trapezoid"): cv.enum( + 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) -def to_code(config): +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]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) 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('sensor.integration.reset', ResetAction, automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(IntegrationSensor), -})) -def sensor_integration_reset_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) +@automation.register_action( + "sensor.integration.reset", + ResetAction, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(IntegrationSensor), + } + ), +) +async def sensor_integration_reset_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index e0816b7407..4514f80ba3 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -3,20 +3,26 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ID, CONF_INTERVAL -interval_ns = cg.esphome_ns.namespace('interval') -IntervalTrigger = interval_ns.class_('IntervalTrigger', automation.Trigger.template(), - cg.PollingComponent) +CODEOWNERS = ["@esphome/core"] +interval_ns = cg.esphome_ns.namespace("interval") +IntervalTrigger = interval_ns.class_( + "IntervalTrigger", automation.Trigger.template(), cg.PollingComponent +) -CONFIG_SCHEMA = automation.validate_automation(cv.Schema({ - cv.GenerateID(): cv.declare_id(IntervalTrigger), - cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, -}).extend(cv.COMPONENT_SCHEMA)) +CONFIG_SCHEMA = automation.validate_automation( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(IntervalTrigger), + cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, + } + ).extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): +async def to_code(config): for conf in config: var = cg.new_Pvariable(conf[CONF_ID]) - yield cg.register_component(var, conf) - yield automation.build_automation(var, [], conf) + await cg.register_component(var, conf) + await automation.build_automation(var, [], conf) cg.add(var.set_update_interval(conf[CONF_INTERVAL])) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index f719b05340..fda0a552f1 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,11 +1,18 @@ import esphome.codegen as cg +import esphome.config_validation as cv from esphome.core import coroutine_with_priority -json_ns = cg.esphome_ns.namespace('json') +CODEOWNERS = ["@OttoWinter"] +json_ns = cg.esphome_ns.namespace("json") + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + cv.only_with_arduino, +) @coroutine_with_priority(1.0) -def to_code(config): - cg.add_library('ArduinoJson-esphomelib', '5.13.3') - cg.add_define('USE_JSON') +async def to_code(config): + 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 b82ddf90f0..12c5beb73f 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,26 +1,14 @@ +#ifdef USE_ARDUINO + #include "json_util.h" #include "esphome/core/log.h" namespace esphome { namespace json { -static const char *TAG = "json"; +static const char *const TAG = "json"; -static char *global_json_build_buffer = nullptr; -static size_t global_json_build_buffer_size = 0; - -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_); @@ -123,7 +111,9 @@ void VectorJsonBuffer::reserve(size_t size) { // NOLINT size_t VectorJsonBuffer::size() const { return this->size_; } -VectorJsonBuffer global_json_buffer; +VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace json } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index d7b5267064..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 @@ -56,7 +60,9 @@ class VectorJsonBuffer : public ArduinoJson::Internals::JsonBufferBase free_blocks_; }; -extern VectorJsonBuffer global_json_buffer; +extern VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace json } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index bff194578c..0ed2036c55 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -2,10 +2,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display from esphome.const import CONF_DIMENSIONS -from esphome.core import coroutine -lcd_base_ns = cg.esphome_ns.namespace('lcd_base') -LCDDisplay = lcd_base_ns.class_('LCDDisplay', cg.PollingComponent) +lcd_base_ns = cg.esphome_ns.namespace("lcd_base") +LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent) def validate_lcd_dimensions(value): @@ -17,13 +16,14 @@ def validate_lcd_dimensions(value): return value -LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ - cv.Required(CONF_DIMENSIONS): validate_lcd_dimensions, -}).extend(cv.polling_component_schema('1s')) +LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.Required(CONF_DIMENSIONS): validate_lcd_dimensions, + } +).extend(cv.polling_component_schema("1s")) -@coroutine -def setup_lcd_display(var, config): - yield cg.register_component(var, config) - yield display.register_display(var, config) +async def setup_lcd_display(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 25ac143817..ddd7d6a6b3 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -1,11 +1,12 @@ #include "lcd_display.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace lcd_base { -static const char *TAG = "lcd"; +static const char *const TAG = "lcd"; // First set bit determines command, bits after that are the data. static const uint8_t LCD_DISPLAY_COMMAND_CLEAR_DISPLAY = 0x01; @@ -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] = ' '; @@ -104,9 +105,7 @@ void HOT LCDDisplay::display() { } } void LCDDisplay::update() { - for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) - this->buffer_[i] = ' '; - + this->clear(); this->call_writer(); this->display(); } @@ -149,9 +148,8 @@ void LCDDisplay::printf(const char *format, ...) { this->print(0, 0, buffer); } void LCDDisplay::clear() { - // clear display, also sets DDRAM address to 0 (home) - this->command_(LCD_DISPLAY_COMMAND_CLEAR_DISPLAY); - delay(2); + for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) + this->buffer_[i] = ' '; } #ifdef USE_TIME void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index 91498d59c9..9fb635eafa 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -2,50 +2,63 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import lcd_base -from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID, \ - CONF_LAMBDA +from esphome.const import ( + CONF_DATA_PINS, + CONF_ENABLE_PIN, + CONF_RS_PIN, + CONF_RW_PIN, + CONF_ID, + CONF_LAMBDA, +) -AUTO_LOAD = ['lcd_base'] +AUTO_LOAD = ["lcd_base"] -lcd_gpio_ns = cg.esphome_ns.namespace('lcd_gpio') -GPIOLCDDisplay = lcd_gpio_ns.class_('GPIOLCDDisplay', lcd_base.LCDDisplay) +lcd_gpio_ns = cg.esphome_ns.namespace("lcd_gpio") +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))) + raise cv.Invalid( + f"LCD Displays can either operate in 4-pin or 8-pin mode,not {len(value)}-pin mode" + ) return value -CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(GPIOLCDDisplay), - cv.Required(CONF_DATA_PINS): cv.All([pins.gpio_output_pin_schema], validate_pin_length), - cv.Required(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_RS_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_RW_PIN): pins.gpio_output_pin_schema, -}) +CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GPIOLCDDisplay), + cv.Required(CONF_DATA_PINS): cv.All( + [pins.gpio_output_pin_schema], validate_pin_length + ), + cv.Required(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_RS_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RW_PIN): pins.gpio_output_pin_schema, + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield lcd_base.setup_lcd_display(var, config) + await lcd_base.setup_lcd_display(var, config) pins_ = [] for conf in config[CONF_DATA_PINS]: - pins_.append((yield cg.gpio_pin_expression(conf))) + pins_.append((await cg.gpio_pin_expression(conf))) cg.add(var.set_data_pins(*pins_)) - enable = yield cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) + enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable)) - rs = yield cg.gpio_pin_expression(config[CONF_RS_PIN]) + rs = await cg.gpio_pin_expression(config[CONF_RS_PIN]) cg.add(var.set_rs_pin(rs)) if CONF_RW_PIN in config: - rw = yield cg.gpio_pin_expression(config[CONF_RW_PIN]) + rw = await cg.gpio_pin_expression(config[CONF_RW_PIN]) cg.add(var.set_rw_pin(rw)) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], - [(GPIOLCDDisplay.operator('ref'), 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], + [(GPIOLCDDisplay.operator("ref"), "it")], + return_type=cg.void, + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.cpp b/esphome/components/lcd_gpio/gpio_lcd_display.cpp index 96d074bec8..94ddc34051 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.cpp +++ b/esphome/components/lcd_gpio/gpio_lcd_display.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace lcd_gpio { -static const char *TAG = "lcd_gpio"; +static const char *const TAG = "lcd_gpio"; void GPIOLCDDisplay::setup() { ESP_LOGCONFIG(TAG, "Setting up GPIO LCD Display..."); @@ -17,7 +17,7 @@ void GPIOLCDDisplay::setup() { this->enable_pin_->setup(); // OUTPUT this->enable_pin_->digital_write(false); - for (uint8_t i = 0; i < (this->is_four_bit_mode() ? 4 : 8); i++) { + for (uint8_t i = 0; i < (uint8_t)(this->is_four_bit_mode() ? 4u : 8u); i++) { this->data_pins_[i]->setup(); // OUTPUT this->data_pins_[i]->digital_write(false); } @@ -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/display.py b/esphome/components/lcd_pcf8574/display.py index 2bbb3a2f7b..5d9dd7adba 100644 --- a/esphome/components/lcd_pcf8574/display.py +++ b/esphome/components/lcd_pcf8574/display.py @@ -3,24 +3,30 @@ import esphome.config_validation as cv from esphome.components import lcd_base, i2c from esphome.const import CONF_ID, CONF_LAMBDA -DEPENDENCIES = ['i2c'] -AUTO_LOAD = ['lcd_base'] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["lcd_base"] -lcd_pcf8574_ns = cg.esphome_ns.namespace('lcd_pcf8574') -PCF8574LCDDisplay = lcd_pcf8574_ns.class_('PCF8574LCDDisplay', lcd_base.LCDDisplay, i2c.I2CDevice) +lcd_pcf8574_ns = cg.esphome_ns.namespace("lcd_pcf8574") +PCF8574LCDDisplay = lcd_pcf8574_ns.class_( + "PCF8574LCDDisplay", lcd_base.LCDDisplay, i2c.I2CDevice +) -CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(PCF8574LCDDisplay), -}).extend(i2c.i2c_device_schema(0x3F)) +CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PCF8574LCDDisplay), + } +).extend(i2c.i2c_device_schema(0x3F)) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield lcd_base.setup_lcd_display(var, config) - yield i2c.register_i2c_device(var, config) + await lcd_base.setup_lcd_display(var, config) + await i2c.register_i2c_device(var, config) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], - [(PCF8574LCDDisplay.operator('ref'), 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], + [(PCF8574LCDDisplay.operator("ref"), "it")], + return_type=cg.void, + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp index e3002da25d..5b00b08aff 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp +++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp @@ -1,10 +1,11 @@ #include "pcf8574_display.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace lcd_pcf8574 { -static const char *TAG = "lcd_pcf8574"; +static const char *const TAG = "lcd_pcf8574"; static const uint8_t LCD_DISPLAY_BACKLIGHT_ON = 0x08; static const uint8_t LCD_DISPLAY_BACKLIGHT_OFF = 0x00; diff --git a/esphome/components/ledc/__init__.py b/esphome/components/ledc/__init__.py index e69de29bb2..71a87b6ae5 100644 --- a/esphome/components/ledc/__init__.py +++ b/esphome/components/ledc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 2b1c181a62..a56dccfd72 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -1,16 +1,61 @@ #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 *TAG = "ledc.output"; +static const char *const TAG = "ledc.output"; + +#ifdef USE_ESP_IDF +static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; +#if SOC_LEDC_SUPPORT_HS_MODE +// Only ESP32 has LEDC_HIGH_SPEED_MODE +inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; } +#else +// S2, C3, S3 only support LEDC_LOW_SPEED_MODE +// See +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/ledc.html#functionality-overview +inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } +#endif +#else +static const int MAX_RES_BITS = 20; +#endif + +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, bool low_frequency) { + const float max_div_num = ((1 << MAX_RES_BITS) - 1) / (low_frequency ? 32.0f : 256.0f); + return 80e6f / (max_div_num * float(1 << bit_depth)); +} + +optional ledc_bit_depth_for_frequency(float frequency) { + ESP_LOGD(TAG, "Calculating resolution bit-depth for frequency %f", frequency); + for (int i = MAX_RES_BITS; i >= 1; i--) { + const float min_frequency = ledc_min_frequency_for_bit_depth(i, (frequency < 100)); + const float max_frequency = ledc_max_frequency_for_bit_depth(i); + if (min_frequency <= frequency && frequency <= max_frequency) { + ESP_LOGD(TAG, "Resolution calculated as %d", i); + return i; + } + } + 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; @@ -18,14 +63,55 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); + +#ifdef USE_ARDUINO ledcWrite(this->channel_, duty); +#endif +#ifdef USE_ESP_IDF + auto speed_mode = get_speed_mode(channel_); + auto chan_num = static_cast(channel_ % 8); + ledc_set_duty(speed_mode, chan_num, duty); + ledc_update_duty(speed_mode, chan_num); +#endif } void LEDCOutput::setup() { - this->apply_frequency(this->frequency_); +#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 = get_speed_mode(channel_); + auto timer_num = static_cast((channel_ % 8) / 2); + auto chan_num = static_cast(channel_ % 8); + + bit_depth_ = *ledc_bit_depth_for_frequency(frequency_); + if (bit_depth_ < 1) { + ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency_); + this->status_set_warning(); + } + + 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() { @@ -35,22 +121,7 @@ void LEDCOutput::dump_config() { 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; - return 80e6f / (max_div_num * float(1 << bit_depth)); -} -optional ledc_bit_depth_for_frequency(float frequency) { - for (int i = 20; i >= 1; i--) { - const float min_frequency = ledc_min_frequency_for_bit_depth(i); - const float max_frequency = ledc_max_frequency_for_bit_depth(i); - if (min_frequency <= frequency && frequency <= max_frequency) - return i; - } - return {}; -} - -void LEDCOutput::apply_frequency(float frequency) { +void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); @@ -58,12 +129,31 @@ void LEDCOutput::apply_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 = get_speed_mode(channel_); + 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 3f56f502b0..a78bf440a9 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -1,25 +1,26 @@ #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; } /// Dynamically change frequency at runtime - void apply_frequency(float frequency); + void update_frequency(float frequency) override; /// Setup LEDC. void setup() override; @@ -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 { @@ -45,7 +47,7 @@ template class SetFrequencyAction : public Action { void play(Ts... x) { float freq = this->frequency_.value(x...); - this->parent_->apply_frequency(freq); + this->parent_->update_frequency(freq); } protected: diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index b608e9bbf7..895dcc998b 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -2,19 +2,23 @@ from esphome import pins, automation from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_BIT_DEPTH, CONF_CHANNEL, CONF_FREQUENCY, \ - CONF_ID, CONF_PIN, ESP_PLATFORM_ESP32 +from esphome.const import ( + CONF_CHANNEL, + CONF_FREQUENCY, + CONF_ID, + CONF_PIN, +) -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] def calc_max_frequency(bit_depth): - return 80e6 / (2**bit_depth) + return 80e6 / (2 ** bit_depth) def calc_min_frequency(bit_depth): - max_div_num = ((2**20) - 1) / 256.0 - return 80e6 / (max_div_num * (2**bit_depth)) + max_div_num = ((2 ** 20) - 1) / 256.0 + return 80e6 / (max_div_num * (2 ** bit_depth)) def validate_frequency(value): @@ -22,46 +26,53 @@ def validate_frequency(value): min_freq = calc_min_frequency(20) 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))) + raise cv.Invalid( + 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))) + raise cv.Invalid( + f"This frequency setting is not possible, please choose a lower frequency (at most {int(max_freq)}Hz)" + ) return value -ledc_ns = cg.esphome_ns.namespace('ledc') -LEDCOutput = ledc_ns.class_('LEDCOutput', output.FloatOutput, cg.Component) -SetFrequencyAction = ledc_ns.class_('SetFrequencyAction', automation.Action) +ledc_ns = cg.esphome_ns.namespace("ledc") +LEDCOutput = ledc_ns.class_("LEDCOutput", output.FloatOutput, cg.Component) +SetFrequencyAction = ledc_ns.class_("SetFrequencyAction", automation.Action) -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(LEDCOutput), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default='1kHz'): cv.frequency, - cv.Optional(CONF_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) +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(LEDCOutput), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): - gpio = yield cg.gpio_pin_expression(config[CONF_PIN]) +async def to_code(config): + gpio = await cg.gpio_pin_expression(config[CONF_PIN]) var = cg.new_Pvariable(config[CONF_ID], gpio) - yield cg.register_component(var, config) - yield output.register_output(var, config) + await cg.register_component(var, config) + await output.register_output(var, config) if CONF_CHANNEL in config: cg.add(var.set_channel(config[CONF_CHANNEL])) cg.add(var.set_frequency(config[CONF_FREQUENCY])) -@automation.register_action('output.ledc.set_frequency', SetFrequencyAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(LEDCOutput), - cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), -})) -def ledc_set_frequency_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "output.ledc.set_frequency", + SetFrequencyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LEDCOutput), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), + } + ), +) +async def ledc_set_frequency_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) cg.add(var.set_frequency(template_)) - yield var + return var diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 2a44b044b9..fe8a90b8db 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -2,103 +2,186 @@ import esphome.codegen as cg import esphome.config_validation as cv import esphome.automation as auto from esphome.components import mqtt, power_supply -from esphome.const import CONF_COLOR_CORRECT, \ - CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, 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 -from esphome.core import coroutine, coroutine_with_priority +from esphome.const import ( + CONF_COLOR_CORRECT, + CONF_DEFAULT_TRANSITION_LENGTH, + CONF_EFFECTS, + CONF_FLASH_TRANSITION_LENGTH, + CONF_GAMMA_CORRECT, + CONF_ID, + CONF_MQTT_ID, + CONF_POWER_SUPPLY, + CONF_RESTORE_MODE, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, + CONF_ON_STATE, + 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, BINARY_EFFECTS, \ - MONOCHROMATIC_EFFECTS, RGB_EFFECTS, ADDRESSABLE_EFFECTS, EFFECTS_REGISTRY +from .effects import ( + validate_effects, + BINARY_EFFECTS, + MONOCHROMATIC_EFFECTS, + RGB_EFFECTS, + ADDRESSABLE_EFFECTS, + EFFECTS_REGISTRY, +) from .types import ( # noqa - LightState, AddressableLightState, light_ns, LightOutput, AddressableLight, \ - LightTurnOnTrigger, LightTurnOffTrigger) + LightState, + AddressableLightState, + light_ns, + LightOutput, + AddressableLight, + LightTurnOnTrigger, + LightTurnOffTrigger, + LightStateTrigger, +) +CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True -LightRestoreMode = light_ns.enum('LightRestoreMode') +LightRestoreMode = light_ns.enum("LightRestoreMode") RESTORE_MODES = { - 'RESTORE_DEFAULT_OFF': LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, - 'RESTORE_DEFAULT_ON': LightRestoreMode.LIGHT_RESTORE_DEFAULT_ON, - 'ALWAYS_OFF': LightRestoreMode.LIGHT_ALWAYS_OFF, - 'ALWAYS_ON': LightRestoreMode.LIGHT_ALWAYS_ON, + "RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, + "RESTORE_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_DEFAULT_ON, + "ALWAYS_OFF": LightRestoreMode.LIGHT_ALWAYS_OFF, + "ALWAYS_ON": LightRestoreMode.LIGHT_ALWAYS_ON, + "RESTORE_INVERTED_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_OFF, + "RESTORE_INVERTED_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_ON, } -LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(LightState), - cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTJSONLightComponent), - cv.Optional(CONF_RESTORE_MODE, default='restore_default_off'): - cv.enum(RESTORE_MODES, upper=True, space='_'), - cv.Optional(CONF_ON_TURN_ON): auto.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger), - }), - cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), - }), -}) +LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(LightState), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTJSONLightComponent), + cv.Optional(CONF_RESTORE_MODE, default="restore_default_off"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_ON_TURN_ON): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), + } + ), + cv.Optional(CONF_ON_STATE): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightStateTrigger), + } + ), + } +) -BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend({ - cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS), -}) +BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( + { + cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS), + } +) -BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend({ - cv.Optional(CONF_GAMMA_CORRECT, default=2.8): cv.positive_float, - cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='1s'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), -}) +BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( + { + cv.Optional(CONF_GAMMA_CORRECT, default=2.8): cv.positive_float, + 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), + } +) -RGB_LIGHT_SCHEMA = BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ - cv.Optional(CONF_EFFECTS): validate_effects(RGB_EFFECTS), -}) +RGB_LIGHT_SCHEMA = BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.Optional(CONF_EFFECTS): validate_effects(RGB_EFFECTS), + } +) -ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(AddressableLightState), - cv.Optional(CONF_EFFECTS): validate_effects(ADDRESSABLE_EFFECTS), - cv.Optional(CONF_COLOR_CORRECT): cv.All([cv.percentage], cv.Length(min=3, max=4)), - cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), -}) +ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AddressableLightState), + cv.Optional(CONF_EFFECTS): validate_effects(ADDRESSABLE_EFFECTS), + cv.Optional(CONF_COLOR_CORRECT): cv.All( + [cv.percentage], cv.Length(min=3, max=4) + ), + cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), + } +) -@coroutine -def setup_light_core_(light_var, output_var, config): +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])) + 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 = yield cg.build_registry_list(EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])) + effects = await cg.build_registry_list( + EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) + ) cg.add(light_var.add_effects(effects)) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) - yield auto.build_automation(trigger, [], conf) + await auto.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) - yield auto.build_automation(trigger, [], conf) + await auto.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) + await auto.build_automation(trigger, [], conf) if CONF_COLOR_CORRECT in config: cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT])) if CONF_POWER_SUPPLY in config: - var_ = yield cg.get_variable(config[CONF_POWER_SUPPLY]) + var_ = await cg.get_variable(config[CONF_POWER_SUPPLY]) cg.add(output_var.set_power_supply(var_)) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], light_var) - yield mqtt.register_mqtt_component(mqtt_, config) + await mqtt.register_mqtt_component(mqtt_, config) -@coroutine -def register_light(output_var, config): - light_var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME], output_var) +async def register_light(output_var, config): + light_var = cg.new_Pvariable(config[CONF_ID], output_var) cg.add(cg.App.register_light(light_var)) - yield cg.register_component(light_var, config) - yield setup_light_core_(light_var, output_var, config) + await cg.register_component(light_var, config) + await setup_light_core_(light_var, output_var, config) @coroutine_with_priority(100.0) -def to_code(config): - cg.add_define('USE_LIGHT') +async def to_code(config): + cg.add_define("USE_LIGHT") cg.add_global(light_ns.using) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index b5dc70a083..a8e0c7b762 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -4,162 +4,7 @@ namespace esphome { namespace light { -static const char *TAG = "light.addressable"; - -const ESPColor ESPColor::BLACK = ESPColor(0, 0, 0, 0); -const ESPColor ESPColor::WHITE = ESPColor(255, 255, 255, 255); - -ESPColor 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); - ESPColor 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 ESPColor &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) { - // 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; -} +static const char *const TAG = "light.addressable"; void AddressableLight::call_setup() { this->setup(); @@ -167,112 +12,101 @@ void AddressableLight::call_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 } -ESPColor 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)); - return ESPColor(r, g, b, w); +std::unique_ptr AddressableLight::create_default_transition() { + return make_unique(*this); } -void AddressableLight::write_state(LightState *state) { - auto val = state->current_values; - auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); - this->correction_.set_local_brightness(max_brightness); +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); +} - this->last_transition_progress_ = 0.0f; - this->accumulated_alpha_ = 0.0f; +void AddressableLight::update_state(LightState *state) { + auto val = state->current_values; + auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); + this->correction_.set_local_brightness(max_brightness); 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(); - ESPColor 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; - ESPColor 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 ? 1.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 a95d70f274..8302239d6a 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -2,8 +2,13 @@ #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" @@ -12,410 +17,15 @@ namespace esphome { namespace light { -inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; } +using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color; -struct ESPColor { - union { - struct { - union { - uint8_t r; - uint8_t red; - }; - union { - uint8_t g; - uint8_t green; - }; - union { - uint8_t b; - uint8_t blue; - }; - union { - uint8_t w; - uint8_t white; - }; - }; - uint8_t raw[4]; - uint32_t raw_32; - }; - inline ESPColor() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT - inline ESPColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ALWAYS_INLINE : r(red), - g(green), - b(blue), - w(white) {} - inline ESPColor(uint8_t red, uint8_t green, uint8_t blue) ALWAYS_INLINE : r(red), g(green), b(blue), w(0) {} - inline ESPColor(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), - g((colorcode >> 8) & 0xFF), - b((colorcode >> 0) & 0xFF), - w((colorcode >> 24) & 0xFF) {} - inline ESPColor(const ESPColor &rhs) ALWAYS_INLINE { - this->r = rhs.r; - this->g = rhs.g; - this->b = rhs.b; - this->w = rhs.w; - } - inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } - inline ESPColor &operator=(const ESPColor &rhs) ALWAYS_INLINE { - this->r = rhs.r; - this->g = rhs.g; - this->b = rhs.b; - this->w = rhs.w; - return *this; - } - inline ESPColor &operator=(uint32_t colorcode) ALWAYS_INLINE { - this->w = (colorcode >> 24) & 0xFF; - this->r = (colorcode >> 16) & 0xFF; - this->g = (colorcode >> 8) & 0xFF; - this->b = (colorcode >> 0) & 0xFF; - return *this; - } - inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; } - inline ESPColor operator*(uint8_t scale) const ALWAYS_INLINE { - return ESPColor(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale), - esp_scale8(this->white, scale)); - } - inline ESPColor &operator*=(uint8_t scale) ALWAYS_INLINE { - this->red = esp_scale8(this->red, scale); - this->green = esp_scale8(this->green, scale); - this->blue = esp_scale8(this->blue, scale); - this->white = esp_scale8(this->white, scale); - return *this; - } - inline ESPColor operator*(const ESPColor &scale) const ALWAYS_INLINE { - return ESPColor(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green), - esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white)); - } - inline ESPColor &operator*=(const ESPColor &scale) ALWAYS_INLINE { - this->red = esp_scale8(this->red, scale.red); - this->green = esp_scale8(this->green, scale.green); - this->blue = esp_scale8(this->blue, scale.blue); - this->white = esp_scale8(this->white, scale.white); - return *this; - } - inline ESPColor operator+(const ESPColor &add) const ALWAYS_INLINE { - ESPColor ret; - if (uint8_t(add.r + this->r) < this->r) - ret.r = 255; - else - ret.r = this->r + add.r; - if (uint8_t(add.g + this->g) < this->g) - ret.g = 255; - else - ret.g = this->g + add.g; - if (uint8_t(add.b + this->b) < this->b) - ret.b = 255; - else - ret.b = this->b + add.b; - if (uint8_t(add.w + this->w) < this->w) - ret.w = 255; - else - ret.w = this->w + add.w; - return ret; - } - inline ESPColor &operator+=(const ESPColor &add) ALWAYS_INLINE { return *this = (*this) + add; } - inline ESPColor operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + ESPColor(add, add, add, add); } - inline ESPColor &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; } - inline ESPColor operator-(const ESPColor &subtract) const ALWAYS_INLINE { - ESPColor ret; - if (subtract.r > this->r) - ret.r = 0; - else - ret.r = this->r - subtract.r; - if (subtract.g > this->g) - ret.g = 0; - else - ret.g = this->g - subtract.g; - if (subtract.b > this->b) - ret.b = 0; - else - ret.b = this->b - subtract.b; - if (subtract.w > this->w) - ret.w = 0; - else - ret.w = this->w - subtract.w; - return ret; - } - inline ESPColor &operator-=(const ESPColor &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } - inline ESPColor operator-(uint8_t subtract) const ALWAYS_INLINE { - return (*this) - ESPColor(subtract, subtract, subtract, subtract); - } - inline ESPColor &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } - static ESPColor random_color() { - uint32_t rand = random_uint32(); - uint8_t w = rand >> 24; - uint8_t r = rand >> 16; - uint8_t g = rand >> 8; - uint8_t b = rand >> 0; - const uint16_t max_rgb = std::max(r, std::max(g, b)); - return ESPColor(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)), - uint8_t((uint16_t(b) * 255U / max_rgb)), w); - } - ESPColor fade_to_white(uint8_t amnt) { return ESPColor(255, 255, 255, 255) - (*this * amnt); } - ESPColor fade_to_black(uint8_t amnt) { return *this * amnt; } - ESPColor lighten(uint8_t delta) { return *this + delta; } - ESPColor darken(uint8_t delta) { return *this - delta; } +/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). +Color color_from_light_color_values(LightColorValues val); - static const ESPColor BLACK; - static const ESPColor WHITE; -}; - -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) {} - ESPColor to_rgb() const; -}; - -class ESPColorCorrection { - public: - ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} - void set_max_brightness(const ESPColor &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 ESPColor color_correct(ESPColor color) const ALWAYS_INLINE { - // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma - return ESPColor(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 ESPColor color_uncorrect(ESPColor color) const ALWAYS_INLINE { - // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) - return ESPColor(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]; - ESPColor max_brightness_; - uint8_t local_brightness_{255}; -}; - -class ESPColorSettable { - public: - virtual void set(const ESPColor &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) { - ESPColor 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 ESPColor &rhs) { - this->set(rhs); - return *this; - } - ESPColorView &operator=(const ESPHSVColor &rhs) { - this->set_hsv(rhs); - return *this; - } - void set(const ESPColor &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)); } - ESPColor get() const { return ESPColor(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 ESPColor &color) override; - ESPRangeView &operator=(const ESPColor &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_; +/// Use a custom state class for addressable lights, to allow type system to discriminate between addressable and +/// non-addressable lights. +class AddressableLightState : public LightState { + using LightState::LightState; }; class AddressableLight : public LightOutput, public Component { @@ -450,18 +60,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(ESPColor(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); } @@ -470,12 +82,12 @@ 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) { - if (c.get().is_on()) { + for (const auto &c : *this) { + if (c.get_red_raw() > 0 || c.get_green_raw() > 0 || c.get_blue_raw() > 0 || c.get_white_raw() > 0) { this->power_.request(); return; } @@ -486,12 +98,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 e6528fcd8a..5091bae2d5 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/light/light_state.h" #include "esphome/components/light/addressable_light.h" @@ -34,13 +36,10 @@ class AddressableLightEffect : public LightEffect { this->start(); } void stop() override { this->get_addressable_()->set_effect_active(false); } - virtual void apply(AddressableLight &it, const ESPColor ¤t_color) = 0; + 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 - ESPColor current_color = - ESPColor(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); } @@ -51,21 +50,22 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: AddressableLambdaLightEffect(const std::string &name, - const std::function &f, + std::function f, uint32_t update_interval) - : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} + : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } - void apply(AddressableLight &it, const ESPColor ¤t_color) override { + void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); if (now - this->last_run_ >= this->update_interval_) { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; + it.schedule_show(); } } protected: - std::function f_; + std::function f_; uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; @@ -74,7 +74,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { class AddressableRainbowLightEffect : public AddressableLightEffect { public: explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {} - void apply(AddressableLight &it, const ESPColor ¤t_color) override { + void apply(AddressableLight &it, const Color ¤t_color) override { ESPHSVColor hsv; hsv.value = 255; hsv.saturation = 240; @@ -85,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; } @@ -106,7 +107,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { void set_colors(const std::vector &colors) { this->colors_ = colors; } void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } void set_reverse(bool reverse) { this->reverse_ = reverse; } - void apply(AddressableLight &it, const ESPColor ¤t_color) override { + void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); if (now - this->last_add_ < this->add_led_interval_) return; @@ -116,7 +117,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { else it.shift_right(1); const AddressableColorWipeEffectColor color = this->colors_[this->at_color_]; - const ESPColor esp_color = ESPColor(color.r, color.g, color.b, color.w); + const Color esp_color = Color(color.r, color.g, color.b, color.w); if (this->reverse_) it[-1] = esp_color; else @@ -126,12 +127,13 @@ class AddressableColorWipeEffect : public AddressableLightEffect { this->at_color_ = (this->at_color_ + 1) % this->colors_.size(); AddressableColorWipeEffectColor &new_color = this->colors_[this->at_color_]; if (new_color.random) { - ESPColor c = ESPColor::random_color(); + Color c = Color::random_color(); new_color.r = c.r; new_color.g = c.g; new_color.b = c.b; } } + it.schedule_show(); } protected: @@ -148,40 +150,42 @@ class AddressableScanEffect : public AddressableLightEffect { explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} 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 ESPColor ¤t_color) override { - it.all() = ESPColor::BLACK; + void apply(AddressableLight &it, const Color ¤t_color) override { + const uint32_t now = millis(); + if (now - this->last_move_ < this->move_interval_) + return; - for (auto i = 0; i < this->scan_width_; i++) { + 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 (uint32_t 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: uint32_t move_interval_{}; uint32_t scan_width_{1}; uint32_t last_move_{0}; - int at_led_{0}; + uint32_t at_led_{0}; bool direction_{true}; }; class AddressableTwinkleEffect : public AddressableLightEffect { public: explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} - void apply(AddressableLight &addressable, const ESPColor ¤t_color) override { + void apply(AddressableLight &addressable, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; if (now - this->last_progress_ > this->progress_interval_) { @@ -199,7 +203,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { else view.set_effect_data(new_pos); } else { - view = ESPColor::BLACK; + view = Color::BLACK; } } while (random_float() < this->twinkle_probability_) { @@ -208,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; } @@ -221,7 +226,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { class AddressableRandomTwinkleEffect : public AddressableLightEffect { public: explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} - void apply(AddressableLight &it, const ESPColor ¤t_color) override { + void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; if (now - this->last_progress_ > this->progress_interval_) { @@ -237,7 +242,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { if (color == 0) { view = current_color * sine; } else { - view = ESPColor(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine); + view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine); } const uint8_t new_x = x + pos_add; if (new_x > 0b11111) @@ -245,7 +250,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { else view.set_effect_data((new_x << 3) | color); } else { - view = ESPColor(0, 0, 0, 0); + view = Color(0, 0, 0, 0); } } while (random_float() < this->twinkle_probability_) { @@ -255,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; } @@ -270,9 +276,9 @@ class AddressableFireworksEffect : public AddressableLightEffect { explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} void start() override { auto &it = *this->get_addressable_(); - it.all() = ESPColor::BLACK; + it.all() = Color::BLACK; } - void apply(AddressableLight &it, const ESPColor ¤t_color) override { + void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); if (now - this->last_update_ < this->update_interval_) return; @@ -280,7 +286,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { // "invert" the fade out parameter so that higher values make fade out faster const uint8_t fade_out_mult = 255u - this->fade_out_rate_; for (auto view : it) { - ESPColor target = view.get() * fade_out_mult; + Color target = view.get() * fade_out_mult; if (target.r < 64) target *= 170; view = target; @@ -294,11 +300,12 @@ class AddressableFireworksEffect : public AddressableLightEffect { if (random_float() < this->spark_probability_) { const size_t pos = random_uint32() % it.size(); if (this->use_random_color_) { - it[pos] = ESPColor::random_color(); + it[pos] = Color::random_color(); } else { 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; } @@ -316,7 +323,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { class AddressableFlickerEffect : public AddressableLightEffect { public: explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} - void apply(AddressableLight &it, const ESPColor ¤t_color) override { + void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); const uint8_t intensity = this->intensity_; const uint8_t inv_intensity = 255 - intensity; @@ -333,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..d358502430 --- /dev/null +++ b/esphome/components/light/addressable_light_wrapper.h @@ -0,0 +1,127 @@ +#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 { + LightTraits traits; + + // Choose which color mode to use. + // This is ordered by how closely each color mode matches the underlying RGBW data structure used in LightPartition. + ColorMode color_mode_precedence[] = {ColorMode::RGB_WHITE, + ColorMode::RGB_COLD_WARM_WHITE, + ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB, + ColorMode::WHITE, + ColorMode::COLD_WARM_WHITE, + ColorMode::COLOR_TEMPERATURE, + ColorMode::BRIGHTNESS, + ColorMode::ON_OFF, + ColorMode::UNKNOWN}; + + LightTraits parent_traits = this->light_state_->get_traits(); + for (auto cm : color_mode_precedence) { + if (parent_traits.supports_color_mode(cm)) { + this->color_mode_ = cm; + break; + } + } + + // Report a color mode that's compatible with both the partition and the underlying light + switch (this->color_mode_) { + case ColorMode::RGB_WHITE: + case ColorMode::RGB_COLD_WARM_WHITE: + case ColorMode::RGB_COLOR_TEMPERATURE: + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + break; + + case ColorMode::RGB: + traits.set_supported_color_modes({light::ColorMode::RGB}); + break; + + case ColorMode::WHITE: + case ColorMode::COLD_WARM_WHITE: + case ColorMode::COLOR_TEMPERATURE: + case ColorMode::BRIGHTNESS: + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + break; + + case ColorMode::ON_OFF: + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); + break; + + default: + traits.set_supported_color_modes({light::ColorMode::UNKNOWN}); + } + + return traits; + } + + void write_state(light::LightState *state) override { + // Don't overwrite state if the underlying light is turned on + if (this->light_state_->remote_values.is_on()) { + this->mark_shown_(); + return; + } + + 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); + + auto call = this->light_state_->make_call(); + + float color_brightness = fmaxf(r, fmaxf(g, b)); + float brightness = fmaxf(color_brightness, w); + if (brightness == 0.0f) { + call.set_state(false); + } else { + color_brightness /= brightness; + w /= brightness; + + call.set_state(true); + call.set_color_mode_if_supported(this->color_mode_); + call.set_brightness_if_supported(brightness); + call.set_color_brightness_if_supported(color_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_warm_white_if_supported(w); + call.set_cold_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_; + ColorMode color_mode_{ColorMode::UNKNOWN}; +}; + +} // 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 dfab780658..b63fc93dc5 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...)); @@ -102,13 +110,18 @@ class LightTurnOnTrigger : public Trigger<> { public: LightTurnOnTrigger(LightState *a_light) { a_light->add_new_remote_values_callback([this, a_light]() { - auto is_on = a_light->current_values.is_on(); - if (is_on && !last_on_) { + // using the remote value because of transitions we need to trigger as early as possible + auto is_on = a_light->remote_values.is_on(); + // only trigger when going from off to on + auto should_trigger = is_on && !this->last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + this->last_on_ = is_on; + if (should_trigger) { this->trigger(); } - last_on_ = is_on; }); - last_on_ = a_light->current_values.is_on(); + this->last_on_ = a_light->current_values.is_on(); } protected: @@ -118,49 +131,74 @@ class LightTurnOnTrigger : public Trigger<> { class LightTurnOffTrigger : public Trigger<> { public: LightTurnOffTrigger(LightState *a_light) { - a_light->add_new_remote_values_callback([this, a_light]() { + a_light->add_new_target_state_reached_callback([this, a_light]() { auto is_on = a_light->current_values.is_on(); - if (!is_on && last_on_) { + // only trigger when going from on to off + if (!is_on) { this->trigger(); } - last_on_ = is_on; }); - last_on_ = a_light->current_values.is_on(); } - - protected: - bool last_on_; }; +class LightStateTrigger : public Trigger<> { + public: + LightStateTrigger(LightState *a_light) { + a_light->add_new_remote_values_callback([this]() { this->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 9e14246c0f..cfba273565 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,160 +1,253 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TRANSITION_LENGTH, CONF_STATE, CONF_FLASH_LENGTH, \ - CONF_EFFECT, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, \ - CONF_COLOR_TEMPERATURE, CONF_RANGE_FROM, CONF_RANGE_TO -from .types import DimRelativeAction, ToggleAction, LightState, LightControlAction, \ - AddressableLightState, AddressableSet, LightIsOnCondition, LightIsOffCondition +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, + LightControlAction, + AddressableLightState, + AddressableSet, + LightIsOnCondition, + LightIsOffCondition, +) -@automation.register_action('light.toggle', ToggleAction, automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), -})) -def light_toggle_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "light.toggle", + ToggleAction, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), +) +async def light_toggle_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_TRANSITION_LENGTH in config: - template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + template_ = await cg.templatable( + config[CONF_TRANSITION_LENGTH], args, cg.uint32 + ) cg.add(var.set_transition_length(template_)) - yield var + return var -LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Optional(CONF_STATE): cv.templatable(cv.boolean), - cv.Exclusive(CONF_TRANSITION_LENGTH, 'transformer'): - cv.templatable(cv.positive_time_period_milliseconds), - cv.Exclusive(CONF_FLASH_LENGTH, 'transformer'): - cv.templatable(cv.positive_time_period_milliseconds), - cv.Exclusive(CONF_EFFECT, 'transformer'): cv.templatable(cv.string), - cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), - cv.Optional(CONF_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), -}) -LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), - cv.Optional(CONF_STATE, default=False): False, -}) -LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(LIGHT_CONTROL_ACTION_SCHEMA.extend({ - cv.Optional(CONF_STATE, default=True): True, -})) +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 + ), + cv.Exclusive(CONF_FLASH_LENGTH, "transformer"): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Exclusive(CONF_EFFECT, "transformer"): cv.templatable(cv.string), + cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), + cv.Optional(CONF_RED): cv.templatable(cv.percentage), + 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( + { + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Optional(CONF_STATE, default=False): False, + } +) +LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( + LIGHT_CONTROL_ACTION_SCHEMA.extend( + { + cv.Optional(CONF_STATE, default=True): True, + } + ) +) -@automation.register_action('light.turn_off', LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA) -@automation.register_action('light.turn_on', LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA) -@automation.register_action('light.control', LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA) -def light_control_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA +) +@automation.register_action( + "light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA +) +@automation.register_action( + "light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA +) +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_ = yield cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) if CONF_TRANSITION_LENGTH in config: - template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + template_ = await cg.templatable( + config[CONF_TRANSITION_LENGTH], args, cg.uint32 + ) cg.add(var.set_transition_length(template_)) if CONF_FLASH_LENGTH in config: - template_ = yield cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) + template_ = await cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) cg.add(var.set_flash_length(template_)) if CONF_BRIGHTNESS in config: - template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, float) + 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_ = yield cg.templatable(config[CONF_RED], args, float) + template_ = await cg.templatable(config[CONF_RED], args, float) cg.add(var.set_red(template_)) if CONF_GREEN in config: - template_ = yield cg.templatable(config[CONF_GREEN], args, float) + template_ = await cg.templatable(config[CONF_GREEN], args, float) cg.add(var.set_green(template_)) if CONF_BLUE in config: - template_ = yield cg.templatable(config[CONF_BLUE], args, float) + template_ = await cg.templatable(config[CONF_BLUE], args, float) cg.add(var.set_blue(template_)) if CONF_WHITE in config: - template_ = yield cg.templatable(config[CONF_WHITE], args, float) + template_ = await cg.templatable(config[CONF_WHITE], args, float) cg.add(var.set_white(template_)) if CONF_COLOR_TEMPERATURE in config: - template_ = yield cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) + 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_ = yield cg.templatable(config[CONF_EFFECT], args, cg.std_string) + template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) cg.add(var.set_effect(template_)) - yield var + return var -CONF_RELATIVE_BRIGHTNESS = 'relative_brightness' -LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Required(CONF_RELATIVE_BRIGHTNESS): cv.templatable(cv.possibly_negative_percentage), - cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), -}) +CONF_RELATIVE_BRIGHTNESS = "relative_brightness" +LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Required(CONF_RELATIVE_BRIGHTNESS): cv.templatable( + cv.possibly_negative_percentage + ), + cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) -@automation.register_action('light.dim_relative', DimRelativeAction, - LIGHT_DIM_RELATIVE_ACTION_SCHEMA) -def light_dim_relative_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "light.dim_relative", DimRelativeAction, LIGHT_DIM_RELATIVE_ACTION_SCHEMA +) +async def light_dim_relative_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) - templ = yield cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float) + templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float) cg.add(var.set_relative_brightness(templ)) if CONF_TRANSITION_LENGTH in config: - templ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) cg.add(var.set_transition_length(templ)) - yield var + return var -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_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), -}) +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), + cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), + } +) -@automation.register_action('light.addressable_set', AddressableSet, - LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA) -def light_addressable_set_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "light.addressable_set", AddressableSet, LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA +) +async def light_addressable_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) if CONF_RANGE_FROM in config: - templ = yield cg.templatable(config[CONF_RANGE_FROM], args, cg.int32) + templ = await cg.templatable(config[CONF_RANGE_FROM], args, cg.int32) cg.add(var.set_range_from(templ)) if CONF_RANGE_TO in config: - templ = yield cg.templatable(config[CONF_RANGE_TO], args, cg.int32) + 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 = yield 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 = yield 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 = yield 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 = yield 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)) - yield var + return var -@automation.register_condition('light.is_on', LightIsOnCondition, - automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(LightState), - })) -@automation.register_condition('light.is_off', LightIsOffCondition, - automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(LightState), - })) -def light_is_on_off_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren) +@automation.register_condition( + "light.is_on", + LightIsOnCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(LightState), + } + ), +) +@automation.register_condition( + "light.is_off", + LightIsOffCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(LightState), + } + ), +) +async def light_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) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index dcef60397d..5ab9f66ce4 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "light_effect.h" #include "esphome/core/automation.h" @@ -11,6 +13,40 @@ inline static float random_cubic_float() { return r * r * r; } +/// Pulse effect. +class PulseLightEffect : public LightEffect { + public: + explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} + + void apply() override { + const uint32_t now = millis(); + if (now - this->last_color_change_ < this->update_interval_) { + return; + } + auto call = this->state_->turn_on(); + float out = this->on_ ? 1.0 : 0.0; + call.set_brightness_if_supported(out); + this->on_ = !this->on_; + call.set_transition_length_if_supported(this->transition_length_); + // don't tell HA every change + call.set_publish(false); + call.set_save(false); + call.perform(); + + this->last_color_change_ = now; + } + + void set_transition_length(uint32_t transition_length) { this->transition_length_ = transition_length; } + + void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } + + protected: + bool on_ = false; + uint32_t last_color_change_{0}; + uint32_t transition_length_{}; + uint32_t update_interval_{}; +}; + /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. class RandomLightEffect : public LightEffect { public: @@ -21,12 +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(); - 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()); - call.set_color_temperature_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_transition_length_if_supported(this->transition_length_); call.set_publish(true); call.set_save(false); @@ -47,8 +102,8 @@ class RandomLightEffect : public LightEffect { class LambdaLightEffect : public LightEffect { public: - LambdaLightEffect(const std::string &name, const std::function &f, uint32_t update_interval) - : LightEffect(name), f_(f), update_interval_(update_interval) {} + LambdaLightEffect(const std::string &name, std::function f, uint32_t update_interval) + : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} void apply() override { const uint32_t now = millis(); @@ -67,9 +122,9 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: AutomationLightEffect(const std::string &name) : LightEffect(name) {} - void stop() override { this->trig_->stop(); } + void stop() override { this->trig_->stop_action(); } void apply() override { - if (!this->trig_->is_running()) { + if (!this->trig_->is_action_running()) { this->trig_->trigger(); } } @@ -101,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); @@ -137,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 d8c709b8ad..4b2209c833 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -1,38 +1,79 @@ +from esphome.jsonschema import jschema_extractor import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_NAME, CONF_LAMBDA, CONF_UPDATE_INTERVAL, CONF_TRANSITION_LENGTH, \ - CONF_COLORS, CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, \ - CONF_WHITE, CONF_ALPHA, CONF_INTENSITY, CONF_SPEED, CONF_WIDTH, CONF_NUM_LEDS, CONF_RANDOM, \ - CONF_SEQUENCE -from esphome.util import Registry -from .types import LambdaLightEffect, RandomLightEffect, StrobeLightEffect, \ - StrobeLightEffectColor, LightColorValues, AddressableLightRef, AddressableLambdaLightEffect, \ - FlickerLightEffect, AddressableRainbowLightEffect, AddressableColorWipeEffect, \ - AddressableColorWipeEffectColor, AddressableScanEffect, AddressableTwinkleEffect, \ - AddressableRandomTwinkleEffect, AddressableFireworksEffect, AddressableFlickerEffect, \ - AutomationLightEffect, ESPColor -CONF_ADD_LED_INTERVAL = 'add_led_interval' -CONF_REVERSE = 'reverse' -CONF_MOVE_INTERVAL = 'move_interval' -CONF_SCAN_WIDTH = 'scan_width' -CONF_TWINKLE_PROBABILITY = 'twinkle_probability' -CONF_PROGRESS_INTERVAL = 'progress_interval' -CONF_SPARK_PROBABILITY = 'spark_probability' -CONF_USE_RANDOM_COLOR = 'use_random_color' -CONF_FADE_OUT_RATE = 'fade_out_rate' -CONF_STROBE = 'strobe' -CONF_FLICKER = 'flicker' -CONF_ADDRESSABLE_LAMBDA = 'addressable_lambda' -CONF_ADDRESSABLE_RAINBOW = 'addressable_rainbow' -CONF_ADDRESSABLE_COLOR_WIPE = 'addressable_color_wipe' -CONF_ADDRESSABLE_SCAN = 'addressable_scan' -CONF_ADDRESSABLE_TWINKLE = 'addressable_twinkle' -CONF_ADDRESSABLE_RANDOM_TWINKLE = 'addressable_random_twinkle' -CONF_ADDRESSABLE_FIREWORKS = 'addressable_fireworks' -CONF_ADDRESSABLE_FLICKER = 'addressable_flicker' -CONF_AUTOMATION = 'automation' +from esphome.const import ( + CONF_NAME, + CONF_LAMBDA, + CONF_UPDATE_INTERVAL, + CONF_TRANSITION_LENGTH, + CONF_COLORS, + CONF_STATE, + CONF_DURATION, + CONF_BRIGHTNESS, + CONF_COLOR_MODE, + CONF_COLOR_BRIGHTNESS, + CONF_RED, + CONF_GREEN, + CONF_BLUE, + CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, + CONF_ALPHA, + CONF_INTENSITY, + CONF_SPEED, + CONF_WIDTH, + CONF_NUM_LEDS, + CONF_RANDOM, + CONF_SEQUENCE, +) +from esphome.util import Registry +from .types import ( + ColorMode, + COLOR_MODES, + LambdaLightEffect, + PulseLightEffect, + RandomLightEffect, + StrobeLightEffect, + StrobeLightEffectColor, + LightColorValues, + AddressableLightRef, + AddressableLambdaLightEffect, + FlickerLightEffect, + AddressableRainbowLightEffect, + AddressableColorWipeEffect, + AddressableColorWipeEffectColor, + AddressableScanEffect, + AddressableTwinkleEffect, + AddressableRandomTwinkleEffect, + AddressableFireworksEffect, + AddressableFlickerEffect, + AutomationLightEffect, + Color, +) + +CONF_ADD_LED_INTERVAL = "add_led_interval" +CONF_REVERSE = "reverse" +CONF_MOVE_INTERVAL = "move_interval" +CONF_SCAN_WIDTH = "scan_width" +CONF_TWINKLE_PROBABILITY = "twinkle_probability" +CONF_PROGRESS_INTERVAL = "progress_interval" +CONF_SPARK_PROBABILITY = "spark_probability" +CONF_USE_RANDOM_COLOR = "use_random_color" +CONF_FADE_OUT_RATE = "fade_out_rate" +CONF_STROBE = "strobe" +CONF_FLICKER = "flicker" +CONF_ADDRESSABLE_LAMBDA = "addressable_lambda" +CONF_ADDRESSABLE_RAINBOW = "addressable_rainbow" +CONF_ADDRESSABLE_COLOR_WIPE = "addressable_color_wipe" +CONF_ADDRESSABLE_SCAN = "addressable_scan" +CONF_ADDRESSABLE_TWINKLE = "addressable_twinkle" +CONF_ADDRESSABLE_RANDOM_TWINKLE = "addressable_random_twinkle" +CONF_ADDRESSABLE_FIREWORKS = "addressable_fireworks" +CONF_ADDRESSABLE_FLICKER = "addressable_flicker" +CONF_AUTOMATION = "automation" BINARY_EFFECTS = [] MONOCHROMATIC_EFFECTS = [] @@ -43,9 +84,11 @@ EFFECTS_REGISTRY = Registry() def register_effect(name, effect_type, default_name, schema, *extra_validators): - schema = cv.Schema(schema).extend({ - cv.Optional(CONF_NAME, default=default_name): cv.string_strict, - }) + schema = cv.Schema(schema).extend( + { + cv.Optional(CONF_NAME, default=default_name): cv.string_strict, + } + ) validator = cv.All(schema, *extra_validators) return EFFECTS_REGISTRY.register(name, effect_type, validator) @@ -60,7 +103,9 @@ def register_binary_effect(name, effect_type, default_name, schema, *extra_valid return register_effect(name, effect_type, default_name, schema, *extra_validators) -def register_monochromatic_effect(name, effect_type, default_name, schema, *extra_validators): +def register_monochromatic_effect( + name, effect_type, default_name, schema, *extra_validators +): # monochromatic effect can be used for all lights expect binary MONOCHROMATIC_EFFECTS.append(name) RGB_EFFECTS.append(name) @@ -77,221 +122,386 @@ def register_rgb_effect(name, effect_type, default_name, schema, *extra_validato return register_effect(name, effect_type, default_name, schema, *extra_validators) -def register_addressable_effect(name, effect_type, default_name, schema, *extra_validators): +def register_addressable_effect( + name, effect_type, default_name, schema, *extra_validators +): # addressable effect can be used only in addressable ADDRESSABLE_EFFECTS.append(name) return register_effect(name, effect_type, default_name, schema, *extra_validators) -@register_binary_effect('lambda', LambdaLightEffect, "Lambda", { - cv.Required(CONF_LAMBDA): cv.lambda_, - cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.update_interval, -}) -def lambda_effect_to_code(config, effect_id): - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.void) - yield cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, - config[CONF_UPDATE_INTERVAL]) +@register_binary_effect( + "lambda", + LambdaLightEffect, + "Lambda", + { + cv.Required(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_UPDATE_INTERVAL, default="0ms"): cv.update_interval, + }, +) +async def lambda_effect_to_code(config, effect_id): + lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.void) + return cg.new_Pvariable( + effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL] + ) -@register_binary_effect('automation', AutomationLightEffect, "Automation", { - cv.Required(CONF_SEQUENCE): automation.validate_automation(single=True), -}) -def automation_effect_to_code(config, effect_id): - var = yield cg.new_Pvariable(effect_id, config[CONF_NAME]) - yield automation.build_automation(var.get_trig(), [], config[CONF_SEQUENCE]) - yield var +@register_binary_effect( + "automation", + AutomationLightEffect, + "Automation", + { + cv.Required(CONF_SEQUENCE): automation.validate_automation(single=True), + }, +) +async def automation_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + await automation.build_automation(var.get_trig(), [], config[CONF_SEQUENCE]) + return var -@register_rgb_effect('random', RandomLightEffect, "Random", { - cv.Optional(CONF_TRANSITION_LENGTH, default='7.5s'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_UPDATE_INTERVAL, default='10s'): cv.positive_time_period_milliseconds, -}) -def random_effect_to_code(config, effect_id): +@register_monochromatic_effect( + "pulse", + PulseLightEffect, + "Pulse", + { + cv.Optional( + CONF_TRANSITION_LENGTH, default="1s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_UPDATE_INTERVAL, default="1s" + ): cv.positive_time_period_milliseconds, + }, +) +async def pulse_effect_to_code(config, effect_id): effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) - yield effect + return effect -@register_binary_effect('strobe', StrobeLightEffect, "Strobe", { - cv.Optional(CONF_COLORS, default=[ - {CONF_STATE: True, CONF_DURATION: '0.5s'}, - {CONF_STATE: False, CONF_DURATION: '0.5s'}, - ]): cv.All(cv.ensure_list(cv.Schema({ - cv.Optional(CONF_STATE, default=True): cv.boolean, - cv.Optional(CONF_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.Required(CONF_DURATION): cv.positive_time_period_milliseconds, - }), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, - CONF_WHITE)), cv.Length(min=2)), -}) -def strobe_effect_to_code(config, effect_id): +@register_monochromatic_effect( + "random", + RandomLightEffect, + "Random", + { + cv.Optional( + CONF_TRANSITION_LENGTH, default="7.5s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_UPDATE_INTERVAL, default="10s" + ): cv.positive_time_period_milliseconds, + }, +) +async def random_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) + cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) + return effect + + +@register_binary_effect( + "strobe", + StrobeLightEffect, + "Strobe", + { + cv.Optional( + CONF_COLORS, + default=[ + {CONF_STATE: True, CONF_DURATION: "0.5s"}, + {CONF_STATE: False, CONF_DURATION: "0.5s"}, + ], + ): cv.All( + cv.ensure_list( + cv.Schema( + { + 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, + } + ), + 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), + ), + }, +) +async def strobe_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) colors = [] for color in config.get(CONF_COLORS, []): - colors.append(cg.StructInitializer( - StrobeLightEffectColor, - ('color', LightColorValues(color[CONF_STATE], color[CONF_BRIGHTNESS], - color[CONF_RED], color[CONF_GREEN], color[CONF_BLUE], - color[CONF_WHITE])), - ('duration', color[CONF_DURATION]), - )) + colors.append( + cg.StructInitializer( + StrobeLightEffectColor, + ( + "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]), + ) + ) cg.add(var.set_colors(colors)) - yield var + return var -@register_monochromatic_effect('flicker', FlickerLightEffect, "Flicker", { - cv.Optional(CONF_ALPHA, default=0.95): cv.percentage, - cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage, -}) -def flicker_effect_to_code(config, effect_id): +@register_monochromatic_effect( + "flicker", + FlickerLightEffect, + "Flicker", + { + cv.Optional(CONF_ALPHA, default=0.95): cv.percentage, + cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage, + }, +) +async def flicker_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_alpha(config[CONF_ALPHA])) cg.add(var.set_intensity(config[CONF_INTENSITY])) - yield var + return var @register_addressable_effect( - 'addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { + "addressable_lambda", + AddressableLambdaLightEffect, + "Addressable Lambda", + { cv.Required(CONF_LAMBDA): cv.lambda_, - cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, - } + cv.Optional( + CONF_UPDATE_INTERVAL, default="0ms" + ): cv.positive_time_period_milliseconds, + }, ) -def addressable_lambda_effect_to_code(config, effect_id): - args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color'), (bool, 'initial_run')] - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) - var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, - config[CONF_UPDATE_INTERVAL]) - yield var +async def addressable_lambda_effect_to_code(config, effect_id): + args = [ + (AddressableLightRef, "it"), + (Color, "current_color"), + (bool, "initial_run"), + ] + lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) + var = cg.new_Pvariable( + effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL] + ) + return var -@register_addressable_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { - cv.Optional(CONF_SPEED, default=10): cv.uint32_t, - cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, -}) -def addressable_rainbow_effect_to_code(config, effect_id): +@register_addressable_effect( + "addressable_rainbow", + AddressableRainbowLightEffect, + "Rainbow", + { + cv.Optional(CONF_SPEED, default=10): cv.uint32_t, + cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, + }, +) +async def addressable_rainbow_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_speed(config[CONF_SPEED])) cg.add(var.set_width(config[CONF_WIDTH])) - yield var + return var -@register_addressable_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { - cv.Optional(CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]): cv.ensure_list({ - 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_RANDOM, default=False): cv.boolean, - cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)), - }), - cv.Optional(CONF_ADD_LED_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_REVERSE, default=False): cv.boolean, -}) -def addressable_color_wipe_effect_to_code(config, effect_id): +@register_addressable_effect( + "addressable_color_wipe", + AddressableColorWipeEffect, + "Color Wipe", + { + cv.Optional( + CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}] + ): cv.ensure_list( + { + 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_RANDOM, default=False): cv.boolean, + cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)), + } + ), + cv.Optional( + CONF_ADD_LED_INTERVAL, default="0.1s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REVERSE, default=False): cv.boolean, + }, +) +async def addressable_color_wipe_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL])) cg.add(var.set_reverse(config[CONF_REVERSE])) colors = [] for color in config.get(CONF_COLORS, []): - colors.append(cg.StructInitializer( - AddressableColorWipeEffectColor, - ('r', int(round(color[CONF_RED] * 255))), - ('g', int(round(color[CONF_GREEN] * 255))), - ('b', int(round(color[CONF_BLUE] * 255))), - ('w', int(round(color[CONF_WHITE] * 255))), - ('random', color[CONF_RANDOM]), - ('num_leds', color[CONF_NUM_LEDS]), - )) + colors.append( + cg.StructInitializer( + AddressableColorWipeEffectColor, + ("r", int(round(color[CONF_RED] * 255))), + ("g", int(round(color[CONF_GREEN] * 255))), + ("b", int(round(color[CONF_BLUE] * 255))), + ("w", int(round(color[CONF_WHITE] * 255))), + ("random", color[CONF_RANDOM]), + ("num_leds", color[CONF_NUM_LEDS]), + ) + ) cg.add(var.set_colors(colors)) - yield var - - -@register_addressable_effect('addressable_scan', AddressableScanEffect, "Scan", { - cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1), -}) -def addressable_scan_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL])) - cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH])) - yield var - - -@register_addressable_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { - cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, - cv.Optional(CONF_PROGRESS_INTERVAL, default='4ms'): cv.positive_time_period_milliseconds, -}) -def addressable_twinkle_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) - cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) - yield var + return var @register_addressable_effect( - 'addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { - cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, - cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, - } + "addressable_scan", + AddressableScanEffect, + "Scan", + { + cv.Optional( + CONF_MOVE_INTERVAL, default="0.1s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1), + }, ) -def addressable_random_twinkle_effect_to_code(config, effect_id): +async def addressable_scan_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL])) + cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH])) + return var + + +@register_addressable_effect( + "addressable_twinkle", + AddressableTwinkleEffect, + "Twinkle", + { + cv.Optional(CONF_TWINKLE_PROBABILITY, default="5%"): cv.percentage, + cv.Optional( + CONF_PROGRESS_INTERVAL, default="4ms" + ): cv.positive_time_period_milliseconds, + }, +) +async def addressable_twinkle_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) - yield var + return var -@register_addressable_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { - cv.Optional(CONF_UPDATE_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SPARK_PROBABILITY, default='10%'): cv.percentage, - cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean, - cv.Optional(CONF_FADE_OUT_RATE, default=120): cv.uint8_t, -}) -def addressable_fireworks_effect_to_code(config, effect_id): +@register_addressable_effect( + "addressable_random_twinkle", + AddressableRandomTwinkleEffect, + "Random Twinkle", + { + cv.Optional(CONF_TWINKLE_PROBABILITY, default="5%"): cv.percentage, + cv.Optional( + CONF_PROGRESS_INTERVAL, default="32ms" + ): cv.positive_time_period_milliseconds, + }, +) +async def addressable_random_twinkle_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) + cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) + return var + + +@register_addressable_effect( + "addressable_fireworks", + AddressableFireworksEffect, + "Fireworks", + { + cv.Optional( + CONF_UPDATE_INTERVAL, default="32ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SPARK_PROBABILITY, default="10%"): cv.percentage, + cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean, + cv.Optional(CONF_FADE_OUT_RATE, default=120): cv.uint8_t, + }, +) +async def addressable_fireworks_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) cg.add(var.set_spark_probability(config[CONF_SPARK_PROBABILITY])) cg.add(var.set_use_random_color(config[CONF_USE_RANDOM_COLOR])) cg.add(var.set_fade_out_rate(config[CONF_FADE_OUT_RATE])) - yield var + return var @register_addressable_effect( - 'addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { - cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, - } + "addressable_flicker", + AddressableFlickerEffect, + "Addressable Flicker", + { + cv.Optional( + CONF_UPDATE_INTERVAL, default="16ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INTENSITY, default="5%"): cv.percentage, + }, ) -def addressable_flicker_effect_to_code(config, effect_id): +async def addressable_flicker_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) cg.add(var.set_intensity(config[CONF_INTENSITY])) - yield var + return var def validate_effects(allowed_effects): + @jschema_extractor("effects") def validator(value): - value = cv.validate_registry('effect', EFFECTS_REGISTRY)(value) + # pylint: disable=comparison-with-callable + if value == jschema_extractor: + return (allowed_effects, EFFECTS_REGISTRY) + value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value) errors = [] names = set() for i, x in enumerate(value): key = next(it for it in x.keys()) if key not in allowed_effects: errors.append( - cv.Invalid("The effect '{}' is not allowed for this " - "light type".format(key), [i]) + cv.Invalid( + f"The effect '{key}' is not allowed for this light type", + [i], + ) ) continue name = x[key][CONF_NAME] if name in names: errors.append( - cv.Invalid("Found the effect name '{}' twice. All effects must have " - "unique names".format(name), [i]) + cv.Invalid( + f"Found the effect name '{name}' twice. All effects must have unique names", + [i], + ) ) continue names.add(name) 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..3f1b8aef30 --- /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 == 0u) + 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 39a93cbbcd..ffbe378ee3 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -1,85 +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. + * 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. @@ -94,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); @@ -155,77 +121,107 @@ 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; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } + void as_brightness(float *brightness, float gamma = 0) const { + *brightness = gamma_correct(this->state_ * this->brightness_, gamma); + } /// 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) const { - *red = this->state_ * this->brightness_ * this->red_; - *green = this->state_ * this->brightness_ * this->green_; - *blue = this->state_ * this->brightness_ * this->blue_; - } - - /// 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) const { - this->as_rgb(red, green, blue); - *white = this->state_ * this->brightness_ * this->white_; - } - - /// 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, bool constant_brightness = false) const { - this->as_rgb(red, green, blue); - 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; - *cold_white = this->state_ * this->brightness_ * this->white_ * cw_fraction; - *warm_white = this->state_ * this->brightness_ * this->white_ * 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_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + 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; } } + /// 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); + 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, - 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; - *cold_white = this->state_ * this->brightness_ * cw_fraction; - *warm_white = this->state_ * this->brightness_ * 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. @@ -240,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 @@ -263,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 bef2562f55..8da51fe8b3 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -1,8 +1,8 @@ #pragma once +#include + #include "esphome/core/component.h" -#include "light_color_values.h" -#include "light_state.h" namespace esphome { namespace light { @@ -11,7 +11,7 @@ class LightState; class LightEffect { public: - explicit LightEffect(const std::string &name) : name_(name) {} + explicit LightEffect(std::string name) : name_(std::move(name)) {} /// Initialize this LightEffect. Will be called once after creation. virtual void start() {} 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 c6e7df0811..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 *TAG = "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,15 +40,32 @@ 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_) { case LIGHT_RESTORE_DEFAULT_OFF: case LIGHT_RESTORE_DEFAULT_ON: - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); - // Attempt to load from preferences, else fall back to default values from struct + case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: + case LIGHT_RESTORE_INVERTED_DEFAULT_ON: + 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 = this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON; + recovered.state = false; + if (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || + this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) { + recovered.state = true; + } + } else if (this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_OFF || + this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) { + // Inverted restore state + recovered.state = !recovered.state; } break; case LIGHT_ALWAYS_OFF: @@ -120,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 { @@ -134,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_(); @@ -143,601 +114,109 @@ void LightState::loop() { // Apply transformer (if any) if (this->transformer_ != nullptr) { - if (this->transformer_->is_finished()) { - this->remote_values = this->current_values = this->transformer_->get_end_values(); - if (this->transformer_->publish_at_end()) - this->publish_state(); - this->transformer_ = nullptr; - } else { - this->current_values = this->transformer_->get_values(); - this->remote_values = this->transformer_->get_remote_values(); + 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()) { + // 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; + 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(); } + +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_; } +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)); +} +void LightState::add_new_target_state_reached_callback(std::function &&send_callback) { + 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) { +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->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(); - } - - // sets RGB to 100% if only White specified - if (this->white_.has_value()) { - 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); - } - } - // White to 0% if (exclusively) setting any RGB value - else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->white_.has_value()) { - this->white_ = optional(0.0f); - } - } - // if changing Kelvin alone, change to white light - else if (this->color_temperature_.has_value()) { - 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); - } - // if setting Kelvin 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; - bool now_white = *this->red_ == 1.0f && *this->blue_ == 1.0f && *this->green_ == 1.0f; - if (!this->white_.has_value() && was_color && now_white) { - 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; } -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); - *brightness = gamma_correct(*brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness, this->gamma_correct_); } -void LightState::current_values_as_rgb(float *red, float *green, float *blue) { - this->current_values.as_rgb(red, green, blue); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, 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) { - this->current_values.as_rgbw(red, green, blue, white); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *white = gamma_correct(*white, this->gamma_correct_); +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_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, constant_brightness); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + 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(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, - constant_brightness); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); } -void LightState::add_new_remote_values_callback(std::function &&send_callback) { - this->remote_values_callback_.add(std::move(send_callback)); +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) @@ -745,6 +224,64 @@ LightEffect *LightState::get_active_effect_() { 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 f399cc2be4..ae3711234d 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -1,180 +1,39 @@ #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, LIGHT_ALWAYS_OFF, LIGHT_ALWAYS_ON, + LIGHT_RESTORE_INVERTED_DEFAULT_OFF, + LIGHT_RESTORE_INVERTED_DEFAULT_ON, }; /** 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 @@ -198,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; @@ -229,52 +84,65 @@ 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); - /// 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 + /** + * 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); /// 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; - void add_effects(std::vector effects); + /// 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); - void current_values_as_rgb(float *red, float *green, float *blue); + void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false); - void current_values_as_rgbw(float *red, float *green, float *blue, float *white); + 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); + 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; @@ -284,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 @@ -318,11 +188,20 @@ class LightState : public Nameable, public Component { * starting with the beginning of the transition. */ CallbackManager remote_values_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}; + + /** Callback to call when the state of current_values and remote_values are equal + * This should be called once the state of current_values changed and equals the state of remote_values + */ + CallbackManager target_state_reached_callback_{}; + + /// 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 2052b55c8e..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,28 +12,47 @@ 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; } + + 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}; }; diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 222be7802c..35b045d5b4 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -1,42 +1,53 @@ #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_; } + /// The progress of this transition, on a scale of 0 to 1. + float get_progress_() { + uint32_t now = esphome::millis(); + if (now < this->start_time_) + return 0.0f; + if (now >= this->start_time_ + this->length_) + return 1.0f; - const LightColorValues &get_target_values_() const { return this->target_values_; } + return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); + } uint32_t start_time_; uint32_t length_; @@ -44,46 +55,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..a557bd39b1 --- /dev/null +++ b/esphome/components/light/transformers.h @@ -0,0 +1,126 @@ +#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. Use a second + // variable for transition end state, as overwriting target_values breaks LightState logic. + if (this->start_values_.is_on() && !this->target_values_.is_on()) { + this->end_values_ = LightColorValues(this->start_values_); + this->end_values_.set_brightness(0.0f); + } else { + this->end_values_ = LightColorValues(this->target_values_); + } + + // 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->end_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 end_values_{}; + 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; + + this->begun_lightstate_restore_ = false; + + // 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 { + optional result = {}; + + if (this->transformer_ == nullptr && 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_); + this->begun_lightstate_restore_ = true; + } + + if (this->transformer_ != nullptr) { + result = this->transformer_->apply(); + + if (this->transformer_->is_finished()) { + this->transformer_->stop(); + this->transformer_ = nullptr; + } + } + + return result; + } + + // Restore the original values after the flash. + void stop() override { + if (this->transformer_ != nullptr) { + this->transformer_->stop(); + this->transformer_ = nullptr; + } + this->state_.current_values = this->get_start_values(); + this->state_.remote_values = this->get_start_values(); + this->state_.publish_state(); + } + + bool is_finished() override { return this->begun_lightstate_restore_ && LightTransformer::is_finished(); } + + protected: + LightState &state_; + uint32_t transition_length_; + std::unique_ptr transformer_{nullptr}; + bool begun_lightstate_restore_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index d32ef0214c..a453debd94 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -2,47 +2,77 @@ import esphome.codegen as cg from esphome import automation # Base -light_ns = cg.esphome_ns.namespace('light') -LightState = light_ns.class_('LightState', cg.Nameable, cg.Component) -# Fake class for addressable lights -AddressableLightState = light_ns.class_('LightState', LightState) -LightOutput = light_ns.class_('LightOutput') -AddressableLight = light_ns.class_('AddressableLight', cg.Component) -AddressableLightRef = AddressableLight.operator('ref') +light_ns = cg.esphome_ns.namespace("light") +LightState = light_ns.class_("LightState", cg.EntityBase, cg.Component) +AddressableLightState = light_ns.class_("AddressableLightState", LightState) +LightOutput = light_ns.class_("LightOutput") +AddressableLight = light_ns.class_("AddressableLight", LightOutput, cg.Component) +AddressableLightRef = AddressableLight.operator("ref") -ESPColor = light_ns.class_('ESPColor') -LightColorValues = light_ns.class_('LightColorValues') +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) -DimRelativeAction = light_ns.class_('DimRelativeAction', automation.Action) -AddressableSet = light_ns.class_('AddressableSet', automation.Action) -LightIsOnCondition = light_ns.class_('LightIsOnCondition', automation.Condition) -LightIsOffCondition = light_ns.class_('LightIsOffCondition', automation.Condition) +ToggleAction = light_ns.class_("ToggleAction", automation.Action) +LightControlAction = light_ns.class_("LightControlAction", automation.Action) +DimRelativeAction = light_ns.class_("DimRelativeAction", automation.Action) +AddressableSet = light_ns.class_("AddressableSet", automation.Action) +LightIsOnCondition = light_ns.class_("LightIsOnCondition", automation.Condition) +LightIsOffCondition = light_ns.class_("LightIsOffCondition", automation.Condition) # Triggers -LightTurnOnTrigger = light_ns.class_('LightTurnOnTrigger', automation.Trigger.template()) -LightTurnOffTrigger = light_ns.class_('LightTurnOffTrigger', automation.Trigger.template()) +LightTurnOnTrigger = light_ns.class_( + "LightTurnOnTrigger", automation.Trigger.template() +) +LightTurnOffTrigger = light_ns.class_( + "LightTurnOffTrigger", automation.Trigger.template() +) +LightStateTrigger = light_ns.class_("LightStateTrigger", automation.Trigger.template()) # Effects -LightEffect = light_ns.class_('LightEffect') -RandomLightEffect = light_ns.class_('RandomLightEffect', LightEffect) -LambdaLightEffect = light_ns.class_('LambdaLightEffect', LightEffect) -AutomationLightEffect = light_ns.class_('AutomationLightEffect', LightEffect) -StrobeLightEffect = light_ns.class_('StrobeLightEffect', LightEffect) -StrobeLightEffectColor = light_ns.class_('StrobeLightEffectColor', LightEffect) -FlickerLightEffect = light_ns.class_('FlickerLightEffect', LightEffect) -AddressableLightEffect = light_ns.class_('AddressableLightEffect', LightEffect) -AddressableLambdaLightEffect = light_ns.class_('AddressableLambdaLightEffect', - AddressableLightEffect) -AddressableRainbowLightEffect = light_ns.class_('AddressableRainbowLightEffect', - AddressableLightEffect) -AddressableColorWipeEffect = light_ns.class_('AddressableColorWipeEffect', AddressableLightEffect) -AddressableColorWipeEffectColor = light_ns.struct('AddressableColorWipeEffectColor') -AddressableScanEffect = light_ns.class_('AddressableScanEffect', AddressableLightEffect) -AddressableTwinkleEffect = light_ns.class_('AddressableTwinkleEffect', AddressableLightEffect) -AddressableRandomTwinkleEffect = light_ns.class_('AddressableRandomTwinkleEffect', - AddressableLightEffect) -AddressableFireworksEffect = light_ns.class_('AddressableFireworksEffect', AddressableLightEffect) -AddressableFlickerEffect = light_ns.class_('AddressableFlickerEffect', AddressableLightEffect) +LightEffect = light_ns.class_("LightEffect") +PulseLightEffect = light_ns.class_("PulseLightEffect", LightEffect) +RandomLightEffect = light_ns.class_("RandomLightEffect", LightEffect) +LambdaLightEffect = light_ns.class_("LambdaLightEffect", LightEffect) +AutomationLightEffect = light_ns.class_("AutomationLightEffect", LightEffect) +StrobeLightEffect = light_ns.class_("StrobeLightEffect", LightEffect) +StrobeLightEffectColor = light_ns.class_("StrobeLightEffectColor", LightEffect) +FlickerLightEffect = light_ns.class_("FlickerLightEffect", LightEffect) +AddressableLightEffect = light_ns.class_("AddressableLightEffect", LightEffect) +AddressableLambdaLightEffect = light_ns.class_( + "AddressableLambdaLightEffect", AddressableLightEffect +) +AddressableRainbowLightEffect = light_ns.class_( + "AddressableRainbowLightEffect", AddressableLightEffect +) +AddressableColorWipeEffect = light_ns.class_( + "AddressableColorWipeEffect", AddressableLightEffect +) +AddressableColorWipeEffectColor = light_ns.struct("AddressableColorWipeEffectColor") +AddressableScanEffect = light_ns.class_("AddressableScanEffect", AddressableLightEffect) +AddressableTwinkleEffect = light_ns.class_( + "AddressableTwinkleEffect", AddressableLightEffect +) +AddressableRandomTwinkleEffect = light_ns.class_( + "AddressableRandomTwinkleEffect", AddressableLightEffect +) +AddressableFireworksEffect = light_ns.class_( + "AddressableFireworksEffect", AddressableLightEffect +) +AddressableFlickerEffect = light_ns.class_( + "AddressableFlickerEffect", AddressableLightEffect +) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 329c515aee..20a0b0f792 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -4,48 +4,76 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import LambdaAction -from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, \ - CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE +from esphome.const import ( + CONF_ARGS, + CONF_BAUD_RATE, + CONF_DEASSERT_RTS_DTR, + CONF_FORMAT, + CONF_HARDWARE_UART, + CONF_ID, + CONF_LEVEL, + CONF_LOGS, + CONF_ON_MESSAGE, + CONF_TAG, + CONF_TRIGGER_ID, + CONF_TX_BUFFER_SIZE, +) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3 -logger_ns = cg.esphome_ns.namespace('logger') +CODEOWNERS = ["@esphome/core"] +logger_ns = cg.esphome_ns.namespace("logger") LOG_LEVELS = { - 'NONE': cg.global_ns.ESPHOME_LOG_LEVEL_NONE, - 'ERROR': cg.global_ns.ESPHOME_LOG_LEVEL_ERROR, - 'WARN': cg.global_ns.ESPHOME_LOG_LEVEL_WARN, - 'INFO': cg.global_ns.ESPHOME_LOG_LEVEL_INFO, - 'DEBUG': cg.global_ns.ESPHOME_LOG_LEVEL_DEBUG, - 'VERBOSE': cg.global_ns.ESPHOME_LOG_LEVEL_VERBOSE, - 'VERY_VERBOSE': cg.global_ns.ESPHOME_LOG_LEVEL_VERY_VERBOSE, + "NONE": cg.global_ns.ESPHOME_LOG_LEVEL_NONE, + "ERROR": cg.global_ns.ESPHOME_LOG_LEVEL_ERROR, + "WARN": cg.global_ns.ESPHOME_LOG_LEVEL_WARN, + "INFO": cg.global_ns.ESPHOME_LOG_LEVEL_INFO, + "DEBUG": cg.global_ns.ESPHOME_LOG_LEVEL_DEBUG, + "VERBOSE": cg.global_ns.ESPHOME_LOG_LEVEL_VERBOSE, + "VERY_VERBOSE": cg.global_ns.ESPHOME_LOG_LEVEL_VERY_VERBOSE, } LOG_LEVEL_TO_ESP_LOG = { - 'ERROR': cg.global_ns.ESP_LOGE, - 'WARN': cg.global_ns.ESP_LOGW, - 'INFO': cg.global_ns.ESP_LOGI, - 'DEBUG': cg.global_ns.ESP_LOGD, - 'VERBOSE': cg.global_ns.ESP_LOGV, - 'VERY_VERBOSE': cg.global_ns.ESP_LOGVV, + "ERROR": cg.global_ns.ESP_LOGE, + "WARN": cg.global_ns.ESP_LOGW, + "INFO": cg.global_ns.ESP_LOGI, + "DEBUG": cg.global_ns.ESP_LOGD, + "VERBOSE": cg.global_ns.ESP_LOGV, + "VERY_VERBOSE": cg.global_ns.ESP_LOGVV, } -LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'CONFIG', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE'] +LOG_LEVEL_SEVERITY = [ + "NONE", + "ERROR", + "WARN", + "INFO", + "CONFIG", + "DEBUG", + "VERBOSE", + "VERY_VERBOSE", +] -UART_SELECTION_ESP32 = ['UART0', 'UART1', 'UART2'] +ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2] -UART_SELECTION_ESP8266 = ['UART0', 'UART0_SWAP', 'UART1'] +UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"] + +UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"] + +UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"] HARDWARE_UART_TO_UART_SELECTION = { - 'UART0': logger_ns.UART_SELECTION_UART0, - 'UART0_SWAP': logger_ns.UART_SELECTION_UART0_SWAP, - 'UART1': logger_ns.UART_SELECTION_UART1, - 'UART2': logger_ns.UART_SELECTION_UART2, + "UART0": logger_ns.UART_SELECTION_UART0, + "UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP, + "UART1": logger_ns.UART_SELECTION_UART1, + "UART2": logger_ns.UART_SELECTION_UART2, } HARDWARE_UART_TO_SERIAL = { - 'UART0': cg.global_ns.Serial, - 'UART0_SWAP': cg.global_ns.Serial, - 'UART1': cg.global_ns.Serial1, - 'UART2': cg.global_ns.Serial2, + "UART0": cg.global_ns.Serial, + "UART0_SWAP": cg.global_ns.Serial, + "UART1": cg.global_ns.Serial1, + "UART2": cg.global_ns.Serial2, } is_log_level = cv.one_of(*LOG_LEVELS, upper=True) @@ -53,6 +81,8 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: + if get_esp32_variant() in ESP32_REDUCED_VARIANTS: + return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value) return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) @@ -60,45 +90,59 @@ def uart_selection(value): def validate_local_no_higher_than_global(value): - global_level = value.get(CONF_LEVEL, 'DEBUG') + global_level = value.get(CONF_LEVEL, "DEBUG") 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)) + raise EsphomeError( + f"The local log level {level} for {tag} must be less severe than the global log level {global_level}." + ) return value -Logger = logger_ns.class_('Logger', cg.Component) -LoggerMessageTrigger = logger_ns.class_('LoggerMessageTrigger', - automation.Trigger.template(cg.int_, cg.const_char_ptr, - cg.const_char_ptr)) +Logger = logger_ns.class_("Logger", cg.Component) +LoggerMessageTrigger = logger_ns.class_( + "LoggerMessageTrigger", + automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), +) -CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = 'esp8266_store_log_strings_in_flash' -CONFIG_SCHEMA = cv.All(cv.Schema({ - 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_HARDWARE_UART, default='UART0'): uart_selection, - cv.Optional(CONF_LEVEL, default='DEBUG'): is_log_level, - cv.Optional(CONF_LOGS, default={}): cv.Schema({ - cv.string: is_log_level, - }), - cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), - cv.Optional(CONF_LEVEL, default='WARN'): is_log_level, - }), - - cv.SplitDefault(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True): - cv.All(cv.only_on_esp8266, cv.boolean), -}).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global) +CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + 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( + { + cv.string: is_log_level, + } + ), + cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), + cv.Optional(CONF_LEVEL, default="WARN"): is_log_level, + } + ), + cv.SplitDefault( + CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True + ): cv.All(cv.only_on_esp8266, cv.boolean), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_local_no_higher_than_global, +) @coroutine_with_priority(90.0) -def to_code(config): +async def to_code(config): baud_rate = config[CONF_BAUD_RATE] - rhs = Logger.new(baud_rate, - config[CONF_TX_BUFFER_SIZE], - HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]]) + rhs = Logger.new( + baud_rate, + config[CONF_TX_BUFFER_SIZE], + HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]], + ) log = cg.Pvariable(config[CONF_ID], rhs) cg.add(log.pre_setup()) @@ -106,12 +150,12 @@ def to_code(config): cg.add(log.set_log_level(tag, LOG_LEVELS[level])) level = config[CONF_LEVEL] - cg.add_define('USE_LOGGER') + 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') + verbose_severity = LOG_LEVEL_SEVERITY.index("VERBOSE") + very_verbose_severity = LOG_LEVEL_SEVERITY.index("VERY_VERBOSE") is_at_least_verbose = this_severity >= verbose_severity is_at_least_very_verbose = this_severity >= very_verbose_severity has_serial_logging = baud_rate != 0 @@ -121,35 +165,42 @@ def to_code(config): cg.add_build_flag(f"-DDEBUG_ESP_PORT={debug_serial_port}") cg.add_build_flag("-DLWIP_DEBUG") DEBUG_COMPONENTS = { - 'HTTP_CLIENT', - 'HTTP_SERVER', - 'HTTP_UPDATE', - 'OTA', - 'SSL', - 'TLS_MEM', - 'UPDATER', - 'WIFI', + "HTTP_CLIENT", + "HTTP_SERVER", + "HTTP_UPDATE", + "OTA", + "SSL", + "TLS_MEM", + "UPDATER", + "WIFI", # Spams logs too much: # 'MDNS_RESPONDER', } for comp in DEBUG_COMPONENTS: cg.add_build_flag(f"-DDEBUG_ESP_{comp}") if CORE.is_esp32 and is_at_least_verbose: - cg.add_build_flag('-DCORE_DEBUG_LEVEL=5') + cg.add_build_flag("-DCORE_DEBUG_LEVEL=5") if CORE.is_esp32 and is_at_least_very_verbose: - cg.add_build_flag('-DENABLE_I2C_DEBUG_BUFFER') + cg.add_build_flag("-DENABLE_I2C_DEBUG_BUFFER") if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): - cg.add_build_flag('-DUSE_STORE_LOG_STR_IN_FLASH') + cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") # Register at end for safe mode - yield cg.register_component(log, config) + await cg.register_component(log, config) for conf in config.get(CONF_ON_MESSAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], log, - LOG_LEVEL_SEVERITY.index(conf[CONF_LEVEL])) - yield automation.build_automation(trigger, [(cg.int_, 'level'), - (cg.const_char_ptr, 'tag'), - (cg.const_char_ptr, 'message')], conf) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], log, LOG_LEVEL_SEVERITY.index(conf[CONF_LEVEL]) + ) + await automation.build_automation( + trigger, + [ + (cg.int_, "level"), + (cg.const_char_ptr, "tag"), + (cg.const_char_ptr, "message"), + ], + conf, + ) def maybe_simple_message(schema): @@ -163,41 +214,46 @@ 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 "%" - (?: # first option (?:[-+0 #]{0,5}) # optional flags (?:\d+|\*)? # width (?:\.(?:\d+|\*))? # precision (?:h|l|ll|w|I|I32|I64)? # size [cCdiouxXeEfgGaAnpsSZ] # type - ) | # OR - %%) # literal "%%" + ) """ # 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]))) + raise cv.Invalid( + f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" + ) return value -CONF_LOGGER_LOG = 'logger.log' -LOGGER_LOG_ACTION_SCHEMA = cv.All(maybe_simple_message({ - cv.Required(CONF_FORMAT): cv.string, - cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), - cv.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(*LOG_LEVEL_TO_ESP_LOG, upper=True), - cv.Optional(CONF_TAG, default="main"): cv.string, -}), validate_printf) +CONF_LOGGER_LOG = "logger.log" +LOGGER_LOG_ACTION_SCHEMA = cv.All( + maybe_simple_message( + { + cv.Required(CONF_FORMAT): cv.string, + cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), + cv.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of( + *LOG_LEVEL_TO_ESP_LOG, upper=True + ), + cv.Optional(CONF_TAG, default="main"): cv.string, + } + ), + validate_printf, +) @automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA) -def logger_log_action_to_code(config, action_id, template_arg, args): +async def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) - lambda_ = yield cg.process_lambda(Lambda(text), args, return_type=cg.void) - yield cg.new_Pvariable(action_id, template_arg, lambda_) + lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) + return cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bc6951c9b9..11c0733701 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -1,16 +1,22 @@ #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 { -static const char *TAG = "logger"; +static const char *const TAG = "logger"; -static const char *LOG_LEVEL_COLORS[] = { +static const char *const LOG_LEVEL_COLORS[] = { "", // NONE ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING @@ -20,7 +26,7 @@ static const char *LOG_LEVEL_COLORS[] = { ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE }; -static const char *LOG_LEVEL_LETTERS[] = { +static const char *const LOG_LEVEL_LETTERS[] = { "", // NONE "E", // ERROR "W", // WARNING @@ -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,22 +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); +#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) + 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; @@ -124,29 +153,58 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; break; -#ifdef ARDUINO_ARCH_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) case UART_SELECTION_UART2: this->hw_serial_ = &Serial2; break; #endif } +#endif // USE_ARDUINO +#ifdef USE_ESP_IDF + uart_num_ = UART_NUM_0; + switch (uart_) { + case UART_SELECTION_UART0: + uart_num_ = UART_NUM_0; + break; + case UART_SELECTION_UART1: + uart_num_ = UART_NUM_1; + break; +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) + case UART_SELECTION_UART2: + uart_num_ = UART_NUM_2; + break; +#endif + } + 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); @@ -163,13 +221,13 @@ UARTSelection Logger::get_uart() const { return this->uart_; } void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } -float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; -#ifdef ARDUINO_ARCH_ESP32 -const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; +float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } +const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +#ifdef USE_ESP32 +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; #endif -#ifdef ARDUINO_ARCH_ESP8266 -const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; +#ifdef USE_ESP8266 +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; #endif void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); @@ -182,7 +240,7 @@ void Logger::dump_config() { } void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); } -Logger *global_logger = nullptr; +Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace logger } // namespace esphome diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 039ad78c63..8756bc2387 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,9 +2,16 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" +#include + +#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 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) + 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,16 +118,23 @@ 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; +extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) class LoggerMessageTrigger : public Trigger { public: 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..959af68235 --- /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 < (int) 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/max31855.cpp b/esphome/components/max31855/max31855.cpp index 88f9e836f9..2578c4742d 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace max31855 { -static const char* TAG = "max31855"; +static const char *const TAG = "max31855"; void MAX31855Sensor::update() { this->enable(); @@ -41,10 +41,10 @@ void MAX31855Sensor::read_data_() { this->read_array(data, 4); this->disable(); - const uint32_t mem = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3] << 0; + const uint32_t mem = encode_uint32(data[0], data[1], data[2], data[3]); // Verify we got data - if (mem != 0 && mem != 0xFFFFFFFF) { + if (mem != 0xFFFFFFFF) { this->status_clear_error(); } else { ESP_LOGE(TAG, "No data received from MAX31855 (0x%08X). Check wiring!", mem); diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index dce28bd542..c7732dfbe3 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -1,24 +1,46 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import CONF_ID, CONF_REFERENCE_TEMPERATURE, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import ( + CONF_ID, + CONF_REFERENCE_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) -max31855_ns = cg.esphome_ns.namespace('max31855') -MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingComponent, - spi.SPIDevice) +max31855_ns = cg.esphome_ns.namespace("max31855") +MAX31855Sensor = max31855_ns.class_( + "MAX31855Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.GenerateID(): cv.declare_id(MAX31855Sensor), - cv.Optional(CONF_REFERENCE_TEMPERATURE): - sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + 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_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await sensor.register_sensor(var, config) if CONF_REFERENCE_TEMPERATURE in config: - tc_ref = yield sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE]) + tc_ref = await sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE]) cg.add(var.set_reference_sensor(tc_ref)) diff --git a/esphome/components/max31856/__init__.py b/esphome/components/max31856/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp new file mode 100644 index 0000000000..9300916fdc --- /dev/null +++ b/esphome/components/max31856/max31856.cpp @@ -0,0 +1,198 @@ +#include "max31856.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace max31856 { + +static const char *const TAG = "max31856"; + +// Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856 + +void MAX31856Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX31856Sensor '%s'...", this->name_.c_str()); + this->spi_setup(); + + // assert on any fault + ESP_LOGCONFIG(TAG, "Setting up assertion on all faults"); + this->write_register_(MAX31856_MASK_REG, 0x0); + + ESP_LOGCONFIG(TAG, "Setting up open circuit fault detection"); + this->write_register_(MAX31856_CR0_REG, MAX31856_CR0_OCFAULT01); + + this->set_thermocouple_type_(); + this->set_noise_filter_(); + + ESP_LOGCONFIG(TAG, "Completed setting up MAX31856Sensor '%s'...", this->name_.c_str()); +} + +void MAX31856Sensor::dump_config() { + LOG_SENSOR("", "MAX31856", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Mains Filter: %s", + (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + LOG_UPDATE_INTERVAL(this); +} + +void MAX31856Sensor::update() { + ESP_LOGVV(TAG, "update"); + + this->one_shot_temperature_(); + + // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz + auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this); + this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f); +} + +void MAX31856Sensor::read_thermocouple_temperature_() { + if (this->has_fault_()) { + // Faults have been logged, clear it for next loop + this->clear_fault_(); + } else { + int32_t temp24 = this->read_register24_(MAX31856_LTCBH_REG); + if (temp24 & 0x800000) { + temp24 |= 0xFF000000; // fix sign + } + + temp24 >>= 5; // bottom 5 bits are unused + + float temp_c = temp24; + temp_c *= 0.0078125; + + ESP_LOGD(TAG, "Got thermocouple temperature: %.2f°C", temp_c); + this->publish_state(temp_c); + } +} + +void MAX31856Sensor::one_shot_temperature_() { + ESP_LOGVV(TAG, "one_shot_temperature_"); + this->write_register_(MAX31856_CJTO_REG, 0x0); + + uint8_t t = this->read_register_(MAX31856_CR0_REG); + + t &= ~MAX31856_CR0_AUTOCONVERT; // turn off autoconversion mode + t |= MAX31856_CR0_1SHOT; // turn on one shot mode + + this->write_register_(MAX31856_CR0_REG, t); +} + +bool MAX31856Sensor::has_fault_() { + ESP_LOGVV(TAG, "read_fault_"); + uint8_t faults = this->read_register_(MAX31856_SR_REG); + + if (faults == 0) { + ESP_LOGV(TAG, "status_set_warning"); + this->status_clear_warning(); + return false; + } + + ESP_LOGV(TAG, "status_set_warning"); + this->status_set_warning(); + + if ((faults & MAX31856_FAULT_CJRANGE) == MAX31856_FAULT_CJRANGE) { + ESP_LOGW(TAG, "Cold Junction Out-of-Range: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCRANGE) == MAX31856_FAULT_TCRANGE) { + ESP_LOGW(TAG, "Thermocouple Out-of-Range: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_CJHIGH) == MAX31856_FAULT_CJHIGH) { + ESP_LOGW(TAG, "Cold-Junction High Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_CJLOW) == MAX31856_FAULT_CJLOW) { + ESP_LOGW(TAG, "Cold-Junction Low Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCHIGH) == MAX31856_FAULT_TCHIGH) { + ESP_LOGW(TAG, "Thermocouple Temperature High Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCLOW) == MAX31856_FAULT_TCLOW) { + ESP_LOGW(TAG, "Thermocouple Temperature Low Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_OVUV) == MAX31856_FAULT_OVUV) { + ESP_LOGW(TAG, "Overvoltage or Undervoltage Input Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_OPEN) == MAX31856_FAULT_OPEN) { + ESP_LOGW(TAG, "Thermocouple Open-Circuit Fault (possibly not connected): '%s'...", this->name_.c_str()); + } + + return true; +} + +void MAX31856Sensor::clear_fault_() { + ESP_LOGV(TAG, "clear_fault_"); + uint8_t t = this->read_register_(MAX31856_CR0_REG); + + t |= MAX31856_CR0_FAULT; // turn on fault interrupt mode + t |= MAX31856_CR0_FAULTCLR; // enable the fault status clear bit + + this->write_register_(MAX31856_CR0_REG, t); +} + +void MAX31856Sensor::set_thermocouple_type_() { + MAX31856ThermocoupleType type = MAX31856_TCTYPE_K; + ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type); + uint8_t t = this->read_register_(MAX31856_CR1_REG); + t &= 0xF0; // mask off bottom 4 bits + t |= (uint8_t) type & 0x0F; + this->write_register_(MAX31856_CR1_REG, t); +} + +void MAX31856Sensor::set_noise_filter_() { + ESP_LOGCONFIG(TAG, "set_noise_filter_: 0x%02X", filter_); + uint8_t t = this->read_register_(MAX31856_CR0_REG); + if (filter_ == FILTER_50HZ) { + t |= 0x01; + ESP_LOGCONFIG(TAG, "set_noise_filter_: 50 Hz, t==0x%02X", t); + } else { + t &= 0xfe; + ESP_LOGCONFIG(TAG, "set_noise_filter_: 60 Hz, t==0x%02X", t); + } + this->write_register_(MAX31856_CR0_REG, t); +} + +void MAX31856Sensor::write_register_(uint8_t reg, uint8_t value) { + ESP_LOGVV(TAG, "write_register_ raw reg=0x%02X value=0x%02X", reg, value); + reg |= SPI_WRITE_M; + ESP_LOGVV(TAG, "write_register_ masked reg=0x%02X value=0x%02X", reg, value); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + ESP_LOGVV(TAG, "write_byte value=0x%02X", value); + this->write_byte(value); + this->disable(); + ESP_LOGV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); +} + +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); + this->write_byte(reg); + const uint8_t value(this->read_byte()); + ESP_LOGVV(TAG, "read_byte value=0x%02X", value); + this->disable(); + ESP_LOGV(TAG, "read_register_ reg=0x%02X: value=0x%02X", reg, value); + return value; +} + +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); + this->write_byte(reg); + const uint8_t msb(this->read_byte()); + ESP_LOGVV(TAG, "read_byte msb=0x%02X", msb); + const uint8_t mid(this->read_byte()); + ESP_LOGVV(TAG, "read_byte mid=0x%02X", mid); + const uint8_t lsb(this->read_byte()); + ESP_LOGVV(TAG, "read_byte lsb=0x%02X", lsb); + this->disable(); + const uint32_t value((msb << 16) | (mid << 8) | lsb); + ESP_LOGV(TAG, "read_register_24_ reg=0x%02X: value=0x%06X", reg, value); + return value; +} + +float MAX31856Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace max31856 +} // namespace esphome diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h new file mode 100644 index 0000000000..157aad433c --- /dev/null +++ b/esphome/components/max31856/max31856.h @@ -0,0 +1,98 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace max31856 { + +enum MAX31856RegisterMasks { SPI_WRITE_M = 0x80 }; + +enum MAX31856Registers { + MAX31856_CR0_REG = 0x00, ///< Config 0 register + MAX31856_CR0_AUTOCONVERT = 0x80, ///< Config 0 Auto convert flag + MAX31856_CR0_1SHOT = 0x40, ///< Config 0 one shot convert flag + MAX31856_CR0_OCFAULT00 = 0x00, ///< Config 0 open circuit fault 00 flag + MAX31856_CR0_OCFAULT01 = 0x10, ///< Config 0 open circuit fault 01 flag + MAX31856_CR0_OCFAULT10 = 0x20, ///< Config 0 open circuit fault 10 flag + MAX31856_CR0_CJ = 0x08, ///< Config 0 cold junction disable flag + MAX31856_CR0_FAULT = 0x04, ///< Config 0 fault mode flag + MAX31856_CR0_FAULTCLR = 0x02, ///< Config 0 fault clear flag + + MAX31856_CR1_REG = 0x01, ///< Config 1 register + MAX31856_MASK_REG = 0x02, ///< Fault Mask register + MAX31856_CJHF_REG = 0x03, ///< Cold junction High temp fault register + MAX31856_CJLF_REG = 0x04, ///< Cold junction Low temp fault register + MAX31856_LTHFTH_REG = 0x05, ///< Linearized Temperature High Fault Threshold Register, MSB + MAX31856_LTHFTL_REG = 0x06, ///< Linearized Temperature High Fault Threshold Register, LSB + MAX31856_LTLFTH_REG = 0x07, ///< Linearized Temperature Low Fault Threshold Register, MSB + MAX31856_LTLFTL_REG = 0x08, ///< Linearized Temperature Low Fault Threshold Register, LSB + MAX31856_CJTO_REG = 0x09, ///< Cold-Junction Temperature Offset Register + MAX31856_CJTH_REG = 0x0A, ///< Cold-Junction Temperature Register, MSB + MAX31856_CJTL_REG = 0x0B, ///< Cold-Junction Temperature Register, LSB + MAX31856_LTCBH_REG = 0x0C, ///< Linearized TC Temperature, Byte 2 + MAX31856_LTCBM_REG = 0x0D, ///< Linearized TC Temperature, Byte 1 + MAX31856_LTCBL_REG = 0x0E, ///< Linearized TC Temperature, Byte 0 + MAX31856_SR_REG = 0x0F, ///< Fault Status Register + + MAX31856_FAULT_CJRANGE = 0x80, ///< Fault status Cold Junction Out-of-Range flag + MAX31856_FAULT_TCRANGE = 0x40, ///< Fault status Thermocouple Out-of-Range flag + MAX31856_FAULT_CJHIGH = 0x20, ///< Fault status Cold-Junction High Fault flag + MAX31856_FAULT_CJLOW = 0x10, ///< Fault status Cold-Junction Low Fault flag + MAX31856_FAULT_TCHIGH = 0x08, ///< Fault status Thermocouple Temperature High Fault flag + MAX31856_FAULT_TCLOW = 0x04, ///< Fault status Thermocouple Temperature Low Fault flag + MAX31856_FAULT_OVUV = 0x02, ///< Fault status Overvoltage or Undervoltage Input Fault flag + MAX31856_FAULT_OPEN = 0x01, ///< Fault status Thermocouple Open-Circuit Fault flag +}; + +/** + * Multiple types of thermocouples supported by the chip. + * Currently only K type implemented here. + */ +enum MAX31856ThermocoupleType { + MAX31856_TCTYPE_B = 0b0000, // 0x00 + MAX31856_TCTYPE_E = 0b0001, // 0x01 + MAX31856_TCTYPE_J = 0b0010, // 0x02 + MAX31856_TCTYPE_K = 0b0011, // 0x03 + MAX31856_TCTYPE_N = 0b0100, // 0x04 + MAX31856_TCTYPE_R = 0b0101, // 0x05 + MAX31856_TCTYPE_S = 0b0110, // 0x06 + MAX31856_TCTYPE_T = 0b0111, // 0x07 + MAX31856_VMODE_G8 = 0b1000, // 0x08 + MAX31856_VMODE_G32 = 0b1100, // 0x12 +}; + +enum MAX31856ConfigFilter { + FILTER_60HZ = 0, + FILTER_50HZ = 1, +}; + +class MAX31856Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void set_filter(MAX31856ConfigFilter filter) { filter_ = filter; } + void update() override; + + protected: + MAX31856ConfigFilter filter_; + + 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_(); + bool has_fault_(); + void clear_fault_(); + void read_thermocouple_temperature_(); + void set_thermocouple_type_(); + void set_noise_filter_(); +}; + +} // namespace max31856 +} // namespace esphome diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py new file mode 100644 index 0000000000..083d2ac30c --- /dev/null +++ b/esphome/components/max31856/sensor.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import ( + CONF_ID, + CONF_MAINS_FILTER, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +max31856_ns = cg.esphome_ns.namespace("max31856") +MAX31856Sensor = max31856_ns.class_( + "MAX31856Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice +) + +MAX31865ConfigFilter = max31856_ns.enum("MAX31856ConfigFilter") +FILTER = { + "50HZ": MAX31865ConfigFilter.FILTER_50HZ, + "60HZ": MAX31865ConfigFilter.FILTER_60HZ, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MAX31856Sensor), + cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( + FILTER, upper=True, space="" + ), + } + ) + .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) + await sensor.register_sensor(var, config) + cg.add(var.set_filter(config[CONF_MAINS_FILTER])) diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index a3c537a2c2..126915dc15 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -6,7 +6,7 @@ namespace esphome { namespace max31865 { -static const char* TAG = "max31865"; +static const char *const TAG = "max31865"; void MAX31865Sensor::update() { // Check new faults since last measurement @@ -83,7 +83,7 @@ void MAX31865Sensor::dump_config() { LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Reference Resistance: %.2fΩ", reference_resistance_); ESP_LOGCONFIG(TAG, " RTD: %u-wire %.2fΩ", rtd_wires_, rtd_nominal_resistance_); - ESP_LOGCONFIG(TAG, " Filter: %s", + ESP_LOGCONFIG(TAG, " Mains Filter: %s", (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); } @@ -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 @@ -203,16 +203,16 @@ float MAX31865Sensor::calc_temperature_(const float& rtd_ratio) { rtd_resistance *= 100; } float rpoly = rtd_resistance; - float neg_temp = -242.02; - neg_temp += 2.2228 * rpoly; + float neg_temp = -242.02f; + neg_temp += 2.2228f * rpoly; rpoly *= rtd_resistance; // square - neg_temp += 2.5859e-3 * rpoly; + neg_temp += 2.5859e-3f * rpoly; rpoly *= rtd_resistance; // ^3 - neg_temp -= 4.8260e-6 * rpoly; + neg_temp -= 4.8260e-6f * rpoly; rpoly *= rtd_resistance; // ^4 - neg_temp -= 2.8183e-8 * rpoly; + neg_temp -= 2.8183e-8f * rpoly; rpoly *= rtd_resistance; // ^5 - neg_temp += 1.5243e-10 * rpoly; + neg_temp += 1.5243e-10f * rpoly; return neg_temp; } diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index ee2f155f74..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 ff1df9c5c8..33d9c42be3 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -1,33 +1,60 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import CONF_ID, CONF_MAINS_FILTER, CONF_REFERENCE_RESISTANCE, \ - CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import ( + CONF_ID, + CONF_MAINS_FILTER, + CONF_REFERENCE_RESISTANCE, + CONF_RTD_NOMINAL_RESISTANCE, + CONF_RTD_WIRES, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) -max31865_ns = cg.esphome_ns.namespace('max31865') -MAX31865Sensor = max31865_ns.class_('MAX31865Sensor', sensor.Sensor, cg.PollingComponent, - spi.SPIDevice) +max31865_ns = cg.esphome_ns.namespace("max31865") +MAX31865Sensor = max31865_ns.class_( + "MAX31865Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice +) -MAX31865ConfigFilter = max31865_ns.enum('MAX31865ConfigFilter') +MAX31865ConfigFilter = max31865_ns.enum("MAX31865ConfigFilter") FILTER = { - '50HZ': MAX31865ConfigFilter.FILTER_50HZ, - '60HZ': MAX31865ConfigFilter.FILTER_60HZ, + "50HZ": MAX31865ConfigFilter.FILTER_50HZ, + "60HZ": MAX31865ConfigFilter.FILTER_60HZ, } -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2).extend({ - cv.GenerateID(): cv.declare_id(MAX31865Sensor), - cv.Required(CONF_REFERENCE_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=10000)), - cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=1000)), - cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), - cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MAX31865Sensor), + cv.Required(CONF_REFERENCE_RESISTANCE): cv.All( + cv.resistance, cv.Range(min=100, max=10000) + ), + cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All( + cv.resistance, cv.Range(min=100, max=1000) + ), + cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( + FILTER, upper=True, space="" + ), + cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await sensor.register_sensor(var, config) cg.add(var.set_reference_resistance(config[CONF_REFERENCE_RESISTANCE])) cg.add(var.set_nominal_resistance(config[CONF_RTD_NOMINAL_RESISTANCE])) cg.add(var.set_filter(config[CONF_MAINS_FILTER])) diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index 53442b9cb1..1ec1d5ee53 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace max6675 { -static const char *TAG = "max6675"; +static const char *const TAG = "max6675"; void MAX6675Sensor::update() { this->enable(); diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index 59d24a5283..dff8360226 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -1,19 +1,37 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) -max6675_ns = cg.esphome_ns.namespace('max6675') -MAX6675Sensor = max6675_ns.class_('MAX6675Sensor', sensor.Sensor, cg.PollingComponent, - spi.SPIDevice) +max6675_ns = cg.esphome_ns.namespace("max6675") +MAX6675Sensor = max6675_ns.class_( + "MAX6675Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.GenerateID(): cv.declare_id(MAX6675Sensor), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MAX6675Sensor), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index c96454ce8c..391d033f24 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -3,30 +3,42 @@ import esphome.config_validation as cv from esphome.components import display, spi from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS -DEPENDENCIES = ['spi'] +DEPENDENCIES = ["spi"] -max7219_ns = cg.esphome_ns.namespace('max7219') -MAX7219Component = max7219_ns.class_('MAX7219Component', cg.PollingComponent, spi.SPIDevice) -MAX7219ComponentRef = MAX7219Component.operator('ref') +max7219_ns = cg.esphome_ns.namespace("max7219") +MAX7219Component = max7219_ns.class_( + "MAX7219Component", cg.PollingComponent, spi.SPIDevice +) +MAX7219ComponentRef = MAX7219Component.operator("ref") -CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(MAX7219Component), +CONF_REVERSE_ENABLE = "reverse_enable" - cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=255), - cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MAX7219Component), + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=255), + cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), + cv.Optional(CONF_REVERSE_ENABLE, default=False): cv.boolean, + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(spi.spi_device_schema()) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) - yield display.register_display(var, config) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await display.register_display(var, config) cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(MAX7219ComponentRef, 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 99eca6c14f..960ac58071 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -1,11 +1,12 @@ #include "max7219.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace max7219 { -static const char *TAG = "max7219"; +static const char *const TAG = "max7219"; static const uint8_t MAX7219_REGISTER_NOOP = 0x00; static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09; @@ -41,7 +42,7 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b01011111, // '6', ord 0x36 0b01110000, // '7', ord 0x37 0b01111111, // '8', ord 0x38 - 0b01110011, // '9', ord 0x39 + 0b01111011, // '9', ord 0x39 0b01001000, // ':', ord 0x3A 0b01011000, // ';', ord 0x3B MAX7219_UNKNOWN_CHAR, // '<', ord 0x3C @@ -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; @@ -142,9 +143,11 @@ void MAX7219Component::dump_config() { void MAX7219Component::display() { for (uint8_t i = 0; i < 8; i++) { this->enable(); - for (uint8_t j = 0; j < this->num_chips_; j++) { - this->send_byte_(8 - i, this->buffer_[j * 8 + i]); - } + for (uint8_t j = 0; j < this->num_chips_; j++) + if (reverse_) + this->send_byte_(8 - i, buffer_[(num_chips_ - j - 1) * 8 + i]); + else + this->send_byte_(8 - i, buffer_[j * 8 + i]); this->disable(); } } @@ -170,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/max7219/max7219.h b/esphome/components/max7219/max7219.h index 1920268ba4..47b54a4c50 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -34,6 +34,7 @@ class MAX7219Component : public PollingComponent, void set_intensity(uint8_t intensity); void set_num_chips(uint8_t num_chips); + void set_reverse(bool reverse) { this->reverse_ = reverse; }; /// Evaluate the printf-format and print the result at the given position. uint8_t printf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); @@ -60,6 +61,7 @@ class MAX7219Component : public PollingComponent, uint8_t intensity_{15}; /// Intensity of the display from 0 to 15 (most) uint8_t num_chips_{1}; uint8_t *buffer_; + bool reverse_{false}; optional writer_{}; }; diff --git a/esphome/components/max7219digit/__init__.py b/esphome/components/max7219digit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py new file mode 100644 index 0000000000..2753f70eef --- /dev/null +++ b/esphome/components/max7219digit/display.py @@ -0,0 +1,99 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS + +CODEOWNERS = ["@rspaargaren"] +DEPENDENCIES = ["spi"] + +CONF_ROTATE_CHIP = "rotate_chip" +CONF_SCROLL_SPEED = "scroll_speed" +CONF_SCROLL_DWELL = "scroll_dwell" +CONF_SCROLL_DELAY = "scroll_delay" +CONF_SCROLL_ENABLE = "scroll_enable" +CONF_SCROLL_MODE = "scroll_mode" +CONF_REVERSE_ENABLE = "reverse_enable" +CONF_NUM_CHIP_LINES = "num_chip_lines" +CONF_CHIP_LINES_STYLE = "chip_lines_style" + +integration_ns = cg.esphome_ns.namespace("max7219digit") +ChipLinesStyle = integration_ns.enum("ChipLinesStyle") +CHIP_LINES_STYLE = { + "ZIGZAG": ChipLinesStyle.ZIGZAG, + "SNAKE": ChipLinesStyle.SNAKE, +} + +ScrollMode = integration_ns.enum("ScrollMode") +SCROLL_MODES = { + "CONTINUOUS": ScrollMode.CONTINUOUS, + "STOP": ScrollMode.STOP, +} + +CHIP_MODES = { + "0": 0, + "90": 1, + "180": 2, + "270": 3, +} + +max7219_ns = cg.esphome_ns.namespace("max7219digit") +MAX7219Component = max7219_ns.class_( + "MAX7219Component", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) +MAX7219ComponentRef = MAX7219Component.operator("ref") + +CONFIG_SCHEMA = ( + display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MAX7219Component), + cv.Optional(CONF_NUM_CHIPS, default=4): cv.int_range(min=1, max=255), + cv.Optional(CONF_NUM_CHIP_LINES, default=1): cv.int_range(min=1, max=255), + cv.Optional(CONF_CHIP_LINES_STYLE, default="SNAKE"): cv.enum( + CHIP_LINES_STYLE, upper=True + ), + cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), + cv.Optional(CONF_ROTATE_CHIP, default="0"): cv.enum(CHIP_MODES, upper=True), + cv.Optional(CONF_SCROLL_MODE, default="CONTINUOUS"): cv.enum( + SCROLL_MODES, upper=True + ), + cv.Optional(CONF_SCROLL_ENABLE, default=True): cv.boolean, + cv.Optional( + CONF_SCROLL_SPEED, default="250ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_SCROLL_DELAY, default="1000ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_SCROLL_DWELL, default="1000ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REVERSE_ENABLE, default=False): cv.boolean, + } + ) + .extend(cv.polling_component_schema("500ms")) + .extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await display.register_display(var, config) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) + cg.add(var.set_num_chip_lines(config[CONF_NUM_CHIP_LINES])) + cg.add(var.set_chip_lines_style(config[CONF_CHIP_LINES_STYLE])) + cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_chip_orientation(config[CONF_ROTATE_CHIP])) + cg.add(var.set_scroll_speed(config[CONF_SCROLL_SPEED])) + cg.add(var.set_scroll_dwell(config[CONF_SCROLL_DWELL])) + cg.add(var.set_scroll_delay(config[CONF_SCROLL_DELAY])) + cg.add(var.set_scroll(config[CONF_SCROLL_ENABLE])) + cg.add(var.set_scroll_mode(config[CONF_SCROLL_MODE])) + cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp new file mode 100644 index 0000000000..2368c17448 --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -0,0 +1,333 @@ +#include "max7219digit.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" +#include "max7219font.h" + +namespace esphome { +namespace max7219digit { + +static const char *const TAG = "max7219DIGIT"; + +static const uint8_t MAX7219_REGISTER_NOOP = 0x00; +static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09; +static const uint8_t MAX7219_REGISTER_INTENSITY = 0x0A; +static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B; +static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; +static const uint8_t MAX7219_REGISTER_DISPLAY_TEST = 0x0F; +constexpr uint8_t MAX7219_NO_SHUTDOWN = 0x00; +constexpr uint8_t MAX7219_SHUTDOWN = 0x01; +constexpr uint8_t MAX7219_NO_DISPLAY_TEST = 0x00; +constexpr uint8_t MAX7219_DISPLAY_TEST = 0x01; + +float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void MAX7219Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); + this->spi_setup(); + this->stepsleft_ = 0; + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + std::vector vec(1); + this->max_displaybuffer_.push_back(vec); + // Initialize buffer with 0 for display so all non written pixels are blank + this->max_displaybuffer_[chip_line].resize(get_width_internal(), 0); + } + // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway + this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); + // let's use our own ASCII -> led pattern encoding + this->send_to_all_(MAX7219_REGISTER_DECODE_MODE, 0); + // No display test with all the pixels on + this->send_to_all_(MAX7219_REGISTER_DISPLAY_TEST, MAX7219_NO_DISPLAY_TEST); + // SET Intsity of display + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); + // this->send_to_all_(MAX7219_REGISTER_INTENSITY, 1); + this->display(); + // power up + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); +} + +void MAX7219Component::dump_config() { + ESP_LOGCONFIG(TAG, "MAX7219DIGIT:"); + ESP_LOGCONFIG(TAG, " Number of Chips: %u", this->num_chips_); + ESP_LOGCONFIG(TAG, " Number of Chips Lines: %u", this->num_chip_lines_); + ESP_LOGCONFIG(TAG, " Chips Lines Style : %u", this->chip_lines_style_); + ESP_LOGCONFIG(TAG, " Intensity: %u", this->intensity_); + ESP_LOGCONFIG(TAG, " Scroll Mode: %u", this->scroll_mode_); + ESP_LOGCONFIG(TAG, " Scroll Speed: %u", this->scroll_speed_); + ESP_LOGCONFIG(TAG, " Scroll Dwell: %u", this->scroll_dwell_); + ESP_LOGCONFIG(TAG, " Scroll Delay: %u", this->scroll_delay_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); +} + +void MAX7219Component::loop() { + uint32_t now = millis(); + + // check if the buffer has shrunk past the current position since last update + if ((this->max_displaybuffer_[0].size() >= this->old_buffer_size_ + 3) || + (this->max_displaybuffer_[0].size() <= this->old_buffer_size_ - 3)) { + this->stepsleft_ = 0; + this->display(); + this->old_buffer_size_ = this->max_displaybuffer_[0].size(); + } + + // Reset the counter back to 0 when full string has been displayed. + if (this->stepsleft_ > this->max_displaybuffer_[0].size()) + this->stepsleft_ = 0; + + // Return if there is no need to scroll or scroll is off + if (!this->scroll_ || (this->max_displaybuffer_[0].size() <= (size_t) get_width_internal())) { + this->display(); + return; + } + + if ((this->stepsleft_ == 0) && (now - this->last_scroll_ < this->scroll_delay_)) { + this->display(); + return; + } + + // Dwell time at end of string in case of stop at end + if (this->scroll_mode_ == ScrollMode::STOP) { + if (this->stepsleft_ >= this->max_displaybuffer_[0].size() - (size_t) get_width_internal() + 1) { + if (now - this->last_scroll_ >= this->scroll_dwell_) { + this->stepsleft_ = 0; + this->last_scroll_ = now; + this->display(); + } + return; + } + } + + // Actual call to scroll left action + if (now - this->last_scroll_ >= this->scroll_speed_) { + this->last_scroll_ = now; + this->scroll_left(); + this->display(); + } +} + +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 display data + // Send the data to the chip + for (uint8_t chip = 0; chip < this->num_chips_ / this->num_chip_lines_; chip++) { + for (uint8_t chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + for (uint8_t j = 0; j < 8; j++) { + bool reverse = + chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE ? !this->reverse_ : this->reverse_; + if (reverse) { + pixels[j] = + this->max_displaybuffer_[chip_line][(this->num_chips_ / this->num_chip_lines_ - chip - 1) * 8 + j]; + } else { + pixels[j] = this->max_displaybuffer_[chip_line][chip * 8 + j]; + } + } + if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE) + this->orientation_ = orientation_180_(); + this->send64pixels(chip_line * this->num_chips_ / this->num_chip_lines_ + chip, pixels); + if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE) + this->orientation_ = orientation_180_(); + } + } +} + +uint8_t MAX7219Component::orientation_180_() { + switch (this->orientation_) { + case 0: + return 2; + case 1: + return 3; + case 2: + return 0; + case 3: + return 1; + default: + return 0; + } +} + +int MAX7219Component::get_height_internal() { + return 8 * this->num_chip_lines_; // TO BE DONE -> CREATE Virtual size of screen and scroll +} + +int MAX7219Component::get_width_internal() { return this->num_chips_ / this->num_chip_lines_ * 8; } + +void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x + 1 > (int) this->max_displaybuffer_[0].size()) { // Extend the display buffer in case required + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + this->max_displaybuffer_[chip_line].resize(x + 1, this->bckgrnd_); + } + } + + if ((y >= this->get_height_internal()) || (y < 0) || (x < 0)) // If pixel is outside display then dont draw + return; + + uint16_t pos = x; // X is starting at 0 top left + uint8_t subpos = y; // Y is starting at 0 top left + + if (color.is_on()) { + this->max_displaybuffer_[subpos / 8][pos] |= (1 << subpos % 8); + } else { + this->max_displaybuffer_[subpos / 8][pos] &= ~(1 << subpos % 8); + } +} + +void MAX7219Component::send_byte_(uint8_t a_register, uint8_t data) { + this->write_byte(a_register); // Write register value to MAX + this->write_byte(data); // Followed by actual data +} +void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { + this->enable(); // Enable SPI + for (uint8_t i = 0; i < this->num_chips_; i++) // Run the loop for every MAX chip in the stack + this->send_byte_(a_register, data); // Send the data to the chips + this->disable(); // Disable SPI +} +void MAX7219Component::update() { + this->update_ = true; + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + this->max_displaybuffer_[chip_line].clear(); + this->max_displaybuffer_[chip_line].resize(get_width_internal(), this->bckgrnd_); + } + if (this->writer_local_.has_value()) // insert Labda function if available + (*this->writer_local_)(*this); +} + +void MAX7219Component::invert_on_off(bool on_off) { this->invert_ = on_off; }; +void MAX7219Component::invert_on_off() { this->invert_ = !this->invert_; }; + +void MAX7219Component::turn_on_off(bool on_off) { + if (on_off) { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); + } else { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 0); + } +} + +void MAX7219Component::scroll(bool on_off, ScrollMode mode, uint16_t speed, uint16_t delay, uint16_t dwell) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); + this->set_scroll_speed(speed); + this->set_scroll_dwell(dwell); + this->set_scroll_delay(delay); +} + +void MAX7219Component::scroll(bool on_off, ScrollMode mode) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); +} + +void MAX7219Component::intensity(uint8_t intensity) { + this->intensity_ = intensity; + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); +} + +void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } + +void MAX7219Component::scroll_left() { + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + if (this->update_) { + this->max_displaybuffer_[chip_line].push_back(this->bckgrnd_); + for (uint16_t i = 0; i < this->stepsleft_; i++) { + this->max_displaybuffer_[chip_line].push_back(this->max_displaybuffer_[chip_line].front()); + this->max_displaybuffer_[chip_line].erase(this->max_displaybuffer_[chip_line].begin()); + } + } else { + this->max_displaybuffer_[chip_line].push_back(this->max_displaybuffer_[chip_line].front()); + this->max_displaybuffer_[chip_line].erase(this->max_displaybuffer_[chip_line].begin()); + } + } + this->update_ = false; + this->stepsleft_++; +} + +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_[0][chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); +} // end of send_char + +// send one character (data) to position (chip) + +void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { + for (uint8_t col = 0; col < 8; col++) { // RUN THIS LOOP 8 times until column is 7 + this->enable(); // start sending by enabling SPI + for (uint8_t i = 0; i < chip; i++) // send extra NOPs to push the pixels out to extra displays + this->send_byte_(MAX7219_REGISTER_NOOP, + MAX7219_REGISTER_NOOP); // run this loop unit the matching chip is reached + uint8_t b = 0; // rotate pixels 90 degrees -- set byte to 0 + if (this->orientation_ == 0) { + for (uint8_t i = 0; i < 8; i++) { + // run this loop 8 times for all the pixels[8] received + b |= ((pixels[i] >> col) & 1) << (7 - i); // change the column bits into row bits + } + } else if (this->orientation_ == 1) { + b = pixels[col]; + } else if (this->orientation_ == 2) { + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[i] >> (7 - col)) & 1) << i; + } + } else { + b = pixels[7 - col]; + } + // send this byte to display at selected chip + if (this->invert_) { + this->send_byte_(col + 1, ~b); + } else { + this->send_byte_(col + 1, b); + } + for (int i = 0; i < this->num_chips_ - chip - 1; i++) // end with enough NOPs so later chips don't update + this->send_byte_(MAX7219_REGISTER_NOOP, MAX7219_REGISTER_NOOP); + this->disable(); // all done disable SPI + } // end of for each column +} // end of send64pixels + +uint8_t MAX7219Component::printdigit(const char *str) { return this->printdigit(0, str); } + +uint8_t MAX7219Component::printdigit(uint8_t start_pos, const char *s) { + uint8_t chip = start_pos; + for (; chip < this->num_chips_ && *s; chip++) + send_char(chip, *s++); + // space out rest + while (chip < (this->num_chips_)) + send_char(chip++, ' '); + return 0; +} // end of sendString + +uint8_t MAX7219Component::printdigitf(uint8_t pos, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::printdigitf(const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(buffer); + return 0; +} + +#ifdef USE_TIME +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { + return this->strftimedigit(0, format, time); +} +#endif + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h new file mode 100644 index 0000000000..3bf934632f --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace max7219digit { + +enum ChipLinesStyle { + ZIGZAG = 0, + SNAKE, +}; + +enum ScrollMode { + CONTINUOUS = 0, + STOP, +}; + +class MAX7219Component; + +using max7219_writer_t = std::function; + +class MAX7219Component : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(max7219_writer_t &&writer) { this->writer_local_ = writer; }; + + void setup() override; + + void loop() override; + + void dump_config() override; + + void update() override; + + float get_setup_priority() const override; + + void display(); + + void invert_on_off(bool on_off); + void invert_on_off(); + + void turn_on_off(bool on_off); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + int get_height_internal() override; + int get_width_internal() override; + + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }; + void set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }; + void set_num_chip_lines(uint8_t num_chip_lines) { this->num_chip_lines_ = num_chip_lines; }; + void set_chip_lines_style(ChipLinesStyle chip_lines_style) { this->chip_lines_style_ = chip_lines_style; }; + void set_chip_orientation(uint8_t rotate) { this->orientation_ = rotate; }; + void set_scroll_speed(uint16_t speed) { this->scroll_speed_ = speed; }; + void set_scroll_dwell(uint16_t dwell) { this->scroll_dwell_ = dwell; }; + void set_scroll_delay(uint16_t delay) { this->scroll_delay_ = delay; }; + void set_scroll(bool on_off) { this->scroll_ = on_off; }; + void set_scroll_mode(ScrollMode mode) { this->scroll_mode_ = mode; }; + void set_reverse(bool on_off) { this->reverse_ = on_off; }; + + 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, ScrollMode mode, uint16_t speed, uint16_t delay, uint16_t dwell); + void scroll(bool on_off, ScrollMode mode); + void scroll(bool on_off); + void intensity(uint8_t intensity); + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printdigitf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printdigitf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t printdigit(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t printdigit(const char *str); + +#ifdef USE_TIME + /// Evaluate the strftime-format and print the result at the given position. + uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + + /// Evaluate the strftime-format and print the result at position 0. + uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); +#endif + + protected: + void send_byte_(uint8_t a_register, uint8_t data); + void send_to_all_(uint8_t a_register, uint8_t data); + uint8_t orientation_180_(); + + uint8_t intensity_; /// Intensity of the display from 0 to 15 (most) + uint8_t num_chips_; + uint8_t num_chip_lines_; + ChipLinesStyle chip_lines_style_; + bool scroll_; + bool reverse_; + bool update_{false}; + uint16_t scroll_speed_; + uint16_t scroll_delay_; + uint16_t scroll_dwell_; + uint16_t old_buffer_size_ = 0; + ScrollMode scroll_mode_; + bool invert_ = false; + uint8_t orientation_; + uint8_t bckgrnd_ = 0x0; + std::vector> max_displaybuffer_; + uint32_t last_scroll_ = 0; + uint16_t stepsleft_; + size_t get_buffer_length_(); + optional writer_local_{}; +}; + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h new file mode 100644 index 0000000000..22d64d1ecd --- /dev/null +++ b/esphome/components/max7219digit/max7219font.h @@ -0,0 +1,270 @@ +#pragma once + +#include "esphome/core/hal.h" + +namespace esphome { +namespace max7219digit { + +// bit patterns for the CP437 font + +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 + {0x0E, 0x1F, 0x3F, 0x7E, 0x3F, 0x1F, 0x0E, 0x00}, // 0x03 + {0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x1C, 0x08, 0x00}, // 0x04 + {0x18, 0xBA, 0xFF, 0xFF, 0xFF, 0xBA, 0x18, 0x00}, // 0x05 + {0x10, 0xB8, 0xFC, 0xFF, 0xFC, 0xB8, 0x10, 0x00}, // 0x06 + {0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00}, // 0x07 + {0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF}, // 0x08 + {0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00}, // 0x09 + {0xFF, 0xC3, 0x99, 0xBD, 0xBD, 0x99, 0xC3, 0xFF}, // 0x0A + {0x70, 0xF8, 0x88, 0x88, 0xFD, 0x7F, 0x07, 0x0F}, // 0x0B + {0x00, 0x4E, 0x5F, 0xF1, 0xF1, 0x5F, 0x4E, 0x00}, // 0x0C + {0xC0, 0xE0, 0xFF, 0x7F, 0x05, 0x05, 0x07, 0x07}, // 0x0D + {0xC0, 0xFF, 0x7F, 0x05, 0x05, 0x65, 0x7F, 0x3F}, // 0x0E + {0x99, 0x5A, 0x3C, 0xE7, 0xE7, 0x3C, 0x5A, 0x99}, // 0x0F + {0x7F, 0x3E, 0x3E, 0x1C, 0x1C, 0x08, 0x08, 0x00}, // 0x10 + {0x08, 0x08, 0x1C, 0x1C, 0x3E, 0x3E, 0x7F, 0x00}, // 0x11 + {0x00, 0x24, 0x66, 0xFF, 0xFF, 0x66, 0x24, 0x00}, // 0x12 + {0x00, 0x5F, 0x5F, 0x00, 0x00, 0x5F, 0x5F, 0x00}, // 0x13 + {0x06, 0x0F, 0x09, 0x7F, 0x7F, 0x01, 0x7F, 0x7F}, // 0x14 + {0x40, 0xDA, 0xBF, 0xA5, 0xFD, 0x59, 0x03, 0x02}, // 0x15 + {0x00, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x00}, // 0x16 + {0x80, 0x94, 0xB6, 0xFF, 0xFF, 0xB6, 0x94, 0x80}, // 0x17 + {0x00, 0x04, 0x06, 0x7F, 0x7F, 0x06, 0x04, 0x00}, // 0x18 + {0x00, 0x10, 0x30, 0x7F, 0x7F, 0x30, 0x10, 0x00}, // 0x19 + {0x08, 0x08, 0x08, 0x2A, 0x3E, 0x1C, 0x08, 0x00}, // 0x1A + {0x08, 0x1C, 0x3E, 0x2A, 0x08, 0x08, 0x08, 0x00}, // 0x1B + {0x3C, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00}, // 0x1C + {0x08, 0x1C, 0x3E, 0x08, 0x08, 0x3E, 0x1C, 0x08}, // 0x1D + {0x30, 0x38, 0x3C, 0x3E, 0x3E, 0x3C, 0x38, 0x30}, // 0x1E + {0x06, 0x0E, 0x1E, 0x3E, 0x3E, 0x1E, 0x0E, 0x06}, // 0x1F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' + {0x00, 0x06, 0x5F, 0x5F, 0x06, 0x00, 0x00, 0x00}, // '!' + {0x00, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x00}, // '"' + {0x14, 0x7F, 0x7F, 0x14, 0x7F, 0x7F, 0x14, 0x00}, // '#' + {0x24, 0x2E, 0x6B, 0x6B, 0x3A, 0x12, 0x00, 0x00}, // '$' + {0x46, 0x66, 0x30, 0x18, 0x0C, 0x66, 0x62, 0x00}, // '%' + {0x30, 0x7A, 0x4F, 0x5D, 0x37, 0x7A, 0x48, 0x00}, // '&' + {0x04, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // ''' + {0x00, 0x1C, 0x3E, 0x63, 0x41, 0x00, 0x00, 0x00}, // '(' + {0x00, 0x41, 0x63, 0x3E, 0x1C, 0x00, 0x00, 0x00}, // ')' + {0x08, 0x2A, 0x3E, 0x1C, 0x1C, 0x3E, 0x2A, 0x08}, // '*' + {0x08, 0x08, 0x3E, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '+' + {0x00, 0x80, 0xE0, 0x60, 0x00, 0x00, 0x00, 0x00}, // ',' + {0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // '-' + {0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00}, // '.' + {0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // '/' + {0x3E, 0x7F, 0x71, 0x59, 0x4D, 0x7F, 0x3E, 0x00}, // '0' + {0x40, 0x42, 0x7F, 0x7F, 0x40, 0x40, 0x00, 0x00}, // '1' + {0x62, 0x73, 0x59, 0x49, 0x6F, 0x66, 0x00, 0x00}, // '2' + {0x22, 0x63, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '3' + {0x18, 0x1C, 0x16, 0x53, 0x7F, 0x7F, 0x50, 0x00}, // '4' + {0x27, 0x67, 0x45, 0x45, 0x7D, 0x39, 0x00, 0x00}, // '5' + {0x3C, 0x7E, 0x4B, 0x49, 0x79, 0x30, 0x00, 0x00}, // '6' + {0x03, 0x03, 0x71, 0x79, 0x0F, 0x07, 0x00, 0x00}, // '7' + {0x36, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '8' + {0x06, 0x4F, 0x49, 0x69, 0x3F, 0x1E, 0x00, 0x00}, // '9' + {0x00, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00}, // ':' + {0x00, 0x80, 0xE6, 0x66, 0x00, 0x00, 0x00, 0x00}, // ';' + {0x08, 0x1C, 0x36, 0x63, 0x41, 0x00, 0x00, 0x00}, // '<' + {0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x00, 0x00}, // '=' + {0x00, 0x41, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00}, // '>' + {0x02, 0x03, 0x51, 0x59, 0x0F, 0x06, 0x00, 0x00}, // '?' + {0x3E, 0x7F, 0x41, 0x5D, 0x5D, 0x1F, 0x1E, 0x00}, // '@' + {0x7C, 0x7E, 0x13, 0x13, 0x7E, 0x7C, 0x00, 0x00}, // 'A' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 'B' + {0x1C, 0x3E, 0x63, 0x41, 0x41, 0x63, 0x22, 0x00}, // 'C' + {0x41, 0x7F, 0x7F, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'D' + {0x41, 0x7F, 0x7F, 0x49, 0x5D, 0x41, 0x63, 0x00}, // 'E' + {0x41, 0x7F, 0x7F, 0x49, 0x1D, 0x01, 0x03, 0x00}, // 'F' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x7F, 0x7F, 0x08, 0x08, 0x7F, 0x7F, 0x00, 0x00}, // 'H' + {0x00, 0x41, 0x7F, 0x7F, 0x41, 0x00, 0x00, 0x00}, // 'I' + {0x30, 0x70, 0x40, 0x41, 0x7F, 0x3F, 0x01, 0x00}, // 'J' + {0x41, 0x7F, 0x7F, 0x08, 0x1C, 0x77, 0x63, 0x00}, // 'K' + {0x41, 0x7F, 0x7F, 0x41, 0x40, 0x60, 0x70, 0x00}, // 'L' + {0x7F, 0x7F, 0x0E, 0x1C, 0x0E, 0x7F, 0x7F, 0x00}, // 'M' + {0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00}, // 'N' + {0x1C, 0x3E, 0x63, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'O' + {0x41, 0x7F, 0x7F, 0x49, 0x09, 0x0F, 0x06, 0x00}, // 'P' + {0x1E, 0x3F, 0x21, 0x71, 0x7F, 0x5E, 0x00, 0x00}, // 'Q' + {0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x26, 0x6F, 0x4D, 0x59, 0x73, 0x32, 0x00, 0x00}, // 'S' + {0x03, 0x41, 0x7F, 0x7F, 0x41, 0x03, 0x00, 0x00}, // 'T' + {0x7F, 0x7F, 0x40, 0x40, 0x7F, 0x7F, 0x00, 0x00}, // 'U' + {0x1F, 0x3F, 0x60, 0x60, 0x3F, 0x1F, 0x00, 0x00}, // 'V' + {0x7F, 0x7F, 0x30, 0x18, 0x30, 0x7F, 0x7F, 0x00}, // 'W' + {0x43, 0x67, 0x3C, 0x18, 0x3C, 0x67, 0x43, 0x00}, // 'X' + {0x07, 0x4F, 0x78, 0x78, 0x4F, 0x07, 0x00, 0x00}, // 'Y' + {0x47, 0x63, 0x71, 0x59, 0x4D, 0x67, 0x73, 0x00}, // 'Z' + {0x00, 0x7F, 0x7F, 0x41, 0x41, 0x00, 0x00, 0x00}, // '[' + {0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00}, // backslash + {0x00, 0x41, 0x41, 0x7F, 0x7F, 0x00, 0x00, 0x00}, // ']' + {0x08, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x08, 0x00}, // '^' + {0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}, // '_' + {0x00, 0x00, 0x03, 0x07, 0x04, 0x00, 0x00, 0x00}, // '`' + {0x20, 0x74, 0x54, 0x54, 0x3C, 0x78, 0x40, 0x00}, // 'a' + {0x41, 0x7F, 0x3F, 0x48, 0x48, 0x78, 0x30, 0x00}, // 'b' + {0x38, 0x7C, 0x44, 0x44, 0x6C, 0x28, 0x00, 0x00}, // 'c' + {0x30, 0x78, 0x48, 0x49, 0x3F, 0x7F, 0x40, 0x00}, // 'd' + {0x38, 0x7C, 0x54, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 'e' + {0x48, 0x7E, 0x7F, 0x49, 0x03, 0x02, 0x00, 0x00}, // 'f' + {0x98, 0xBC, 0xA4, 0xA4, 0xF8, 0x7C, 0x04, 0x00}, // 'g' + {0x41, 0x7F, 0x7F, 0x08, 0x04, 0x7C, 0x78, 0x00}, // 'h' + {0x00, 0x44, 0x7D, 0x7D, 0x40, 0x00, 0x00, 0x00}, // 'i' + {0x60, 0xE0, 0x80, 0x80, 0xFD, 0x7D, 0x00, 0x00}, // 'j' + {0x41, 0x7F, 0x7F, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'k' + {0x00, 0x41, 0x7F, 0x7F, 0x40, 0x00, 0x00, 0x00}, // 'l' + {0x7C, 0x7C, 0x18, 0x38, 0x1C, 0x7C, 0x78, 0x00}, // 'm' + {0x7C, 0x7C, 0x04, 0x04, 0x7C, 0x78, 0x00, 0x00}, // 'n' + {0x38, 0x7C, 0x44, 0x44, 0x7C, 0x38, 0x00, 0x00}, // 'o' + {0x84, 0xFC, 0xF8, 0xA4, 0x24, 0x3C, 0x18, 0x00}, // 'p' + {0x18, 0x3C, 0x24, 0xA4, 0xF8, 0xFC, 0x84, 0x00}, // 'q' + {0x44, 0x7C, 0x78, 0x4C, 0x04, 0x1C, 0x18, 0x00}, // 'r' + {0x48, 0x5C, 0x54, 0x54, 0x74, 0x24, 0x00, 0x00}, // 's' + {0x00, 0x04, 0x3E, 0x7F, 0x44, 0x24, 0x00, 0x00}, // 't' + {0x3C, 0x7C, 0x40, 0x40, 0x3C, 0x7C, 0x40, 0x00}, // 'u' + {0x1C, 0x3C, 0x60, 0x60, 0x3C, 0x1C, 0x00, 0x00}, // 'v' + {0x3C, 0x7C, 0x70, 0x38, 0x70, 0x7C, 0x3C, 0x00}, // 'w' + {0x44, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'x' + {0x9C, 0xBC, 0xA0, 0xA0, 0xFC, 0x7C, 0x00, 0x00}, // 'y' + {0x4C, 0x64, 0x74, 0x5C, 0x4C, 0x64, 0x00, 0x00}, // 'z' + {0x08, 0x08, 0x3E, 0x77, 0x41, 0x41, 0x00, 0x00}, // '{' + {0x00, 0x00, 0x00, 0x77, 0x77, 0x00, 0x00, 0x00}, // '|' + {0x41, 0x41, 0x77, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '}' + {0x02, 0x03, 0x01, 0x03, 0x02, 0x03, 0x01, 0x00}, // '~' + {0x70, 0x78, 0x4C, 0x46, 0x4C, 0x78, 0x70, 0x00}, // 0x7F + {0x0E, 0x9F, 0x91, 0xB1, 0xFB, 0x4A, 0x00, 0x00}, // 0x80 + {0x3A, 0x7A, 0x40, 0x40, 0x7A, 0x7A, 0x40, 0x00}, // 0x81 + {0x38, 0x7C, 0x54, 0x55, 0x5D, 0x19, 0x00, 0x00}, // 0x82 + {0x02, 0x23, 0x75, 0x55, 0x55, 0x7D, 0x7B, 0x42}, // 0x83 + {0x21, 0x75, 0x54, 0x54, 0x7D, 0x79, 0x40, 0x00}, // 0x84 + {0x21, 0x75, 0x55, 0x54, 0x7C, 0x78, 0x40, 0x00}, // 0x85 + {0x20, 0x74, 0x57, 0x57, 0x7C, 0x78, 0x40, 0x00}, // 0x86 + {0x18, 0x3C, 0xA4, 0xA4, 0xE4, 0x40, 0x00, 0x00}, // 0x87 + {0x02, 0x3B, 0x7D, 0x55, 0x55, 0x5D, 0x1B, 0x02}, // 0x88 + {0x39, 0x7D, 0x54, 0x54, 0x5D, 0x19, 0x00, 0x00}, // 0x89 + {0x39, 0x7D, 0x55, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 0x8A + {0x01, 0x45, 0x7C, 0x7C, 0x41, 0x01, 0x00, 0x00}, // 0x8B + {0x02, 0x03, 0x45, 0x7D, 0x7D, 0x43, 0x02, 0x00}, // 0x8C + {0x01, 0x45, 0x7D, 0x7C, 0x40, 0x00, 0x00, 0x00}, // 0x8D + {0x79, 0x7D, 0x16, 0x12, 0x16, 0x7D, 0x79, 0x00}, // 0x8E + {0x70, 0x78, 0x2B, 0x2B, 0x78, 0x70, 0x00, 0x00}, // 0x8F + {0x44, 0x7C, 0x7C, 0x55, 0x55, 0x45, 0x00, 0x00}, // 0x90 + {0x20, 0x74, 0x54, 0x54, 0x7C, 0x7C, 0x54, 0x54}, // 0x91 + {0x7C, 0x7E, 0x0B, 0x09, 0x7F, 0x7F, 0x49, 0x00}, // 0x92 + {0x32, 0x7B, 0x49, 0x49, 0x7B, 0x32, 0x00, 0x00}, // 0x93 + {0x32, 0x7A, 0x48, 0x48, 0x7A, 0x32, 0x00, 0x00}, // 0x94 + {0x32, 0x7A, 0x4A, 0x48, 0x78, 0x30, 0x00, 0x00}, // 0x95 + {0x3A, 0x7B, 0x41, 0x41, 0x7B, 0x7A, 0x40, 0x00}, // 0x96 + {0x3A, 0x7A, 0x42, 0x40, 0x78, 0x78, 0x40, 0x00}, // 0x97 + {0x9A, 0xBA, 0xA0, 0xA0, 0xFA, 0x7A, 0x00, 0x00}, // 0x98 + {0x01, 0x19, 0x3C, 0x66, 0x66, 0x3C, 0x19, 0x01}, // 0x99 + {0x3D, 0x7D, 0x40, 0x40, 0x7D, 0x3D, 0x00, 0x00}, // 0x9A + {0x18, 0x3C, 0x24, 0xE7, 0xE7, 0x24, 0x24, 0x00}, // 0x9B + {0x68, 0x7E, 0x7F, 0x49, 0x43, 0x66, 0x20, 0x00}, // 0x9C + {0x2B, 0x2F, 0xFC, 0xFC, 0x2F, 0x2B, 0x00, 0x00}, // 0x9D + {0xFF, 0xFF, 0x09, 0x09, 0x2F, 0xF6, 0xF8, 0xA0}, // 0x9E + {0x40, 0xC0, 0x88, 0xFE, 0x7F, 0x09, 0x03, 0x02}, // 0x9F + {0x20, 0x74, 0x54, 0x55, 0x7D, 0x79, 0x40, 0x00}, // 0xA0 + {0x00, 0x44, 0x7D, 0x7D, 0x41, 0x00, 0x00, 0x00}, // 0xA1 + {0x30, 0x78, 0x48, 0x4A, 0x7A, 0x32, 0x00, 0x00}, // 0xA2 + {0x38, 0x78, 0x40, 0x42, 0x7A, 0x7A, 0x40, 0x00}, // 0xA3 + {0x7A, 0x7A, 0x0A, 0x0A, 0x7A, 0x70, 0x00, 0x00}, // 0xA4 + {0x7D, 0x7D, 0x19, 0x31, 0x7D, 0x7D, 0x00, 0x00}, // 0xA5 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x2F, 0x28, 0x00}, // 0xA6 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x26, 0x00, 0x00}, // 0xA7 + {0x30, 0x78, 0x4D, 0x45, 0x60, 0x20, 0x00, 0x00}, // 0xA8 + {0x38, 0x38, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // 0xA9 + {0x08, 0x08, 0x08, 0x08, 0x38, 0x38, 0x00, 0x00}, // 0xAA + {0x4F, 0x6F, 0x30, 0x18, 0xCC, 0xEE, 0xBB, 0x91}, // 0xAB + {0x4F, 0x6F, 0x30, 0x18, 0x6C, 0x76, 0xFB, 0xF9}, // 0xAC + {0x00, 0x00, 0x00, 0x7B, 0x7B, 0x00, 0x00, 0x00}, // 0xAD + {0x08, 0x1C, 0x36, 0x22, 0x08, 0x1C, 0x36, 0x22}, // 0xAE + {0x22, 0x36, 0x1C, 0x08, 0x22, 0x36, 0x1C, 0x08}, // 0xAF + {0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00}, // 0xB0 + {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}, // 0xB1 + {0xDD, 0xFF, 0xAA, 0x77, 0xDD, 0xAA, 0xFF, 0x77}, // 0xB2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB3 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB4 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB5 + {0x10, 0x10, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xB6 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x00}, // 0xB7 + {0x14, 0x14, 0x14, 0xFC, 0xFC, 0x00, 0x00, 0x00}, // 0xB8 + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xFF, 0xFF, 0x00}, // 0xB9 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xBA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xFC, 0xFC, 0x00}, // 0xBB + {0x14, 0x14, 0x17, 0x17, 0x10, 0x1F, 0x1F, 0x00}, // 0xBC + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x00}, // 0xBD + {0x14, 0x14, 0x14, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xBE + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x00, 0x00, 0x00}, // 0xBF + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC0 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC1 + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xC2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC3 + {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // 0xC4 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC5 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xC6 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x10}, // 0xC7 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x17, 0x17, 0x14}, // 0xC8 + {0x00, 0x00, 0xFC, 0xFC, 0x04, 0xF4, 0xF4, 0x14}, // 0xC9 + {0x14, 0x14, 0x17, 0x17, 0x10, 0x17, 0x17, 0x14}, // 0xCA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xF4, 0xF4, 0x14}, // 0xCB + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xF7, 0xF7, 0x14}, // 0xCC + {0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14}, // 0xCD + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xF7, 0xF7, 0x14}, // 0xCE + {0x14, 0x14, 0x14, 0x17, 0x17, 0x14, 0x14, 0x14}, // 0xCF + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD0 + {0x14, 0x14, 0x14, 0xF4, 0xF4, 0x14, 0x14, 0x14}, // 0xD1 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD2 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD3 + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x14, 0x14, 0x14}, // 0xD4 + {0x00, 0x00, 0x00, 0xFC, 0xFC, 0x14, 0x14, 0x14}, // 0xD5 + {0x00, 0x00, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD6 + {0x10, 0x10, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0x10}, // 0xD7 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xD8 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xD9 + {0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xDA + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDB + {0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0}, // 0xDC + {0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}, // 0xDD + {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDE + {0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F}, // 0xDF + {0x38, 0x7C, 0x44, 0x6C, 0x38, 0x6C, 0x44, 0x00}, // 0xE0 + {0xFC, 0xFE, 0x2A, 0x2A, 0x3E, 0x14, 0x00, 0x00}, // 0xE1 + {0x7E, 0x7E, 0x02, 0x02, 0x06, 0x06, 0x00, 0x00}, // 0xE2 + {0x02, 0x7E, 0x7E, 0x02, 0x7E, 0x7E, 0x02, 0x00}, // 0xE3 + {0x63, 0x77, 0x5D, 0x49, 0x63, 0x63, 0x00, 0x00}, // 0xE4 + {0x38, 0x7C, 0x44, 0x7C, 0x3C, 0x04, 0x04, 0x00}, // 0xE5 + {0x80, 0xFE, 0x7E, 0x20, 0x20, 0x3E, 0x1E, 0x00}, // 0xE6 + {0x04, 0x06, 0x02, 0x7E, 0x7C, 0x06, 0x02, 0x00}, // 0xE7 + {0x99, 0xBD, 0xE7, 0xE7, 0xBD, 0x99, 0x00, 0x00}, // 0xE8 + {0x1C, 0x3E, 0x6B, 0x49, 0x6B, 0x3E, 0x1C, 0x00}, // 0xE9 + {0x4C, 0x7E, 0x73, 0x01, 0x73, 0x7E, 0x4C, 0x00}, // 0xEA + {0x30, 0x78, 0x4A, 0x4F, 0x7D, 0x39, 0x00, 0x00}, // 0xEB + {0x18, 0x3C, 0x24, 0x3C, 0x3C, 0x24, 0x3C, 0x18}, // 0xEC + {0x98, 0xFC, 0x64, 0x3C, 0x3E, 0x27, 0x3D, 0x18}, // 0xED + {0x1C, 0x3E, 0x6B, 0x49, 0x49, 0x00, 0x00, 0x00}, // 0xEE + {0x7E, 0x7F, 0x01, 0x01, 0x7F, 0x7E, 0x00, 0x00}, // 0xEF + {0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00}, // 0xF0 + {0x44, 0x44, 0x5F, 0x5F, 0x44, 0x44, 0x00, 0x00}, // 0xF1 + {0x40, 0x51, 0x5B, 0x4E, 0x44, 0x40, 0x00, 0x00}, // 0xF2 + {0x40, 0x44, 0x4E, 0x5B, 0x51, 0x40, 0x00, 0x00}, // 0xF3 + {0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x07, 0x06}, // 0xF4 + {0x60, 0xE0, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0x00}, // 0xF5 + {0x08, 0x08, 0x6B, 0x6B, 0x08, 0x08, 0x00, 0x00}, // 0xF6 + {0x24, 0x36, 0x12, 0x36, 0x24, 0x36, 0x12, 0x00}, // 0xF7 + {0x00, 0x06, 0x0F, 0x09, 0x0F, 0x06, 0x00, 0x00}, // 0xF8 + {0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00}, // 0xF9 + {0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00}, // 0xFA + {0x10, 0x30, 0x70, 0xC0, 0xFF, 0xFF, 0x01, 0x01}, // 0xFB + {0x00, 0x1F, 0x1F, 0x01, 0x1F, 0x1E, 0x00, 0x00}, // 0xFC + {0x00, 0x19, 0x1D, 0x17, 0x12, 0x00, 0x00, 0x00}, // 0xFD + {0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0x00}, // 0xFE + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0xFF +}; // end of MAX7219_Dot_Matrix_font + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py index 4241b6ba48..a534c9f87f 100644 --- a/esphome/components/mcp23008/__init__.py +++ b/esphome/components/mcp23008/__init__.py @@ -1,51 +1,28 @@ 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.components import i2c, mcp23x08_base, mcp23xxx_base +from esphome.const import CONF_ID -DEPENDENCIES = ['i2c'] +AUTO_LOAD = ["mcp23x08_base"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] MULTI_CONF = True -mcp23008_ns = cg.esphome_ns.namespace('mcp23008') -MCP23008GPIOMode = mcp23008_ns.enum('MCP23008GPIOMode') -MCP23008_GPIO_MODES = { - 'INPUT': MCP23008GPIOMode.MCP23008_INPUT, - 'INPUT_PULLUP': MCP23008GPIOMode.MCP23008_INPUT_PULLUP, - 'OUTPUT': MCP23008GPIOMode.MCP23008_OUTPUT, -} +mcp23008_ns = cg.esphome_ns.namespace("mcp23008") -MCP23008 = mcp23008_ns.class_('MCP23008', cg.Component, i2c.I2CDevice) -MCP23008GPIOPin = mcp23008_ns.class_('MCP23008GPIOPin', cg.GPIOPin) +MCP23008 = mcp23008_ns.class_("MCP23008", mcp23x08_base.MCP23X08Base, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(MCP23008), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MCP23008), + } + ) + .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) - - -CONF_MCP23008 = 'mcp23008' -MCP23008_OUTPUT_PIN_SCHEMA = cv.Schema({ - cv.Required(CONF_MCP23008): cv.use_id(MCP23008), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, -}) -MCP23008_INPUT_PIN_SCHEMA = cv.Schema({ - cv.Required(CONF_MCP23008): cv.use_id(MCP23008), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, -}) - - -@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23008, - (MCP23008_OUTPUT_PIN_SCHEMA, MCP23008_INPUT_PIN_SCHEMA)) -def mcp23008_pin_to_code(config): - parent = yield cg.get_variable(config[CONF_MCP23008]) - yield MCP23008GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) +async def to_code(config): + var = await mcp23xxx_base.register_mcp23xxx(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index bf5bb55f2e..351360fe1c 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -4,88 +4,37 @@ namespace esphome { namespace mcp23008 { -static const char *TAG = "mcp23008"; +static const char *const TAG = "mcp23008"; void MCP23008::setup() { ESP_LOGCONFIG(TAG, "Setting up MCP23008..."); uint8_t iocon; - if (!this->read_reg_(MCP23008_IOCON, &iocon)) { + if (!this->read_reg(mcp23x08_base::MCP23X08_IOCON, &iocon)) { this->mark_failed(); return; } - // all pins input - this->write_reg_(MCP23008_IODIR, 0xFF); -} -bool MCP23008::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; - uint8_t reg_addr = MCP23008_GPIO; - uint8_t value = 0; - this->read_reg_(reg_addr, &value); - return value & (1 << bit); -} -void MCP23008::digital_write(uint8_t pin, bool value) { - uint8_t reg_addr = MCP23008_OLAT; - this->update_reg_(pin, value, reg_addr); -} -void MCP23008::pin_mode(uint8_t pin, uint8_t mode) { - uint8_t iodir = MCP23008_IODIR; - uint8_t gppu = MCP23008_GPPU; - switch (mode) { - case MCP23008_INPUT: - this->update_reg_(pin, true, iodir); - break; - case MCP23008_INPUT_PULLUP: - this->update_reg_(pin, true, iodir); - this->update_reg_(pin, true, gppu); - break; - case MCP23008_OUTPUT: - this->update_reg_(pin, false, iodir); - break; - default: - break; + if (this->open_drain_ints_) { + // enable open-drain interrupt pins, 3.3V-safe + this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); } } -float MCP23008::get_setup_priority() const { return setup_priority::HARDWARE; } -bool MCP23008::read_reg_(uint8_t reg, uint8_t *value) { + +void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); } + +bool MCP23008::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) return false; return this->read_byte(reg, value); } -bool MCP23008::write_reg_(uint8_t reg, uint8_t value) { + +bool MCP23008::write_reg(uint8_t reg, uint8_t value) { if (this->is_failed()) return false; return this->write_byte(reg, value); } -void MCP23008::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { - uint8_t bit = pin % 8; - uint8_t reg_value = 0; - if (reg_addr == MCP23008_OLAT) { - reg_value = this->olat_; - } else { - this->read_reg_(reg_addr, ®_value); - } - - if (pin_value) - reg_value |= 1 << bit; - else - reg_value &= ~(1 << bit); - - this->write_reg_(reg_addr, reg_value); - - if (reg_addr == MCP23008_OLAT) { - this->olat_ = reg_value; - } -} - -MCP23008GPIOPin::MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted) - : GPIOPin(pin, mode, inverted), parent_(parent) {} -void MCP23008GPIOPin::setup() { this->pin_mode(this->mode_); } -void MCP23008GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } -bool MCP23008GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } -void MCP23008GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } } // namespace mcp23008 } // namespace esphome diff --git a/esphome/components/mcp23008/mcp23008.h b/esphome/components/mcp23008/mcp23008.h index b4e5d75fd4..406ce0b419 100644 --- a/esphome/components/mcp23008/mcp23008.h +++ b/esphome/components/mcp23008/mcp23008.h @@ -1,68 +1,23 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/components/mcp23x08_base/mcp23x08_base.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" namespace esphome { namespace mcp23008 { -/// Modes for MCP23008 pins -enum MCP23008GPIOMode : uint8_t { - MCP23008_INPUT = INPUT, // 0x00 - MCP23008_INPUT_PULLUP = INPUT_PULLUP, // 0x02 - MCP23008_OUTPUT = OUTPUT // 0x01 -}; - -enum MCP23008GPIORegisters { - // A side - MCP23008_IODIR = 0x00, - MCP23008_IPOL = 0x01, - MCP23008_GPINTEN = 0x02, - MCP23008_DEFVAL = 0x03, - MCP23008_INTCON = 0x04, - MCP23008_IOCON = 0x05, - MCP23008_GPPU = 0x06, - MCP23008_INTF = 0x07, - MCP23008_INTCAP = 0x08, - MCP23008_GPIO = 0x09, - MCP23008_OLAT = 0x0A, -}; - -class MCP23008 : public Component, public i2c::I2CDevice { +class MCP23008 : public mcp23x08_base::MCP23X08Base, public i2c::I2CDevice { public: MCP23008() = default; void setup() override; - - bool digital_read(uint8_t pin); - void digital_write(uint8_t pin, bool value); - void pin_mode(uint8_t pin, uint8_t mode); - - float get_setup_priority() const override; + void dump_config() override; protected: - // read a given register - bool read_reg_(uint8_t reg, uint8_t *value); - // write a value to a given register - bool write_reg_(uint8_t reg, uint8_t value); - // update registers with given pin value. - void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); - - uint8_t olat_{0x00}; -}; - -class MCP23008GPIOPin : public GPIOPin { - public: - MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted = false); - - void setup() override; - void pin_mode(uint8_t mode) override; - bool digital_read() override; - void digital_write(bool value) override; - - protected: - MCP23008 *parent_; + bool read_reg(uint8_t reg, uint8_t *value) override; + bool write_reg(uint8_t reg, uint8_t value) override; }; } // namespace mcp23008 diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 93c3d3843c..c1209a9627 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -2,49 +2,75 @@ 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'] +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_ns = cg.esphome_ns.namespace("mcp23016") -MCP23016 = mcp23016_ns.class_('MCP23016', cg.Component, i2c.I2CDevice) -MCP23016GPIOPin = mcp23016_ns.class_('MCP23016GPIOPin', cg.GPIOPin) +MCP23016 = mcp23016_ns.class_("MCP23016", cg.Component, i2c.I2CDevice) +MCP23016GPIOPin = mcp23016_ns.class_("MCP23016GPIOPin", cg.GPIOPin) -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(MCP23016), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MCP23016), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) -CONF_MCP23016 = 'mcp23016' -MCP23016_OUTPUT_PIN_SCHEMA = cv.Schema({ - 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.Optional(CONF_INVERTED, default=False): cv.boolean, -}) +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 -@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23016, - (MCP23016_OUTPUT_PIN_SCHEMA, MCP23016_INPUT_PIN_SCHEMA)) -def mcp23016_pin_to_code(config): - parent = yield cg.get_variable(config[CONF_MCP23016]) - yield MCP23016GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) +CONF_MCP23016 = "mcp23016" +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_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_PIN_SCHEMA) +async def mcp23016_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_MCP23016]) + + 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 bd04486965..a8df4e1745 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -1,10 +1,11 @@ #include "mcp23016.h" #include "esphome/core/log.h" +#include namespace esphome { namespace mcp23016 { -static const char *TAG = "mcp23016"; +static const char *const TAG = "mcp23016"; void MCP23016::setup() { ESP_LOGCONFIG(TAG, "Setting up 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/__init__.py b/esphome/components/mcp23017/__init__.py index 4b798bf434..42fc37dd1d 100644 --- a/esphome/components/mcp23017/__init__.py +++ b/esphome/components/mcp23017/__init__.py @@ -1,51 +1,28 @@ 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.components import i2c, mcp23x17_base, mcp23xxx_base +from esphome.const import CONF_ID -DEPENDENCIES = ['i2c'] +AUTO_LOAD = ["mcp23x17_base"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] MULTI_CONF = True -mcp23017_ns = cg.esphome_ns.namespace('mcp23017') -MCP23017GPIOMode = mcp23017_ns.enum('MCP23017GPIOMode') -MCP23017_GPIO_MODES = { - 'INPUT': MCP23017GPIOMode.MCP23017_INPUT, - 'INPUT_PULLUP': MCP23017GPIOMode.MCP23017_INPUT_PULLUP, - 'OUTPUT': MCP23017GPIOMode.MCP23017_OUTPUT, -} +mcp23017_ns = cg.esphome_ns.namespace("mcp23017") -MCP23017 = mcp23017_ns.class_('MCP23017', cg.Component, i2c.I2CDevice) -MCP23017GPIOPin = mcp23017_ns.class_('MCP23017GPIOPin', cg.GPIOPin) +MCP23017 = mcp23017_ns.class_("MCP23017", mcp23x17_base.MCP23X17Base, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(MCP23017), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MCP23017), + } + ) + .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) - - -CONF_MCP23017 = 'mcp23017' -MCP23017_OUTPUT_PIN_SCHEMA = cv.Schema({ - cv.Required(CONF_MCP23017): cv.use_id(MCP23017), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23017_GPIO_MODES, upper=True), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, -}) -MCP23017_INPUT_PIN_SCHEMA = cv.Schema({ - cv.Required(CONF_MCP23017): cv.use_id(MCP23017), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23017_GPIO_MODES, upper=True), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, -}) - - -@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23017, - (MCP23017_OUTPUT_PIN_SCHEMA, MCP23017_INPUT_PIN_SCHEMA)) -def mcp23017_pin_to_code(config): - parent = yield cg.get_variable(config[CONF_MCP23017]) - yield MCP23017GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) +async def to_code(config): + var = await mcp23xxx_base.register_mcp23xxx(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 9653aa680d..7344f482e0 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -4,93 +4,37 @@ namespace esphome { namespace mcp23017 { -static const char *TAG = "mcp23017"; +static const char *const TAG = "mcp23017"; void MCP23017::setup() { ESP_LOGCONFIG(TAG, "Setting up MCP23017..."); uint8_t iocon; - if (!this->read_reg_(MCP23017_IOCONA, &iocon)) { + if (!this->read_reg(mcp23x17_base::MCP23X17_IOCONA, &iocon)) { this->mark_failed(); return; } - // all pins input - this->write_reg_(MCP23017_IODIRA, 0xFF); - this->write_reg_(MCP23017_IODIRB, 0xFF); -} -bool MCP23017::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; - uint8_t reg_addr = pin < 8 ? MCP23017_GPIOA : MCP23017_GPIOB; - uint8_t value = 0; - this->read_reg_(reg_addr, &value); - return value & (1 << bit); -} -void MCP23017::digital_write(uint8_t pin, bool value) { - uint8_t reg_addr = pin < 8 ? MCP23017_OLATA : MCP23017_OLATB; - this->update_reg_(pin, value, reg_addr); -} -void MCP23017::pin_mode(uint8_t pin, uint8_t mode) { - uint8_t iodir = pin < 8 ? MCP23017_IODIRA : MCP23017_IODIRB; - uint8_t gppu = pin < 8 ? MCP23017_GPPUA : MCP23017_GPPUB; - switch (mode) { - case MCP23017_INPUT: - this->update_reg_(pin, true, iodir); - break; - case MCP23017_INPUT_PULLUP: - this->update_reg_(pin, true, iodir); - this->update_reg_(pin, true, gppu); - break; - case MCP23017_OUTPUT: - this->update_reg_(pin, false, iodir); - break; - default: - break; + if (this->open_drain_ints_) { + // enable open-drain interrupt pins, 3.3V-safe + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); } } -float MCP23017::get_setup_priority() const { return setup_priority::IO; } -bool MCP23017::read_reg_(uint8_t reg, uint8_t *value) { + +void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); } + +bool MCP23017::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) return false; return this->read_byte(reg, value); } -bool MCP23017::write_reg_(uint8_t reg, uint8_t value) { +bool MCP23017::write_reg(uint8_t reg, uint8_t value) { if (this->is_failed()) return false; return this->write_byte(reg, value); } -void MCP23017::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { - uint8_t bit = pin % 8; - uint8_t reg_value = 0; - if (reg_addr == MCP23017_OLATA) { - reg_value = this->olat_a_; - } else if (reg_addr == MCP23017_OLATB) { - reg_value = this->olat_b_; - } else { - this->read_reg_(reg_addr, ®_value); - } - - if (pin_value) - reg_value |= 1 << bit; - else - reg_value &= ~(1 << bit); - - this->write_reg_(reg_addr, reg_value); - - if (reg_addr == MCP23017_OLATA) { - this->olat_a_ = reg_value; - } else if (reg_addr == MCP23017_OLATB) { - this->olat_b_ = reg_value; - } -} - -MCP23017GPIOPin::MCP23017GPIOPin(MCP23017 *parent, uint8_t pin, uint8_t mode, bool inverted) - : GPIOPin(pin, mode, inverted), parent_(parent) {} -void MCP23017GPIOPin::setup() { this->pin_mode(this->mode_); } -void MCP23017GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } -bool MCP23017GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } -void MCP23017GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } } // namespace mcp23017 } // namespace esphome diff --git a/esphome/components/mcp23017/mcp23017.h b/esphome/components/mcp23017/mcp23017.h index 4389eeb6ff..8959e06a41 100644 --- a/esphome/components/mcp23017/mcp23017.h +++ b/esphome/components/mcp23017/mcp23017.h @@ -1,81 +1,23 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/components/mcp23x17_base/mcp23x17_base.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" namespace esphome { namespace mcp23017 { -/// Modes for MCP23017 pins -enum MCP23017GPIOMode : uint8_t { - MCP23017_INPUT = INPUT, // 0x00 - MCP23017_INPUT_PULLUP = INPUT_PULLUP, // 0x02 - MCP23017_OUTPUT = OUTPUT // 0x01 -}; - -enum MCP23017GPIORegisters { - // A side - MCP23017_IODIRA = 0x00, - MCP23017_IPOLA = 0x02, - MCP23017_GPINTENA = 0x04, - MCP23017_DEFVALA = 0x06, - MCP23017_INTCONA = 0x08, - MCP23017_IOCONA = 0x0A, - MCP23017_GPPUA = 0x0C, - MCP23017_INTFA = 0x0E, - MCP23017_INTCAPA = 0x10, - MCP23017_GPIOA = 0x12, - MCP23017_OLATA = 0x14, - // B side - MCP23017_IODIRB = 0x01, - MCP23017_IPOLB = 0x03, - MCP23017_GPINTENB = 0x05, - MCP23017_DEFVALB = 0x07, - MCP23017_INTCONB = 0x09, - MCP23017_IOCONB = 0x0B, - MCP23017_GPPUB = 0x0D, - MCP23017_INTFB = 0x0F, - MCP23017_INTCAPB = 0x11, - MCP23017_GPIOB = 0x13, - MCP23017_OLATB = 0x15, -}; - -class MCP23017 : public Component, public i2c::I2CDevice { +class MCP23017 : public mcp23x17_base::MCP23X17Base, public i2c::I2CDevice { public: MCP23017() = default; void setup() override; - - bool digital_read(uint8_t pin); - void digital_write(uint8_t pin, bool value); - void pin_mode(uint8_t pin, uint8_t mode); - - float get_setup_priority() const override; + void dump_config() override; protected: - // read a given register - bool read_reg_(uint8_t reg, uint8_t *value); - // write a value to a given register - bool write_reg_(uint8_t reg, uint8_t value); - // update registers with given pin value. - void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); - - uint8_t olat_a_{0x00}; - uint8_t olat_b_{0x00}; -}; - -class MCP23017GPIOPin : public GPIOPin { - public: - MCP23017GPIOPin(MCP23017 *parent, uint8_t pin, uint8_t mode, bool inverted = false); - - void setup() override; - void pin_mode(uint8_t mode) override; - bool digital_read() override; - void digital_write(bool value) override; - - protected: - MCP23017 *parent_; + bool read_reg(uint8_t reg, uint8_t *value) override; + bool write_reg(uint8_t reg, uint8_t value) override; }; } // namespace mcp23017 diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py new file mode 100644 index 0000000000..4d3998def8 --- /dev/null +++ b/esphome/components/mcp23s08/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, mcp23x08_base, mcp23xxx_base +from esphome.const import CONF_ID + +AUTO_LOAD = ["mcp23x08_base"] +CODEOWNERS = ["@SenexCrenshaw", "@jesserockz"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +CONF_DEVICEADDRESS = "deviceaddress" + +mcp23S08_ns = cg.esphome_ns.namespace("mcp23s08") + +mcp23S08 = mcp23S08_ns.class_("MCP23S08", mcp23x08_base.MCP23X08Base, spi.SPIDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(mcp23S08), + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, + } + ) + .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) + .extend(spi.spi_device_schema()) +) + + +async def to_code(config): + var = await mcp23xxx_base.register_mcp23xxx(config) + cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) + await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp new file mode 100644 index 0000000000..af834b4c40 --- /dev/null +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -0,0 +1,56 @@ +#include "mcp23s08.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23s08 { + +static const char *const TAG = "mcp23s08"; + +void MCP23S08::set_device_address(uint8_t device_addr) { + if (device_addr != 0) { + this->device_opcode_ |= ((device_addr & 0x03) << 1); + } +} + +void MCP23S08::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23S08..."); + this->spi_setup(); + + this->enable(); + uint8_t cmd = 0b01000000; + this->transfer_byte(cmd); + this->transfer_byte(mcp23x08_base::MCP23X08_IOCON); + this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->disable(); + + if (this->open_drain_ints_) { + // enable open-drain interrupt pins, 3.3V-safe + this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + } +} + +void MCP23S08::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23S08:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) { + this->enable(); + this->transfer_byte(this->device_opcode_ | 1); + this->transfer_byte(reg); + *value = this->transfer_byte(0); + this->disable(); + return true; +} + +bool MCP23S08::write_reg(uint8_t reg, uint8_t value) { + this->enable(); + this->transfer_byte(this->device_opcode_); + this->transfer_byte(reg); + this->transfer_byte(value); + this->disable(); + return true; +} + +} // namespace mcp23s08 +} // namespace esphome diff --git a/esphome/components/mcp23s08/mcp23s08.h b/esphome/components/mcp23s08/mcp23s08.h new file mode 100644 index 0000000000..a2a6be880a --- /dev/null +++ b/esphome/components/mcp23s08/mcp23s08.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/mcp23x08_base/mcp23x08_base.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp23s08 { + +class MCP23S08 : public mcp23x08_base::MCP23X08Base, + public spi::SPIDevice { + public: + MCP23S08() = default; + + void setup() override; + void dump_config() override; + + void set_device_address(uint8_t device_addr); + + protected: + bool read_reg(uint8_t reg, uint8_t *value) override; + bool write_reg(uint8_t reg, uint8_t value) override; + + uint8_t device_opcode_ = 0x40; +}; + +} // namespace mcp23s08 +} // namespace esphome diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py new file mode 100644 index 0000000000..9e199f79c4 --- /dev/null +++ b/esphome/components/mcp23s17/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, mcp23x17_base, mcp23xxx_base +from esphome.const import CONF_ID + +AUTO_LOAD = ["mcp23x17_base"] +CODEOWNERS = ["@SenexCrenshaw", "@jesserockz"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +CONF_DEVICEADDRESS = "deviceaddress" + +mcp23S17_ns = cg.esphome_ns.namespace("mcp23s17") + +mcp23S17 = mcp23S17_ns.class_("MCP23S17", mcp23x17_base.MCP23X17Base, spi.SPIDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(mcp23S17), + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, + } + ) + .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) + .extend(spi.spi_device_schema()) +) + + +async def to_code(config): + var = await mcp23xxx_base.register_mcp23xxx(config) + cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) + await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp new file mode 100644 index 0000000000..8e3d5213f8 --- /dev/null +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -0,0 +1,58 @@ +#include "mcp23s17.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23s17 { + +static const char *const TAG = "mcp23s17"; + +void MCP23S17::set_device_address(uint8_t device_addr) { + if (device_addr != 0) { + this->device_opcode_ |= ((device_addr & 0b111) << 1); + } +} + +void MCP23S17::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23S17..."); + this->spi_setup(); + + this->enable(); + uint8_t cmd = 0b01000000; + this->transfer_byte(cmd); + this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); + this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->disable(); + + if (this->open_drain_ints_) { + // enable open-drain interrupt pins, 3.3V-safe + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + } +} + +void MCP23S17::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23S17:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +bool MCP23S17::read_reg(uint8_t reg, uint8_t *value) { + this->enable(); + this->transfer_byte(this->device_opcode_ | 1); + this->transfer_byte(reg); + *value = this->transfer_byte(0xFF); + this->disable(); + return true; +} + +bool MCP23S17::write_reg(uint8_t reg, uint8_t value) { + this->enable(); + this->transfer_byte(this->device_opcode_); + this->transfer_byte(reg); + this->transfer_byte(value); + + this->disable(); + return true; +} + +} // namespace mcp23s17 +} // namespace esphome diff --git a/esphome/components/mcp23s17/mcp23s17.h b/esphome/components/mcp23s17/mcp23s17.h new file mode 100644 index 0000000000..cb5d6cfcd8 --- /dev/null +++ b/esphome/components/mcp23s17/mcp23s17.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/mcp23x17_base/mcp23x17_base.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp23s17 { + +class MCP23S17 : public mcp23x17_base::MCP23X17Base, + public spi::SPIDevice { + public: + MCP23S17() = default; + + void setup() override; + void dump_config() override; + void set_device_address(uint8_t device_addr); + + protected: + bool read_reg(uint8_t reg, uint8_t *value) override; + bool write_reg(uint8_t reg, uint8_t value) override; + + uint8_t device_opcode_ = 0x40; +}; + +} // namespace mcp23s17 +} // namespace esphome diff --git a/esphome/components/mcp23x08_base/__init__.py b/esphome/components/mcp23x08_base/__init__.py new file mode 100644 index 0000000000..ba44917202 --- /dev/null +++ b/esphome/components/mcp23x08_base/__init__.py @@ -0,0 +1,8 @@ +import esphome.codegen as cg +from esphome.components import mcp23xxx_base + +AUTO_LOAD = ["mcp23xxx_base"] +CODEOWNERS = ["@jesserockz"] + +mcp23x08_base_ns = cg.esphome_ns.namespace("mcp23x08_base") +MCP23X08Base = mcp23x08_base_ns.class_("MCP23X08Base", mcp23xxx_base.MCP23XXXBase) diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp new file mode 100644 index 0000000000..2137b36921 --- /dev/null +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -0,0 +1,83 @@ +#include "mcp23x08_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23x08_base { + +static const char *const TAG = "mcp23x08_base"; + +bool MCP23X08Base::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = mcp23x08_base::MCP23X08_GPIO; + uint8_t value = 0; + this->read_reg(reg_addr, &value); + return value & (1 << bit); +} + +void MCP23X08Base::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = mcp23x08_base::MCP23X08_OLAT; + this->update_reg(pin, value, reg_addr); +} + +void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t iodir = mcp23x08_base::MCP23X08_IODIR; + uint8_t gppu = mcp23x08_base::MCP23X08_GPPU; + 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); + } +} + +void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { + uint8_t gpinten = mcp23x08_base::MCP23X08_GPINTEN; + uint8_t intcon = mcp23x08_base::MCP23X08_INTCON; + uint8_t defval = mcp23x08_base::MCP23X08_DEFVAL; + + switch (interrupt_mode) { + case mcp23xxx_base::MCP23XXX_CHANGE: + this->update_reg(pin, true, gpinten); + this->update_reg(pin, false, intcon); + break; + case mcp23xxx_base::MCP23XXX_RISING: + this->update_reg(pin, true, gpinten); + this->update_reg(pin, true, intcon); + this->update_reg(pin, true, defval); + break; + case mcp23xxx_base::MCP23XXX_FALLING: + this->update_reg(pin, true, gpinten); + this->update_reg(pin, true, intcon); + this->update_reg(pin, false, defval); + break; + case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: + this->update_reg(pin, false, gpinten); + break; + } +} + +void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == mcp23x08_base::MCP23X08_OLAT) { + reg_value = this->olat_; + } else { + this->read_reg(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg(reg_addr, reg_value); + + if (reg_addr == mcp23x08_base::MCP23X08_OLAT) { + this->olat_ = reg_value; + } +} + +} // namespace mcp23x08_base +} // namespace esphome diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.h b/esphome/components/mcp23x08_base/mcp23x08_base.h new file mode 100644 index 0000000000..910519119b --- /dev/null +++ b/esphome/components/mcp23x08_base/mcp23x08_base.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace mcp23x08_base { + +enum MCP23S08GPIORegisters { + // A side + MCP23X08_IODIR = 0x00, + MCP23X08_IPOL = 0x01, + MCP23X08_GPINTEN = 0x02, + MCP23X08_DEFVAL = 0x03, + MCP23X08_INTCON = 0x04, + MCP23X08_IOCON = 0x05, + MCP23X08_GPPU = 0x06, + MCP23X08_INTF = 0x07, + MCP23X08_INTCAP = 0x08, + MCP23X08_GPIO = 0x09, + MCP23X08_OLAT = 0x0A, +}; + +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, gpio::Flags flags) override; + void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; + + protected: + void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) override; + + uint8_t olat_{0x00}; +}; + +} // namespace mcp23x08_base +} // namespace esphome diff --git a/esphome/components/mcp23x17_base/__init__.py b/esphome/components/mcp23x17_base/__init__.py new file mode 100644 index 0000000000..97e0b3823d --- /dev/null +++ b/esphome/components/mcp23x17_base/__init__.py @@ -0,0 +1,8 @@ +import esphome.codegen as cg +from esphome.components import mcp23xxx_base + +AUTO_LOAD = ["mcp23xxx_base"] +CODEOWNERS = ["@jesserockz"] + +mcp23x17_base_ns = cg.esphome_ns.namespace("mcp23x17_base") +MCP23X17Base = mcp23x17_base_ns.class_("MCP23X17Base", mcp23xxx_base.MCP23XXXBase) diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp new file mode 100644 index 0000000000..744f2fbe9c --- /dev/null +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -0,0 +1,88 @@ +#include "mcp23x17_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23x17_base { + +static const char *const TAG = "mcp23x17_base"; + +bool MCP23X17Base::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_GPIOA : mcp23x17_base::MCP23X17_GPIOB; + uint8_t value = 0; + this->read_reg(reg_addr, &value); + return value & (1 << bit); +} + +void MCP23X17Base::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_OLATA : mcp23x17_base::MCP23X17_OLATB; + this->update_reg(pin, value, reg_addr); +} + +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; + if (flags == gpio::FLAG_INPUT) { + this->update_reg(pin, true, iodir); + this->update_reg(pin, false, gppu); + } 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); + } +} + +void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { + uint8_t gpinten = pin < 8 ? mcp23x17_base::MCP23X17_GPINTENA : mcp23x17_base::MCP23X17_GPINTENB; + uint8_t intcon = pin < 8 ? mcp23x17_base::MCP23X17_INTCONA : mcp23x17_base::MCP23X17_INTCONB; + uint8_t defval = pin < 8 ? mcp23x17_base::MCP23X17_DEFVALA : mcp23x17_base::MCP23X17_DEFVALB; + + switch (interrupt_mode) { + case mcp23xxx_base::MCP23XXX_CHANGE: + this->update_reg(pin, true, gpinten); + this->update_reg(pin, false, intcon); + break; + case mcp23xxx_base::MCP23XXX_RISING: + this->update_reg(pin, true, gpinten); + this->update_reg(pin, true, intcon); + this->update_reg(pin, true, defval); + break; + case mcp23xxx_base::MCP23XXX_FALLING: + this->update_reg(pin, true, gpinten); + this->update_reg(pin, true, intcon); + this->update_reg(pin, false, defval); + break; + case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: + this->update_reg(pin, false, gpinten); + break; + } +} + +void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == mcp23x17_base::MCP23X17_OLATA) { + reg_value = this->olat_a_; + } else if (reg_addr == mcp23x17_base::MCP23X17_OLATB) { + reg_value = this->olat_b_; + } else { + this->read_reg(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg(reg_addr, reg_value); + + if (reg_addr == mcp23x17_base::MCP23X17_OLATA) { + this->olat_a_ = reg_value; + } else if (reg_addr == mcp23x17_base::MCP23X17_OLATB) { + this->olat_b_ = reg_value; + } +} + +} // namespace mcp23x17_base +} // namespace esphome diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.h b/esphome/components/mcp23x17_base/mcp23x17_base.h new file mode 100644 index 0000000000..3d50ee8c03 --- /dev/null +++ b/esphome/components/mcp23x17_base/mcp23x17_base.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace mcp23x17_base { + +enum MCP23X17GPIORegisters { + // A side + MCP23X17_IODIRA = 0x00, + MCP23X17_IPOLA = 0x02, + MCP23X17_GPINTENA = 0x04, + MCP23X17_DEFVALA = 0x06, + MCP23X17_INTCONA = 0x08, + MCP23X17_IOCONA = 0x0A, + MCP23X17_GPPUA = 0x0C, + MCP23X17_INTFA = 0x0E, + MCP23X17_INTCAPA = 0x10, + MCP23X17_GPIOA = 0x12, + MCP23X17_OLATA = 0x14, + // B side + MCP23X17_IODIRB = 0x01, + MCP23X17_IPOLB = 0x03, + MCP23X17_GPINTENB = 0x05, + MCP23X17_DEFVALB = 0x07, + MCP23X17_INTCONB = 0x09, + MCP23X17_IOCONB = 0x0B, + MCP23X17_GPPUB = 0x0D, + MCP23X17_INTFB = 0x0F, + MCP23X17_INTCAPB = 0x11, + MCP23X17_GPIOB = 0x13, + MCP23X17_OLATB = 0x15, +}; + +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, gpio::Flags flags) override; + void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; + + protected: + void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) override; + + uint8_t olat_a_{0x00}; + uint8_t olat_b_{0x00}; +}; + +} // namespace mcp23x17_base +} // namespace esphome diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py new file mode 100644 index 0000000000..f2c2706416 --- /dev/null +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -0,0 +1,114 @@ +import esphome.codegen as cg +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 + +CODEOWNERS = ["@jesserockz"] + +mcp23xxx_base_ns = cg.esphome_ns.namespace("mcp23xxx_base") +MCP23XXXBase = mcp23xxx_base_ns.class_("MCP23XXXBase", cg.Component) +MCP23XXXGPIOPin = mcp23xxx_base_ns.class_("MCP23XXXGPIOPin", cg.GPIOPin) +MCP23XXXGPIOMode = mcp23xxx_base_ns.enum("MCP23XXXGPIOMode") +MCP23XXXInterruptMode = mcp23xxx_base_ns.enum("MCP23XXXInterruptMode") + +MCP23XXX_INTERRUPT_MODES = { + "NO_INTERRUPT": MCP23XXXInterruptMode.MCP23XXX_NO_INTERRUPT, + "CHANGE": MCP23XXXInterruptMode.MCP23XXX_CHANGE, + "RISING": MCP23XXXInterruptMode.MCP23XXX_RISING, + "FALLING": MCP23XXXInterruptMode.MCP23XXX_FALLING, +} + +MCP23XXX_GPIO_MODES = { + "INPUT": MCP23XXXGPIOMode.MCP23XXX_INPUT, + "INPUT_PULLUP": MCP23XXXGPIOMode.MCP23XXX_INPUT_PULLUP, + "OUTPUT": MCP23XXXGPIOMode.MCP23XXX_OUTPUT, +} + +MCP23XXX_CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +@coroutine +async def register_mcp23xxx(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT])) + 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_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(MCP23XXXGPIOPin), + cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + cv.Optional(CONF_INTERRUPT, default="NO_INTERRUPT"): cv.enum( + MCP23XXX_INTERRUPT_MODES, upper=True + ), + } +) + + +@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]) + + 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 2022.5.0 + +for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: + invalid_schema = cv.invalid( + f"'{id}:' has been removed from the pin schema in 1.17.0, please use 'mcp23xxx:'" + ) + + # pylint: disable=cell-var-from-loop + @pins.PIN_SCHEMA_REGISTRY.register(id, invalid_schema) + def pin_to_code(config): + pass + + +# END Removed pin schemas diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp new file mode 100644 index 0000000000..14a703fb9f --- /dev/null +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -0,0 +1,20 @@ +#include "mcp23xxx_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23xxx_base { + +float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } + +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 new file mode 100644 index 0000000000..a522ea28c5 --- /dev/null +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.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 }; + +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, 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; } + float get_setup_priority() const override; + + protected: + // read a given register + virtual bool read_reg(uint8_t reg, uint8_t *value); + // write a value to a given register + virtual bool write_reg(uint8_t reg, uint8_t value); + // update registers with given pin value. + virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a); + + bool open_drain_ints_; +}; + +class MCP23XXXGPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(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_; +}; + +} // namespace mcp23xxx_base +} // namespace esphome diff --git a/esphome/components/mcp2515/__init__.py b/esphome/components/mcp2515/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp2515/canbus.py b/esphome/components/mcp2515/canbus.py new file mode 100644 index 0000000000..c410c1af69 --- /dev/null +++ b/esphome/components/mcp2515/canbus.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, canbus +from esphome.const import CONF_ID, CONF_MODE +from esphome.components.canbus import CanbusComponent + +CODEOWNERS = ["@mvturnho", "@danielschramm"] +DEPENDENCIES = ["spi"] + +CONF_CLOCK = "clock" + +mcp2515_ns = cg.esphome_ns.namespace("mcp2515") +mcp2515 = mcp2515_ns.class_("MCP2515", CanbusComponent, spi.SPIDevice) +CanClock = mcp2515_ns.enum("CAN_CLOCK") +McpMode = mcp2515_ns.enum("CANCTRL_REQOP_MODE") + +CAN_CLOCK = { + "8MHZ": CanClock.MCP_8MHZ, + "16MHZ": CanClock.MCP_16MHZ, + "20MHZ": CanClock.MCP_20MHZ, +} + +MCP_MODE = { + "NORMAL": McpMode.CANCTRL_REQOP_NORMAL, + "LOOPBACK": McpMode.CANCTRL_REQOP_LOOPBACK, + "LISTENONLY": McpMode.CANCTRL_REQOP_LISTENONLY, +} + +CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(mcp2515), + cv.Optional(CONF_CLOCK, default="8MHZ"): cv.enum(CAN_CLOCK, upper=True), + cv.Optional(CONF_MODE, default="NORMAL"): cv.enum(MCP_MODE, upper=True), + } +).extend(spi.spi_device_schema(True)) + + +async def to_code(config): + rhs = mcp2515.new() + var = cg.Pvariable(config[CONF_ID], rhs) + await canbus.register_canbus(var, config) + if CONF_CLOCK in config: + canclock = CAN_CLOCK[config[CONF_CLOCK]] + cg.add(var.set_mcp_clock(canclock)) + if CONF_MODE in config: + mode = MCP_MODE[config[CONF_MODE]] + cg.add(var.set_mcp_mode(mode)) + + await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp new file mode 100644 index 0000000000..e845c79a64 --- /dev/null +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -0,0 +1,609 @@ +#include "mcp2515.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp2515 { + +static const char *const TAG = "mcp2515"; + +const struct MCP2515::TxBnRegs MCP2515::TXB[N_TXBUFFERS] = {{MCP_TXB0CTRL, MCP_TXB0SIDH, MCP_TXB0DATA}, + {MCP_TXB1CTRL, MCP_TXB1SIDH, MCP_TXB1DATA}, + {MCP_TXB2CTRL, MCP_TXB2SIDH, MCP_TXB2DATA}}; + +const struct MCP2515::RxBnRegs MCP2515::RXB[N_RXBUFFERS] = {{MCP_RXB0CTRL, MCP_RXB0SIDH, MCP_RXB0DATA, CANINTF_RX0IF}, + {MCP_RXB1CTRL, MCP_RXB1SIDH, MCP_RXB1DATA, CANINTF_RX1IF}}; + +bool MCP2515::setup_internal() { + this->spi_setup(); + + if (this->reset_() == canbus::ERROR_FAIL) + return false; + this->set_bitrate_(this->bit_rate_, this->mcp_clock_); + this->set_mode_(this->mcp_mode_); + ESP_LOGV(TAG, "setup done"); + return true; +} + +canbus::Error MCP2515::reset_() { + this->enable(); + this->transfer_byte(INSTRUCTION_RESET); + this->disable(); + ESP_LOGV(TAG, "reset_()"); + delay(10); + + ESP_LOGV(TAG, "reset() CLEAR ALL TXB registers"); + + uint8_t zeros[14]; + memset(zeros, 0, sizeof(zeros)); + set_registers_(MCP_TXB0CTRL, zeros, 14); + set_registers_(MCP_TXB1CTRL, zeros, 14); + set_registers_(MCP_TXB2CTRL, zeros, 14); + ESP_LOGD(TAG, "reset() CLEARED TXB registers"); + + set_register_(MCP_RXB0CTRL, 0); + set_register_(MCP_RXB1CTRL, 0); + + set_register_(MCP_CANINTE, CANINTF_RX0IF | CANINTF_RX1IF | CANINTF_ERRIF | CANINTF_MERRF); + + modify_register_(MCP_RXB0CTRL, RXB_CTRL_RXM_MASK | RXB_0_CTRL_BUKT, RXB_CTRL_RXM_STDEXT | RXB_0_CTRL_BUKT); + modify_register_(MCP_RXB1CTRL, RXB_CTRL_RXM_MASK, RXB_CTRL_RXM_STDEXT); + + return canbus::ERROR_OK; +} + +uint8_t MCP2515::read_register_(const REGISTER reg) { + this->enable(); + this->transfer_byte(INSTRUCTION_READ); + this->transfer_byte(reg); + uint8_t ret = this->transfer_byte(0x00); + this->disable(); + + return ret; +} + +void MCP2515::read_registers_(const REGISTER reg, uint8_t values[], const uint8_t n) { + this->enable(); + this->transfer_byte(INSTRUCTION_READ); + this->transfer_byte(reg); + // this->transfer_array(values, n); + // mcp2515 has auto - increment of address - pointer + for (uint8_t i = 0; i < n; i++) { + values[i] = this->transfer_byte(0x00); + } + this->disable(); +} + +void MCP2515::set_register_(const REGISTER reg, const uint8_t value) { + this->enable(); + this->transfer_byte(INSTRUCTION_WRITE); + this->transfer_byte(reg); + this->transfer_byte(value); + this->disable(); +} + +void MCP2515::set_registers_(const REGISTER reg, uint8_t values[], const uint8_t n) { + this->enable(); + this->transfer_byte(INSTRUCTION_WRITE); + this->transfer_byte(reg); + // this->transfer_array(values, n); + for (uint8_t i = 0; i < n; i++) { + this->transfer_byte(values[i]); + } + this->disable(); +} + +void MCP2515::modify_register_(const REGISTER reg, const uint8_t mask, const uint8_t data) { + this->enable(); + this->transfer_byte(INSTRUCTION_BITMOD); + this->transfer_byte(reg); + this->transfer_byte(mask); + this->transfer_byte(data); + this->disable(); +} + +uint8_t MCP2515::get_status_() { + this->enable(); + this->transfer_byte(INSTRUCTION_READ_STATUS); + uint8_t i = this->transfer_byte(0x00); + this->disable(); + + return i; +} + +canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { + modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); + + uint32_t end_time = millis() + 10; + bool mode_match = false; + while (millis() < end_time) { + uint8_t new_mode = read_register_(MCP_CANSTAT); + new_mode &= CANSTAT_OPMOD; + mode_match = new_mode == mode; + if (mode_match) { + break; + } + } + return mode_match ? canbus::ERROR_OK : canbus::ERROR_FAIL; +} + +canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { + if (divisor == CLKOUT_DISABLE) { + /* Turn off CLKEN */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, 0x00); + + /* Turn on CLKOUT for SOF */ + modify_register_(MCP_CNF3, CNF3_SOF, CNF3_SOF); + return canbus::ERROR_OK; + } + + /* Set the prescaler (CLKPRE) */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKPRE, divisor); + + /* Turn on CLKEN */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, CANCTRL_CLKEN); + + /* Turn off CLKOUT for SOF */ + modify_register_(MCP_CNF3, CNF3_SOF, 0x00); + return canbus::ERROR_OK; +} + +void MCP2515::prepare_id_(uint8_t *buffer, const bool extended, const uint32_t id) { + uint16_t canid = (uint16_t)(id & 0x0FFFF); + + if (extended) { + buffer[MCP_EID0] = (uint8_t)(canid & 0xFF); + buffer[MCP_EID8] = (uint8_t)(canid >> 8); + canid = (uint16_t)(id >> 16); + buffer[MCP_SIDL] = (uint8_t)(canid & 0x03); + buffer[MCP_SIDL] += (uint8_t)((canid & 0x1C) << 3); + buffer[MCP_SIDL] |= TXB_EXIDE_MASK; + buffer[MCP_SIDH] = (uint8_t)(canid >> 5); + } else { + buffer[MCP_SIDH] = (uint8_t)(canid >> 3); + buffer[MCP_SIDL] = (uint8_t)((canid & 0x07) << 5); + buffer[MCP_EID0] = 0; + buffer[MCP_EID8] = 0; + } +} + +canbus::Error MCP2515::set_filter_mask_(const MASK mask, const bool extended, const uint32_t ul_data) { + canbus::Error res = set_mode_(CANCTRL_REQOP_CONFIG); + if (res != canbus::ERROR_OK) { + return res; + } + + uint8_t tbufdata[4]; + prepare_id_(tbufdata, extended, ul_data); + + REGISTER reg; + switch (mask) { + case MASK0: + reg = MCP_RXM0SIDH; + break; + case MASK1: + reg = MCP_RXM1SIDH; + break; + default: + return canbus::ERROR_FAIL; + } + + set_registers_(reg, tbufdata, 4); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::set_filter_(const RXF num, const bool extended, const uint32_t ul_data) { + canbus::Error res = set_mode_(CANCTRL_REQOP_CONFIG); + if (res != canbus::ERROR_OK) { + return res; + } + + REGISTER reg; + + switch (num) { + case RXF0: + reg = MCP_RXF0SIDH; + break; + case RXF1: + reg = MCP_RXF1SIDH; + break; + case RXF2: + reg = MCP_RXF2SIDH; + break; + case RXF3: + reg = MCP_RXF3SIDH; + break; + case RXF4: + reg = MCP_RXF4SIDH; + break; + case RXF5: + reg = MCP_RXF5SIDH; + break; + default: + return canbus::ERROR_FAIL; + } + + uint8_t tbufdata[4]; + prepare_id_(tbufdata, extended, ul_data); + set_registers_(reg, tbufdata, 4); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::send_message_(TXBn txbn, struct canbus::CanFrame *frame) { + const struct TxBnRegs *txbuf = &TXB[txbn]; + + uint8_t data[13]; + + prepare_id_(data, frame->use_extended_id, frame->can_id); + data[MCP_DLC] = + frame->remote_transmission_request ? (frame->can_data_length_code | RTR_MASK) : frame->can_data_length_code; + memcpy(&data[MCP_DATA], frame->data, frame->can_data_length_code); + set_registers_(txbuf->SIDH, data, 5 + frame->can_data_length_code); + modify_register_(txbuf->CTRL, TXB_TXREQ, TXB_TXREQ); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::send_message(struct canbus::CanFrame *frame) { + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAILTX; + } + TXBn tx_buffers[N_TXBUFFERS] = {TXB0, TXB1, TXB2}; + + for (auto &tx_buffer : tx_buffers) { + const struct TxBnRegs *txbuf = &TXB[tx_buffer]; + uint8_t ctrlval = read_register_(txbuf->CTRL); + if ((ctrlval & TXB_TXREQ) == 0) { + return send_message_(tx_buffer, frame); + } + } + + return canbus::ERROR_FAILTX; +} + +canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) { + const struct RxBnRegs *rxb = &RXB[rxbn]; + + uint8_t tbufdata[5]; + + read_registers_(rxb->SIDH, tbufdata, 5); + + uint32_t id = (tbufdata[MCP_SIDH] << 3) + (tbufdata[MCP_SIDL] >> 5); + bool use_extended_id = false; + bool remote_transmission_request = false; + + if ((tbufdata[MCP_SIDL] & TXB_EXIDE_MASK) == TXB_EXIDE_MASK) { + id = (id << 2) + (tbufdata[MCP_SIDL] & 0x03); + id = (id << 8) + tbufdata[MCP_EID8]; + id = (id << 8) + tbufdata[MCP_EID0]; + // id |= canbus::CAN_EFF_FLAG; + use_extended_id = true; + } + + uint8_t dlc = (tbufdata[MCP_DLC] & DLC_MASK); + if (dlc > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAIL; + } + + uint8_t ctrl = read_register_(rxb->CTRL); + if (ctrl & RXB_CTRL_RTR) { + // id |= canbus::CAN_RTR_FLAG; + remote_transmission_request = true; + } + + frame->can_id = id; + frame->can_data_length_code = dlc; + frame->use_extended_id = use_extended_id; + frame->remote_transmission_request = remote_transmission_request; + + read_registers_(rxb->DATA, frame->data, dlc); + + modify_register_(MCP_CANINTF, rxb->CANINTF_RXnIF, 0); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::read_message(struct canbus::CanFrame *frame) { + canbus::Error rc; + uint8_t stat = get_status_(); + + if (stat & STAT_RX0IF) { + rc = read_message_(RXB0, frame); + } else if (stat & STAT_RX1IF) { + rc = read_message_(RXB1, frame); + } else { + rc = canbus::ERROR_NOMSG; + } + + return rc; +} + +bool MCP2515::check_receive_() { + uint8_t res = get_status_(); + return (res & STAT_RXIF_MASK) != 0; +} + +bool MCP2515::check_error_() { + uint8_t eflg = get_error_flags_(); + return (eflg & EFLG_ERRORMASK) != 0; +} + +uint8_t MCP2515::get_error_flags_() { return read_register_(MCP_EFLG); } + +void MCP2515::clear_rx_n_ovr_flags_() { modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); } + +uint8_t MCP2515::get_int_() { return read_register_(MCP_CANINTF); } + +void MCP2515::clear_int_() { set_register_(MCP_CANINTF, 0); } + +uint8_t MCP2515::get_int_mask_() { return read_register_(MCP_CANINTE); } + +void MCP2515::clear_tx_int_() { modify_register_(MCP_CANINTF, (CANINTF_TX0IF | CANINTF_TX1IF | CANINTF_TX2IF), 0); } + +void MCP2515::clear_rx_n_ovr_() { + uint8_t eflg = get_error_flags_(); + if (eflg != 0) { + clear_rx_n_ovr_flags_(); + clear_int_(); + // modify_register_(MCP_CANINTF, CANINTF_ERRIF, 0); + } +} + +void MCP2515::clear_merr_() { + // modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); + // clear_int_(); + modify_register_(MCP_CANINTF, CANINTF_MERRF, 0); +} + +void MCP2515::clear_errif_() { + // modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); + // clear_int_(); + modify_register_(MCP_CANINTF, CANINTF_ERRIF, 0); +} + +canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed) { return this->set_bitrate_(can_speed, MCP_16MHZ); } + +canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clock) { + canbus::Error error = set_mode_(CANCTRL_REQOP_CONFIG); + if (error != canbus::ERROR_OK) { + return error; + } + + uint8_t set, cfg1, cfg2, cfg3; + set = 1; + switch (can_clock) { + case (MCP_8MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5KBPS + cfg1 = MCP_8MHZ_5KBPS_CFG1; + cfg2 = MCP_8MHZ_5KBPS_CFG2; + cfg3 = MCP_8MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10KBPS + cfg1 = MCP_8MHZ_10KBPS_CFG1; + cfg2 = MCP_8MHZ_10KBPS_CFG2; + cfg3 = MCP_8MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20KBPS + cfg1 = MCP_8MHZ_20KBPS_CFG1; + cfg2 = MCP_8MHZ_20KBPS_CFG2; + cfg3 = MCP_8MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_31K25BPS): // 31.25KBPS + cfg1 = MCP_8MHZ_31K25BPS_CFG1; + cfg2 = MCP_8MHZ_31K25BPS_CFG2; + cfg3 = MCP_8MHZ_31K25BPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333KBPS + cfg1 = MCP_8MHZ_33K3BPS_CFG1; + cfg2 = MCP_8MHZ_33K3BPS_CFG2; + cfg3 = MCP_8MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_8MHZ_40KBPS_CFG1; + cfg2 = MCP_8MHZ_40KBPS_CFG2; + cfg3 = MCP_8MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_8MHZ_50KBPS_CFG1; + cfg2 = MCP_8MHZ_50KBPS_CFG2; + cfg3 = MCP_8MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_8MHZ_80KBPS_CFG1; + cfg2 = MCP_8MHZ_80KBPS_CFG2; + cfg3 = MCP_8MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_8MHZ_100KBPS_CFG1; + cfg2 = MCP_8MHZ_100KBPS_CFG2; + cfg3 = MCP_8MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_8MHZ_125KBPS_CFG1; + cfg2 = MCP_8MHZ_125KBPS_CFG2; + cfg3 = MCP_8MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_8MHZ_200KBPS_CFG1; + cfg2 = MCP_8MHZ_200KBPS_CFG2; + cfg3 = MCP_8MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_8MHZ_250KBPS_CFG1; + cfg2 = MCP_8MHZ_250KBPS_CFG2; + cfg3 = MCP_8MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_8MHZ_500KBPS_CFG1; + cfg2 = MCP_8MHZ_500KBPS_CFG2; + cfg3 = MCP_8MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_8MHZ_1000KBPS_CFG1; + cfg2 = MCP_8MHZ_1000KBPS_CFG2; + cfg3 = MCP_8MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_16MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_16MHZ_5KBPS_CFG1; + cfg2 = MCP_16MHZ_5KBPS_CFG2; + cfg3 = MCP_16MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_16MHZ_10KBPS_CFG1; + cfg2 = MCP_16MHZ_10KBPS_CFG2; + cfg3 = MCP_16MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_16MHZ_20KBPS_CFG1; + cfg2 = MCP_16MHZ_20KBPS_CFG2; + cfg3 = MCP_16MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_16MHZ_33K3BPS_CFG1; + cfg2 = MCP_16MHZ_33K3BPS_CFG2; + cfg3 = MCP_16MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_16MHZ_40KBPS_CFG1; + cfg2 = MCP_16MHZ_40KBPS_CFG2; + cfg3 = MCP_16MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg2 = MCP_16MHZ_50KBPS_CFG2; + cfg3 = MCP_16MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_16MHZ_80KBPS_CFG1; + cfg2 = MCP_16MHZ_80KBPS_CFG2; + cfg3 = MCP_16MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_83K3BPS): // 83.333Kbps + cfg1 = MCP_16MHZ_83K3BPS_CFG1; + cfg2 = MCP_16MHZ_83K3BPS_CFG2; + cfg3 = MCP_16MHZ_83K3BPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_16MHZ_100KBPS_CFG1; + cfg2 = MCP_16MHZ_100KBPS_CFG2; + cfg3 = MCP_16MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_16MHZ_125KBPS_CFG1; + cfg2 = MCP_16MHZ_125KBPS_CFG2; + cfg3 = MCP_16MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_16MHZ_200KBPS_CFG1; + cfg2 = MCP_16MHZ_200KBPS_CFG2; + cfg3 = MCP_16MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_16MHZ_250KBPS_CFG1; + cfg2 = MCP_16MHZ_250KBPS_CFG2; + cfg3 = MCP_16MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_16MHZ_500KBPS_CFG1; + cfg2 = MCP_16MHZ_500KBPS_CFG2; + cfg3 = MCP_16MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_16MHZ_1000KBPS_CFG1; + cfg2 = MCP_16MHZ_1000KBPS_CFG2; + cfg3 = MCP_16MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_20MHZ): + switch (can_speed) { + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_20MHZ_33K3BPS_CFG1; + cfg2 = MCP_20MHZ_33K3BPS_CFG2; + cfg3 = MCP_20MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_20MHZ_40KBPS_CFG1; + cfg2 = MCP_20MHZ_40KBPS_CFG2; + cfg3 = MCP_20MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_20MHZ_50KBPS_CFG1; + cfg2 = MCP_20MHZ_50KBPS_CFG2; + cfg3 = MCP_20MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_20MHZ_80KBPS_CFG1; + cfg2 = MCP_20MHZ_80KBPS_CFG2; + cfg3 = MCP_20MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_83K3BPS): // 83.333Kbps + cfg1 = MCP_20MHZ_83K3BPS_CFG1; + cfg2 = MCP_20MHZ_83K3BPS_CFG2; + cfg3 = MCP_20MHZ_83K3BPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_20MHZ_100KBPS_CFG1; + cfg2 = MCP_20MHZ_100KBPS_CFG2; + cfg3 = MCP_20MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_20MHZ_125KBPS_CFG1; + cfg2 = MCP_20MHZ_125KBPS_CFG2; + cfg3 = MCP_20MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_20MHZ_200KBPS_CFG1; + cfg2 = MCP_20MHZ_200KBPS_CFG2; + cfg3 = MCP_20MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_20MHZ_250KBPS_CFG1; + cfg2 = MCP_20MHZ_250KBPS_CFG2; + cfg3 = MCP_20MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_20MHZ_500KBPS_CFG1; + cfg2 = MCP_20MHZ_500KBPS_CFG2; + cfg3 = MCP_20MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_20MHZ_1000KBPS_CFG1; + cfg2 = MCP_20MHZ_1000KBPS_CFG2; + cfg3 = MCP_20MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + default: + set = 0; + break; + } + + if (set) { + set_register_(MCP_CNF1, cfg1); // NOLINT + set_register_(MCP_CNF2, cfg2); // NOLINT + set_register_(MCP_CNF3, cfg3); // NOLINT + return canbus::ERROR_OK; + } else { + return canbus::ERROR_FAIL; + } +} +} // namespace mcp2515 +} // namespace esphome diff --git a/esphome/components/mcp2515/mcp2515.h b/esphome/components/mcp2515/mcp2515.h new file mode 100644 index 0000000000..3b9797a78a --- /dev/null +++ b/esphome/components/mcp2515/mcp2515.h @@ -0,0 +1,112 @@ +#pragma once + +#include "esphome/components/canbus/canbus.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" +#include "mcp2515_defs.h" + +namespace esphome { +namespace mcp2515 { +static const uint32_t SPI_CLOCK = 10000000; // 10MHz + +static const int N_TXBUFFERS = 3; +static const int N_RXBUFFERS = 2; +enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_8MHZ }; +enum MASK { MASK0, MASK1 }; +enum RXF { RXF0 = 0, RXF1 = 1, RXF2 = 2, RXF3 = 3, RXF4 = 4, RXF5 = 5 }; +enum RXBn { RXB0 = 0, RXB1 = 1 }; +enum TXBn { TXB0 = 0, TXB1 = 1, TXB2 = 2 }; + +enum CanClkOut { + CLKOUT_DISABLE = -1, + CLKOUT_DIV1 = 0x0, + CLKOUT_DIV2 = 0x1, + CLKOUT_DIV4 = 0x2, + CLKOUT_DIV8 = 0x3, +}; + +enum CANINTF : uint8_t { + CANINTF_RX0IF = 0x01, + CANINTF_RX1IF = 0x02, + CANINTF_TX0IF = 0x04, + CANINTF_TX1IF = 0x08, + CANINTF_TX2IF = 0x10, + CANINTF_ERRIF = 0x20, + CANINTF_WAKIF = 0x40, + CANINTF_MERRF = 0x80 +}; + +enum EFLG : uint8_t { + EFLG_RX1OVR = (1 << 7), + EFLG_RX0OVR = (1 << 6), + EFLG_TXBO = (1 << 5), + EFLG_TXEP = (1 << 4), + EFLG_RXEP = (1 << 3), + EFLG_TXWAR = (1 << 2), + EFLG_RXWAR = (1 << 1), + EFLG_EWARN = (1 << 0) +}; + +enum STAT : uint8_t { STAT_RX0IF = (1 << 0), STAT_RX1IF = (1 << 1) }; + +static const uint8_t STAT_RXIF_MASK = STAT_RX0IF | STAT_RX1IF; +static const uint8_t EFLG_ERRORMASK = EFLG_RX1OVR | EFLG_RX0OVR | EFLG_TXBO | EFLG_TXEP | EFLG_RXEP; + +class MCP2515 : public canbus::Canbus, + public spi::SPIDevice { + public: + MCP2515(){}; + void set_mcp_clock(CanClock clock) { this->mcp_clock_ = clock; }; + void set_mcp_mode(const CanctrlReqopMode mode) { this->mcp_mode_ = mode; } + static const struct TxBnRegs { + REGISTER CTRL; + REGISTER SIDH; + REGISTER DATA; + } TXB[N_TXBUFFERS]; + + static const struct RxBnRegs { + REGISTER CTRL; + REGISTER SIDH; + REGISTER DATA; + CANINTF CANINTF_RXnIF; + } RXB[N_RXBUFFERS]; + + protected: + CanClock mcp_clock_{MCP_8MHZ}; + CanctrlReqopMode mcp_mode_ = CANCTRL_REQOP_NORMAL; + bool setup_internal() override; + canbus::Error set_mode_(CanctrlReqopMode mode); + + uint8_t read_register_(REGISTER reg); + void read_registers_(REGISTER reg, uint8_t values[], uint8_t n); + void set_register_(REGISTER reg, uint8_t value); + void set_registers_(REGISTER reg, uint8_t values[], uint8_t n); + void modify_register_(REGISTER reg, uint8_t mask, uint8_t data); + + void prepare_id_(uint8_t *buffer, bool extended, uint32_t id); + canbus::Error reset_(); + canbus::Error set_clk_out_(CanClkOut divisor); + canbus::Error set_bitrate_(canbus::CanSpeed can_speed); + canbus::Error set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clock); + canbus::Error set_filter_mask_(MASK mask, bool extended, uint32_t ul_data); + canbus::Error set_filter_(RXF num, bool extended, uint32_t ul_data); + canbus::Error send_message_(TXBn txbn, struct canbus::CanFrame *frame); + canbus::Error send_message(struct canbus::CanFrame *frame) override; + canbus::Error read_message_(RXBn rxbn, struct canbus::CanFrame *frame); + canbus::Error read_message(struct canbus::CanFrame *frame) override; + bool check_receive_(); + bool check_error_(); + uint8_t get_error_flags_(); + void clear_rx_n_ovr_flags_(); + uint8_t get_int_(); + uint8_t get_int_mask_(); + void clear_int_(); + void clear_tx_int_(); + uint8_t get_status_(); + void clear_rx_n_ovr_(); + void clear_merr_(); + void clear_errif_(); +}; +} // namespace mcp2515 +} // namespace esphome diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h new file mode 100644 index 0000000000..454c760c6d --- /dev/null +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -0,0 +1,317 @@ +#pragma once + +namespace esphome { +namespace mcp2515 { + +static const uint8_t CANCTRL_REQOP = 0xE0; +static const uint8_t CANCTRL_ABAT = 0x10; +static const uint8_t CANCTRL_OSM = 0x08; +static const uint8_t CANCTRL_CLKEN = 0x04; +static const uint8_t CANCTRL_CLKPRE = 0x03; + +enum CanctrlReqopMode : uint8_t { + CANCTRL_REQOP_NORMAL = 0x00, + CANCTRL_REQOP_SLEEP = 0x20, + CANCTRL_REQOP_LOOPBACK = 0x40, + CANCTRL_REQOP_LISTENONLY = 0x60, + CANCTRL_REQOP_CONFIG = 0x80, + CANCTRL_REQOP_POWERUP = 0xE0 +}; + +enum TxbNCtrl : uint8_t { + TXB_ABTF = 0x40, + TXB_MLOA = 0x20, + TXB_TXERR = 0x10, + TXB_TXREQ = 0x08, + TXB_TXIE = 0x04, + TXB_TXP = 0x03 +}; + +enum INSTRUCTION : uint8_t { + INSTRUCTION_WRITE = 0x02, + INSTRUCTION_READ = 0x03, + INSTRUCTION_BITMOD = 0x05, + INSTRUCTION_LOAD_TX0 = 0x40, + INSTRUCTION_LOAD_TX1 = 0x42, + INSTRUCTION_LOAD_TX2 = 0x44, + INSTRUCTION_RTS_TX0 = 0x81, + INSTRUCTION_RTS_TX1 = 0x82, + INSTRUCTION_RTS_TX2 = 0x84, + INSTRUCTION_RTS_ALL = 0x87, + INSTRUCTION_READ_RX0 = 0x90, + INSTRUCTION_READ_RX1 = 0x94, + INSTRUCTION_READ_STATUS = 0xA0, + INSTRUCTION_RX_STATUS = 0xB0, + INSTRUCTION_RESET = 0xC0 +}; + +enum REGISTER : uint8_t { + MCP_RXF0SIDH = 0x00, + MCP_RXF0SIDL = 0x01, + MCP_RXF0EID8 = 0x02, + MCP_RXF0EID0 = 0x03, + MCP_RXF1SIDH = 0x04, + MCP_RXF1SIDL = 0x05, + MCP_RXF1EID8 = 0x06, + MCP_RXF1EID0 = 0x07, + MCP_RXF2SIDH = 0x08, + MCP_RXF2SIDL = 0x09, + MCP_RXF2EID8 = 0x0A, + MCP_RXF2EID0 = 0x0B, + MCP_CANSTAT = 0x0E, + MCP_CANCTRL = 0x0F, + MCP_RXF3SIDH = 0x10, + MCP_RXF3SIDL = 0x11, + MCP_RXF3EID8 = 0x12, + MCP_RXF3EID0 = 0x13, + MCP_RXF4SIDH = 0x14, + MCP_RXF4SIDL = 0x15, + MCP_RXF4EID8 = 0x16, + MCP_RXF4EID0 = 0x17, + MCP_RXF5SIDH = 0x18, + MCP_RXF5SIDL = 0x19, + MCP_RXF5EID8 = 0x1A, + MCP_RXF5EID0 = 0x1B, + MCP_TEC = 0x1C, + MCP_REC = 0x1D, + MCP_RXM0SIDH = 0x20, + MCP_RXM0SIDL = 0x21, + MCP_RXM0EID8 = 0x22, + MCP_RXM0EID0 = 0x23, + MCP_RXM1SIDH = 0x24, + MCP_RXM1SIDL = 0x25, + MCP_RXM1EID8 = 0x26, + MCP_RXM1EID0 = 0x27, + MCP_CNF3 = 0x28, + MCP_CNF2 = 0x29, + MCP_CNF1 = 0x2A, + MCP_CANINTE = 0x2B, + MCP_CANINTF = 0x2C, + MCP_EFLG = 0x2D, + MCP_TXB0CTRL = 0x30, + MCP_TXB0SIDH = 0x31, + MCP_TXB0SIDL = 0x32, + MCP_TXB0EID8 = 0x33, + MCP_TXB0EID0 = 0x34, + MCP_TXB0DLC = 0x35, + MCP_TXB0DATA = 0x36, + MCP_TXB1CTRL = 0x40, + MCP_TXB1SIDH = 0x41, + MCP_TXB1SIDL = 0x42, + MCP_TXB1EID8 = 0x43, + MCP_TXB1EID0 = 0x44, + MCP_TXB1DLC = 0x45, + MCP_TXB1DATA = 0x46, + MCP_TXB2CTRL = 0x50, + MCP_TXB2SIDH = 0x51, + MCP_TXB2SIDL = 0x52, + MCP_TXB2EID8 = 0x53, + MCP_TXB2EID0 = 0x54, + MCP_TXB2DLC = 0x55, + MCP_TXB2DATA = 0x56, + MCP_RXB0CTRL = 0x60, + MCP_RXB0SIDH = 0x61, + MCP_RXB0SIDL = 0x62, + MCP_RXB0EID8 = 0x63, + MCP_RXB0EID0 = 0x64, + MCP_RXB0DLC = 0x65, + MCP_RXB0DATA = 0x66, + MCP_RXB1CTRL = 0x70, + MCP_RXB1SIDH = 0x71, + MCP_RXB1SIDL = 0x72, + MCP_RXB1EID8 = 0x73, + MCP_RXB1EID0 = 0x74, + MCP_RXB1DLC = 0x75, + MCP_RXB1DATA = 0x76 +}; + +static const uint8_t CANSTAT_OPMOD = 0xE0; +static const uint8_t CANSTAT_ICOD = 0x0E; + +static const uint8_t CNF3_SOF = 0x80; + +static const uint8_t TXB_EXIDE_MASK = 0x08; +static const uint8_t DLC_MASK = 0x0F; +static const uint8_t RTR_MASK = 0x40; + +static const uint8_t RXB_CTRL_RXM_STD = 0x20; +static const uint8_t RXB_CTRL_RXM_EXT = 0x40; +static const uint8_t RXB_CTRL_RXM_STDEXT = 0x00; +static const uint8_t RXB_CTRL_RXM_MASK = 0x60; +static const uint8_t RXB_CTRL_RTR = 0x08; +static const uint8_t RXB_0_CTRL_BUKT = 0x04; + +static const uint8_t MCP_SIDH = 0; +static const uint8_t MCP_SIDL = 1; +static const uint8_t MCP_EID8 = 2; +static const uint8_t MCP_EID0 = 3; +static const uint8_t MCP_DLC = 4; +static const uint8_t MCP_DATA = 5; + +/* + * Speed 8M + */ +static const uint8_t MCP_8MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_1000KBPS_CFG2 = 0x80; +static const uint8_t MCP_8MHZ_1000KBPS_CFG3 = 0x80; + +static const uint8_t MCP_8MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_500KBPS_CFG2 = 0x90; +static const uint8_t MCP_8MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_8MHZ_250KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_250KBPS_CFG2 = 0xB1; +static const uint8_t MCP_8MHZ_250KBPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_200KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_200KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_200KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_125KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_125KBPS_CFG2 = 0xB1; +static const uint8_t MCP_8MHZ_125KBPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_100KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_100KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_100KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_80KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_80KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_50KBPS_CFG1 = 0x03; +static const uint8_t MCP_8MHZ_50KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_50KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_40KBPS_CFG1 = 0x03; +static const uint8_t MCP_8MHZ_40KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_33K3BPS_CFG1 = 0x47; +static const uint8_t MCP_8MHZ_33K3BPS_CFG2 = 0xE2; +static const uint8_t MCP_8MHZ_33K3BPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_31K25BPS_CFG1 = 0x07; +static const uint8_t MCP_8MHZ_31K25BPS_CFG2 = 0xA4; +static const uint8_t MCP_8MHZ_31K25BPS_CFG3 = 0x84; + +static const uint8_t MCP_8MHZ_20KBPS_CFG1 = 0x07; +static const uint8_t MCP_8MHZ_20KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_20KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_10KBPS_CFG1 = 0x0F; +static const uint8_t MCP_8MHZ_10KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_10KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_5KBPS_CFG1 = 0x1F; +static const uint8_t MCP_8MHZ_5KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_5KBPS_CFG3 = 0x87; + +/* + * speed 16M + */ +static const uint8_t MCP_16MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_16MHZ_1000KBPS_CFG2 = 0xD0; +static const uint8_t MCP_16MHZ_1000KBPS_CFG3 = 0x82; + +static const uint8_t MCP_16MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_16MHZ_500KBPS_CFG2 = 0xF0; +static const uint8_t MCP_16MHZ_500KBPS_CFG3 = 0x86; + +static const uint8_t MCP_16MHZ_250KBPS_CFG1 = 0x41; +static const uint8_t MCP_16MHZ_250KBPS_CFG2 = 0xF1; +static const uint8_t MCP_16MHZ_250KBPS_CFG3 = 0x85; + +static const uint8_t MCP_16MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_16MHZ_200KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_200KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_125KBPS_CFG2 = 0xF0; +static const uint8_t MCP_16MHZ_125KBPS_CFG3 = 0x86; + +static const uint8_t MCP_16MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_100KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_100KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_80KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_80KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_83K3BPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_83K3BPS_CFG2 = 0xBE; +static const uint8_t MCP_16MHZ_83K3BPS_CFG3 = 0x07; + +static const uint8_t MCP_16MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_16MHZ_50KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_50KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_40KBPS_CFG1 = 0x07; +static const uint8_t MCP_16MHZ_40KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_33K3BPS_CFG1 = 0x4E; +static const uint8_t MCP_16MHZ_33K3BPS_CFG2 = 0xF1; +static const uint8_t MCP_16MHZ_33K3BPS_CFG3 = 0x85; + +static const uint8_t MCP_16MHZ_20KBPS_CFG1 = 0x0F; +static const uint8_t MCP_16MHZ_20KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_20KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_10KBPS_CFG1 = 0x1F; +static const uint8_t MCP_16MHZ_10KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_10KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_5KBPS_CFG1 = 0x3F; +static const uint8_t MCP_16MHZ_5KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_5KBPS_CFG3 = 0x87; + +/* + * speed 20M + */ +static const uint8_t MCP_20MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_20MHZ_1000KBPS_CFG2 = 0xD9; +static const uint8_t MCP_20MHZ_1000KBPS_CFG3 = 0x82; + +static const uint8_t MCP_20MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_20MHZ_500KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_500KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_250KBPS_CFG1 = 0x41; +static const uint8_t MCP_20MHZ_250KBPS_CFG2 = 0xFB; +static const uint8_t MCP_20MHZ_250KBPS_CFG3 = 0x86; + +static const uint8_t MCP_20MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_20MHZ_200KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_200KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_20MHZ_125KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_125KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_100KBPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_100KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_100KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_83K3BPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_83K3BPS_CFG2 = 0xFE; +static const uint8_t MCP_20MHZ_83K3BPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_80KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_50KBPS_CFG1 = 0x09; +static const uint8_t MCP_20MHZ_50KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_50KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_20MHZ_40KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_33K3BPS_CFG1 = 0x0B; +static const uint8_t MCP_20MHZ_33K3BPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_33K3BPS_CFG3 = 0x87; + +} // namespace mcp2515 +} // namespace esphome diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py new file mode 100644 index 0000000000..431963acfd --- /dev/null +++ b/esphome/components/mcp3008/__init__.py @@ -0,0 +1,29 @@ +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"] +MULTI_CONF = True + +CONF_MCP3008 = "mcp3008" + +mcp3008_ns = cg.esphome_ns.namespace("mcp3008") +MCP3008 = mcp3008_ns.class_("MCP3008", cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MCP3008), + } +).extend(spi.spi_device_schema(cs_pin_required=True)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + if CORE.is_esp32: + cg.add_library("SPI", None) diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp new file mode 100644 index 0000000000..81abc4f012 --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -0,0 +1,59 @@ +#include "mcp3008.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *const TAG = "mcp3008"; + +float MCP3008::get_setup_priority() const { return setup_priority::HARDWARE; } + +void MCP3008::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp3008"); + this->spi_setup(); +} + +void MCP3008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008:"); + LOG_PIN(" CS Pin:", this->cs_); +} + +float MCP3008::read_data(uint8_t pin) { + uint8_t data_msb, data_lsb = 0; + + uint8_t command = ((0x01 << 7) | // start bit + ((pin & 0x07) << 4)); // channel number + + this->enable(); + this->transfer_byte(0x01); + + data_msb = this->transfer_byte(command) & 0x03; + data_lsb = this->transfer_byte(0x00); + + this->disable(); + + int data = data_msb << 8 | data_lsb; + + return data / 1023.0f; +} + +MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage) + : PollingComponent(1000), parent_(parent), pin_(pin), reference_voltage_(reference_voltage) {} + +float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } +void MCP3008Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); +} +float MCP3008Sensor::sample() { + float value_v = this->parent_->read_data(pin_); + value_v = (value_v * this->reference_voltage_); + return value_v; +} +void MCP3008Sensor::update() { this->publish_state(this->sample()); } + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h new file mode 100644 index 0000000000..5d8b823111 --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008 : public Component, + public spi::SPIDevice { // Running at the slowest max speed supported by the + // mcp3008. 2.7v = 75ksps + public: + MCP3008() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + float read_data(uint8_t pin); + + protected: +}; + +class MCP3008Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { + public: + MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage); + + void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + MCP3008 *parent_; + uint8_t pin_; + float reference_voltage_; +}; + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py new file mode 100644 index 0000000000..d4b9e979ce --- /dev/null +++ b/esphome/components/mcp3008/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import CONF_ID, CONF_NUMBER +from . import mcp3008_ns, MCP3008 + +AUTO_LOAD = ["voltage_sampler"] + +DEPENDENCIES = ["mcp3008"] + +MCP3008Sensor = mcp3008_ns.class_( + "MCP3008Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) +CONF_REFERENCE_VOLTAGE = "reference_voltage" +CONF_MCP3008_ID = "mcp3008_id" + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MCP3008Sensor), + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } +).extend(cv.polling_component_schema("1s")) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_MCP3008_ID]) + var = cg.new_Pvariable( + config[CONF_ID], + parent, + config[CONF_NUMBER], + config[CONF_REFERENCE_VOLTAGE], + ) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp4725/__init__.py b/esphome/components/mcp4725/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp4725/mcp4725.cpp b/esphome/components/mcp4725/mcp4725.cpp new file mode 100644 index 0000000000..2cb19282b6 --- /dev/null +++ b/esphome/components/mcp4725/mcp4725.cpp @@ -0,0 +1,35 @@ +#include "mcp4725.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp4725 { + +static const char *const TAG = "mcp4725"; + +void MCP4725::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP4725 (0x%02X)...", this->address_); + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } +} + +void MCP4725::dump_config() { + LOG_I2C_DEVICE(this); + + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with MCP4725 failed!"); + } +} + +// https://learn.sparkfun.com/tutorials/mcp4725-digital-to-analog-converter-hookup-guide?_ga=2.176055202.1402343014.1607953301-893095255.1606753886 +void MCP4725::write_state(float state) { + const uint16_t value = (uint16_t) round(state * (pow(2, MCP4725_RES) - 1)); + + this->write_byte_16(64, value << 4); +} + +} // namespace mcp4725 +} // namespace esphome diff --git a/esphome/components/mcp4725/mcp4725.h b/esphome/components/mcp4725/mcp4725.h new file mode 100644 index 0000000000..d6fa52e323 --- /dev/null +++ b/esphome/components/mcp4725/mcp4725.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +static const uint8_t MCP4725_ADDR = 0x60; +static const uint8_t MCP4725_RES = 12; + +namespace esphome { +namespace mcp4725 { +class MCP4725 : public Component, public output::FloatOutput, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void write_state(float state) override; + + protected: + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; +}; + +} // namespace mcp4725 +} // namespace esphome diff --git a/esphome/components/mcp4725/output.py b/esphome/components/mcp4725/output.py new file mode 100644 index 0000000000..8f8b80d927 --- /dev/null +++ b/esphome/components/mcp4725/output.py @@ -0,0 +1,26 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import output, i2c +from esphome.const import CONF_ID + +DEPENDENCIES = ["i2c"] + +mcp4725 = cg.esphome_ns.namespace("mcp4725") +MCP4725 = mcp4725.class_("MCP4725", output.FloatOutput, cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MCP4725), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x60)) +) + + +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 output.register_output(var, config) diff --git a/esphome/components/mcp9808/__init__.py b/esphome/components/mcp9808/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp new file mode 100644 index 0000000000..fca1331fc3 --- /dev/null +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -0,0 +1,81 @@ +#include "mcp9808.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp9808 { + +static const uint8_t MCP9808_REG_AMBIENT_TEMP = 0x05; +static const uint8_t MCP9808_REG_MANUF_ID = 0x06; +static const uint8_t MCP9808_REG_DEVICE_ID = 0x07; + +static const uint16_t MCP9808_MANUF_ID = 0x0054; +static const uint16_t MCP9808_DEV_ID = 0x0400; + +static const uint8_t MCP9808_AMBIENT_CLEAR_FLAGS = 0x1F; +static const uint8_t MCP9808_AMBIENT_CLEAR_SIGN = 0x0F; +static const uint8_t MCP9808_AMBIENT_TEMP_NEGATIVE = 0x10; + +static const char *const TAG = "mcp9808"; + +void MCP9808Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up %s...", this->name_.c_str()); + + 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 = 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; + } +} +void MCP9808Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "%s:", this->name_.c_str()); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with %s failed!", this->name_.c_str()); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} +void MCP9808Sensor::update() { + uint16_t raw_temp; + if (!this->read_byte_16(MCP9808_REG_AMBIENT_TEMP, &raw_temp)) { + this->status_set_warning(); + return; + } + if (raw_temp == 0xFFFF) { + this->status_set_warning(); + return; + } + + float temp = NAN; + uint8_t msb = (uint8_t)((raw_temp & 0xff00) >> 8); + uint8_t lsb = raw_temp & 0x00ff; + + msb = msb & MCP9808_AMBIENT_CLEAR_FLAGS; + + if ((msb & MCP9808_AMBIENT_TEMP_NEGATIVE) == MCP9808_AMBIENT_TEMP_NEGATIVE) { + msb = msb & MCP9808_AMBIENT_CLEAR_SIGN; + temp = (256 - ((uint16_t)(msb) *16 + lsb / 16.0f)) * -1; + } else { + temp = (uint16_t)(msb) *16 + lsb / 16.0f; + } + + if (std::isnan(temp)) { + this->status_set_warning(); + return; + } + + ESP_LOGD(TAG, "%s: Got temperature=%.4f°C", this->name_.c_str(), temp); + this->publish_state(temp); + this->status_clear_warning(); +} +float MCP9808Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace mcp9808 +} // namespace esphome diff --git a/esphome/components/mcp9808/mcp9808.h b/esphome/components/mcp9808/mcp9808.h new file mode 100644 index 0000000000..19aa3117c3 --- /dev/null +++ b/esphome/components/mcp9808/mcp9808.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp9808 { + +class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void update() override; +}; + +} // namespace mcp9808 +} // namespace esphome diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py new file mode 100644 index 0000000000..c7f6226e0b --- /dev/null +++ b/esphome/components/mcp9808/sensor.py @@ -0,0 +1,40 @@ +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_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@k7hpn"] +DEPENDENCIES = ["i2c"] + +mcp9808_ns = cg.esphome_ns.namespace("mcp9808") +MCP9808Sensor = mcp9808_ns.class_( + "MCP9808Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MCP9808Sensor), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x18)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/md5/__init__.py b/esphome/components/md5/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/md5/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp new file mode 100644 index 0000000000..0528a87d0e --- /dev/null +++ b/esphome/components/md5/md5.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "md5.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace md5 { + +void MD5Digest::init() { + memset(this->digest_, 0, 16); + MD5Init(&this->ctx_); +} + +void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, data, len); } + +void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); } + +void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16); } + +void MD5Digest::get_hex(char *output) { + for (size_t i = 0; i < 16; i++) { + sprintf(output + i * 2, "%02x", this->digest_[i]); + } +} + +bool MD5Digest::equals_bytes(const uint8_t *expected) { + for (size_t i = 0; i < 16; i++) { + if (expected[i] != this->digest_[i]) { + return false; + } + } + return true; +} + +bool MD5Digest::equals_hex(const char *expected) { + uint8_t parsed[16]; + if (!parse_hex(expected, parsed, 16)) + return false; + return equals_bytes(parsed); +} + +} // namespace md5 +} // namespace esphome diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h new file mode 100644 index 0000000000..1c15c9e57d --- /dev/null +++ b/esphome/components/md5/md5.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP_IDF +#include "esp32/rom/md5_hash.h" +#define MD5_CTX_TYPE MD5Context +#endif + +#if defined(USE_ARDUINO) && defined(USE_ESP32) +#include "rom/md5_hash.h" +#define MD5_CTX_TYPE MD5Context +#endif + +#if defined(USE_ARDUINO) && defined(USE_ESP8266) +#include +#define MD5_CTX_TYPE md5_context_t +#endif + +namespace esphome { +namespace md5 { + +class MD5Digest { + public: + MD5Digest() = default; + ~MD5Digest() = default; + + /// Initialize a new MD5 digest computation. + void init(); + + /// Add bytes of data for the digest. + void add(const uint8_t *data, size_t len); + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the digest, based on the provided data. + void calculate(); + + /// Retrieve the MD5 digest as bytes. + /// The output must be able to hold 16 bytes or more. + void get_bytes(uint8_t *output); + + /// Retrieve the MD5 digest as hex characters. + /// The output must be able to hold 32 bytes or more. + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (16 bytes). + bool equals_bytes(const uint8_t *expected); + + /// Compare the digest against a provided hex-encoded digest (32 bytes). + bool equals_hex(const char *expected); + + protected: + MD5_CTX_TYPE ctx_{}; + uint8_t digest_[16]; +}; + +} // namespace md5 +} // namespace esphome 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..915c640b06 --- /dev/null +++ b/esphome/components/mdns/mdns_component.cpp @@ -0,0 +1,96 @@ +#include "mdns_component.h" +#include "esphome/core/defines.h" +#include "esphome/core/version.h" +#include "esphome/core/application.h" +#include "esphome/core/log.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 { + +static const char *const TAG = "mdns"; + +#ifndef WEBSERVER_PORT +#define WEBSERVER_PORT 80 // NOLINT +#endif + +void MDNSComponent::compile_records_() { + this->hostname_ = App.get_name(); + + this->services_.clear(); +#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 + + this->services_.push_back(service); + } +#endif // USE_API + +#ifdef USE_PROMETHEUS + { + MDNSService service{}; + service.service_type = "_prometheus-http"; + service.proto = "_tcp"; + service.port = WEBSERVER_PORT; + this->services_.push_back(service); + } +#endif + + if (this->services_.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}); + this->services_.push_back(service); + } +} + +void MDNSComponent::dump_config() { + ESP_LOGCONFIG(TAG, "mDNS:"); + ESP_LOGCONFIG(TAG, " Hostname: %s", this->hostname_.c_str()); + ESP_LOGV(TAG, " Services:"); + for (const auto &service : this->services_) { + ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), service.port); + for (const auto &record : service.txt_records) { + ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(), record.value.c_str()); + } + } +} + +} // 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..45614d509a --- /dev/null +++ b/esphome/components/mdns/mdns_component.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" + +namespace esphome { +namespace mdns { + +struct MDNSTXTRecord { + std::string key; + std::string value; +}; + +struct MDNSService { + // service name _including_ underscore character prefix + // as defined in RFC6763 Section 7 + std::string service_type; + // second label indicating protocol _including_ underscore character prefix + // as defined in RFC6763 Section 7, like "_tcp" or "_udp" + std::string proto; + uint16_t port; + std::vector txt_records; +}; + +class MDNSComponent : public Component { + public: + void setup() override; + void dump_config() 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 services_{}; + std::string hostname_; + void compile_records_(); +}; + +} // 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..6a66beef92 --- /dev/null +++ b/esphome/components/mdns/mdns_esp32_arduino.cpp @@ -0,0 +1,26 @@ +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "mdns_component.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace mdns { + +void MDNSComponent::setup() { + this->compile_records_(); + + MDNS.begin(this->hostname_.c_str()); + + for (const auto &service : this->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..ff305f907a --- /dev/null +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -0,0 +1,43 @@ +#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 { + +void MDNSComponent::setup() { + this->compile_records_(); + + network::IPAddress addr = network::get_ip_address(); + MDNS.begin(this->hostname_.c_str(), (uint32_t) addr); + + for (const auto &service : this->services_) { + // Strip the leading underscore from the proto and service_type. While it is + // part of the wire protocol to have an underscore, and for example ESP-IDF + // expects the underscore to be there, the ESP8266 implementation always adds + // the underscore itself. + auto proto = service.proto.c_str(); + while (*proto == '_') { + proto++; + } + auto service_type = service.service_type.c_str(); + while (*service_type == '_') { + service_type++; + } + MDNS.addService(service_type, proto, service.port); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str()); + } + } +} + +void MDNSComponent::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..40d9f1d5f3 --- /dev/null +++ b/esphome/components/mdns/mdns_esp_idf.cpp @@ -0,0 +1,53 @@ +#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() { + this->compile_records_(); + + 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(this->hostname_.c_str()); + mdns_instance_name_set(this->hostname_.c_str()); + + for (const auto &service : this->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/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index 8e28d04dea..db3ad50851 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace mhz19 { -static const char *TAG = "mhz19"; +static const char *const TAG = "mhz19"; static const uint8_t MHZ19_REQUEST_LENGTH = 8; static const uint8_t MHZ19_RESPONSE_LENGTH = 9; static const uint8_t MHZ19_COMMAND_GET_PPM[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00}; diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 2201fc87f0..151351be4c 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -37,6 +37,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } protected: @@ -46,6 +47,7 @@ template class MHZ19CalibrateZeroAction : public Action { template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_enable(); } protected: @@ -55,6 +57,7 @@ template class MHZ19ABCEnableAction : public Action { template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_disable(); } protected: diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index bdcecf12cb..0081f42952 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -3,55 +3,88 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import sensor, uart -from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ - UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER +from esphome.const import ( + CONF_CO2, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_MOLECULE_CO2, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, + UNIT_CELSIUS, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -CONF_AUTOMATIC_BASELINE_CALIBRATION = 'automatic_baseline_calibration' +CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration" -mhz19_ns = cg.esphome_ns.namespace('mhz19') -MHZ19Component = mhz19_ns.class_('MHZ19Component', cg.PollingComponent, uart.UARTDevice) -MHZ19CalibrateZeroAction = mhz19_ns.class_('MHZ19CalibrateZeroAction', automation.Action) -MHZ19ABCEnableAction = mhz19_ns.class_('MHZ19ABCEnableAction', automation.Action) -MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Action) +mhz19_ns = cg.esphome_ns.namespace("mhz19") +MHZ19Component = mhz19_ns.class_("MHZ19Component", cg.PollingComponent, uart.UARTDevice) +MHZ19CalibrateZeroAction = mhz19_ns.class_( + "MHZ19CalibrateZeroAction", automation.Action +) +MHZ19ABCEnableAction = mhz19_ns.class_("MHZ19ABCEnableAction", automation.Action) +MHZ19ABCDisableAction = mhz19_ns.class_("MHZ19ABCDisableAction", automation.Action) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MHZ19Component), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0), - cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, -}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MHZ19Component), + cv.Required(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, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) if CONF_CO2 in config: - sens = yield sensor.new_sensor(config[CONF_CO2]) + sens = await sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_AUTOMATIC_BASELINE_CALIBRATION in config: cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION])) -CALIBRATION_ACTION_SCHEMA = maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(MHZ19Component), -}) +CALIBRATION_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(MHZ19Component), + } +) -@automation.register_action('mhz19.calibrate_zero', MHZ19CalibrateZeroAction, - CALIBRATION_ACTION_SCHEMA) -@automation.register_action('mhz19.abc_enable', MHZ19ABCEnableAction, - CALIBRATION_ACTION_SCHEMA) -@automation.register_action('mhz19.abc_disable', MHZ19ABCDisableAction, - CALIBRATION_ACTION_SCHEMA) -def mhz19_calibration_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) +@automation.register_action( + "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA +) +@automation.register_action( + "mhz19.abc_enable", MHZ19ABCEnableAction, CALIBRATION_ACTION_SCHEMA +) +@automation.register_action( + "mhz19.abc_disable", MHZ19ABCDisableAction, CALIBRATION_ACTION_SCHEMA +) +async def mhz19_calibration_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) 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/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp new file mode 100644 index 0000000000..2837713c35 --- /dev/null +++ b/esphome/components/midea/ac_adapter.cpp @@ -0,0 +1,179 @@ +#ifdef USE_ARDUINO + +#include "esphome/core/log.h" +#include "ac_adapter.h" + +namespace esphome { +namespace midea { +namespace ac { + +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 ac +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h new file mode 100644 index 0000000000..c17894ae31 --- /dev/null +++ b/esphome/components/midea/ac_adapter.h @@ -0,0 +1,51 @@ +#pragma once + +#ifdef USE_ARDUINO + +// MideaUART +#include + +#include "esphome/components/climate/climate_traits.h" +#include "air_conditioner.h" + +namespace esphome { +namespace midea { +namespace ac { + +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 ac +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h new file mode 100644 index 0000000000..d4ed2e7168 --- /dev/null +++ b/esphome/components/midea/ac_automations.h @@ -0,0 +1,63 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/automation.h" +#include "air_conditioner.h" + +namespace esphome { +namespace midea { +namespace ac { + +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 ac +} // 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..dd48f640a2 --- /dev/null +++ b/esphome/components/midea/air_conditioner.cpp @@ -0,0 +1,155 @@ +#ifdef USE_ARDUINO + +#include "esphome/core/log.h" +#include "air_conditioner.h" +#include "ac_adapter.h" + +namespace esphome { +namespace midea { +namespace ac { + +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->transmitter_.transmit(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->transmitter_.transmit(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->transmitter_.transmit(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif + } +} + +} // namespace ac +} // 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..a6023b78bb --- /dev/null +++ b/esphome/components/midea/air_conditioner.h @@ -0,0 +1,65 @@ +#pragma once + +#ifdef USE_ARDUINO + +// MideaUART +#include + +#include "appliance_base.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace midea { +namespace ac { + +using sensor::Sensor; +using climate::ClimateCall; +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; + +class AirConditioner : public ApplianceBase, public climate::Climate { + 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); } + 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; } + + protected: + void control(const ClimateCall &call) override; + ClimateTraits traits() override; + std::set supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; + Sensor *outdoor_sensor_{nullptr}; + Sensor *humidity_sensor_{nullptr}; + Sensor *power_sensor_{nullptr}; +}; + +} // namespace ac +} // 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..060cbd996b --- /dev/null +++ b/esphome/components/midea/appliance_base.h @@ -0,0 +1,102 @@ +#pragma once + +#ifdef USE_ARDUINO + +// MideaUART +#include +#include + +// Include global defines +#include "esphome/core/defines.h" + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/climate/climate.h" +#include "ir_transmitter.h" + +namespace esphome { +namespace midea { + +/* Stream from UART component */ +class UARTStream : public Stream { + public: + void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; } + + /* Stream interface implementation */ + + int available() override { return this->uart_->available(); } + int read() override { + uint8_t data; + this->uart_->read_byte(&data); + return data; + } + int peek() override { + uint8_t data; + this->uart_->peek_byte(&data); + return data; + } + size_t write(uint8_t data) override { + this->uart_->write_byte(data); + return 1; + } + size_t write(const uint8_t *data, size_t size) override { + this->uart_->write_array(data, size); + return size; + } + void flush() override { this->uart_->flush(); } + + protected: + uart::UARTComponent *uart_; +}; + +template class ApplianceBase : public Component { + static_assert(std::is_base_of::value, + "T must derive from dudanov::midea::ApplianceBase class"); + + public: + ApplianceBase() { + this->base_.setStream(&this->stream_); + 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); + }); + } + +#ifdef USE_REMOTE_TRANSMITTER + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_.set_transmitter(transmitter); } +#endif + + /* UART communication */ + + void set_uart_parent(uart::UARTComponent *parent) { this->stream_.set_uart(parent); } + 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); } + + /* Component methods */ + + void setup() override { this->base_.setup(); } + void loop() override { this->base_.loop(); } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + bool can_proceed() override { + return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; + } + + void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } + void set_autoconf(bool value) { this->base_.setAutoconf(value); } + virtual void on_status_change() = 0; + + protected: + T base_; + UARTStream stream_; +#ifdef USE_REMOTE_TRANSMITTER + IrTransmitter transmitter_; +#endif +}; + +} // 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..46c0019efa --- /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_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac") +AirConditioner = midea_ac_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = midea_ac_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_ac_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = midea_ac_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = midea_ac_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = midea_ac_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/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h new file mode 100644 index 0000000000..34a9f8498e --- /dev/null +++ b/esphome/components/midea/ir_transmitter.h @@ -0,0 +1,60 @@ +#pragma once + +#ifdef USE_ARDUINO +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" + +namespace esphome { +namespace midea { + +using remote_base::RemoteTransmitterBase; +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}) {} +}; + +class IrTransmitter { + public: + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } + void transmit(IrData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); + } + + protected: + RemoteTransmitterBase *transmitter_{nullptr}; +}; + +} // namespace midea +} // namespace esphome + +#endif +#endif // USE_ARDUINO diff --git a/esphome/components/midea_ac/__init__.py b/esphome/components/midea_ac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py new file mode 100644 index 0000000000..f336f84787 --- /dev/null +++ b/esphome/components/midea_ac/climate.py @@ -0,0 +1,3 @@ +import esphome.config_validation as cv + +CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9") diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py index 933e53baf0..0df17a5d16 100644 --- a/esphome/components/mitsubishi/climate.py +++ b/esphome/components/mitsubishi/climate.py @@ -3,16 +3,19 @@ import esphome.config_validation as cv from esphome.components import climate_ir from esphome.const import CONF_ID -AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ["@RubyBailey"] +AUTO_LOAD = ["climate_ir"] -mitsubishi_ns = cg.esphome_ns.namespace('mitsubishi') -MitsubishiClimate = mitsubishi_ns.class_('MitsubishiClimate', climate_ir.ClimateIR) +mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi") +MitsubishiClimate = mitsubishi_ns.class_("MitsubishiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(MitsubishiClimate), -}) +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MitsubishiClimate), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield climate_ir.register_climate_ir(var, config) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index b70aa6d394..43397770d1 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace mitsubishi { -static const char *TAG = "mitsubishi.climate"; +static const char *const TAG = "mitsubishi.climate"; const uint32_t MITSUBISHI_OFF = 0x00; @@ -23,8 +23,8 @@ const uint16_t MITSUBISHI_HEADER_SPACE = 1700; const uint16_t MITSUBISHI_MIN_GAP = 17500; void MitsubishiClimate::transmit_state() { - uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x48, 0x00, 0x30, - 0x58, 0x61, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00}; + uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x30, + 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; switch (this->mode) { case climate::CLIMATE_MODE_COOL: @@ -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 cada835905..254322d097 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,28 +1,52 @@ 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_ID, CONF_ADDRESS -from esphome.core import coroutine +from esphome.const import ( + CONF_FLOW_CONTROL_PIN, + CONF_ID, + CONF_ADDRESS, +) +from esphome import pins -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -modbus_ns = cg.esphome_ns.namespace('modbus') -Modbus = modbus_ns.class_('Modbus', cg.Component, uart.UARTDevice) -ModbusDevice = modbus_ns.class_('ModbusDevice') +modbus_ns = cg.esphome_ns.namespace("modbus") +Modbus = modbus_ns.class_("Modbus", cg.Component, uart.UARTDevice) +ModbusDevice = modbus_ns.class_("ModbusDevice") MULTI_CONF = True -CONF_MODBUS_ID = 'modbus_id' -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(Modbus), -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) +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) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): cg.add_global(modbus_ns.using) var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await uart.register_uart_device(var, config) + + if CONF_FLOW_CONTROL_PIN in config: + pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) + cg.add(var.set_flow_control_pin(pin)) + + 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): @@ -36,9 +60,8 @@ def modbus_device_schema(default_address): return cv.Schema(schema) -@coroutine -def register_modbus_device(var, config): - parent = yield cg.get_variable(config[CONF_MODBUS_ID]) +async def register_modbus_device(var, config): + parent = await cg.get_variable(config[CONF_MODBUS_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_address(config[CONF_ADDRESS])) cg.add(parent.register_device(var)) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 74d0c40986..9ee3137be9 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -1,17 +1,28 @@ #include "modbus.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace modbus { -static const char *TAG = "modbus"; +static const char *const TAG = "modbus"; +void Modbus::setup() { + if (this->flow_control_pin_ != nullptr) { + this->flow_control_pin_->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; @@ -44,48 +55,70 @@ 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); - + 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_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); + if (waiting_for_response != 0) { + device->on_modbus_error(function_code & 0x7F, raw[2]); + } else { + // Ignore modbus exception not related to a pending command + ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); + } + } else { + device->on_modbus_data(data); + } found = true; } } + waiting_for_response = 0; + 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 @@ -94,25 +127,83 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:"); - this->check_uart_settings(9600, 2); + 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; - this->write_array(frame, 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; + + // Only check max number of registers for standard function codes + // Some devices use non standard codes like 0x43 + if (number_of_entities > MAX_VALUES && function_code <= 0x10) { + 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(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", format_hex_pretty(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]; + ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty(payload).c_str()); + last_send_ = millis(); } } // namespace modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index b75de147b1..400e29e08b 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -12,6 +12,8 @@ class Modbus : public uart::UARTDevice, public Component { public: Modbus() = default; + void setup() override; + void loop() override; void dump_config() override; @@ -20,25 +22,39 @@ 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: - bool parse_modbus_byte_(uint8_t byte); + 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_; }; +uint16_t crc16(const uint8_t *data, uint8_t len); + class ModbusDevice { public: 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..b927faf9a7 --- /dev/null +++ b/esphome/components/modbus_controller/__init__.py @@ -0,0 +1,227 @@ +import binascii +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import modbus +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_LAMBDA, CONF_OFFSET +from esphome.cpp_helpers import logging +from .const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_COMMAND_THROTTLE, + CONF_CUSTOM_COMMAND, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, + CONF_RESPONSE_SIZE, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, +) + +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 = { + "custom": ModbusRegisterType.CUSTOM, + "coil": ModbusRegisterType.COIL, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, + "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, +} + +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, +} + +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)) +) + + +ModbusItemBaseSchema = cv.Schema( + { + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Optional(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_CUSTOM_COMMAND): cv.ensure_list(cv.hex_uint8_t), + cv.Exclusive( + CONF_OFFSET, + "offset", + f"{CONF_OFFSET} and {CONF_BYTE_OFFSET} can't be used together", + ): cv.positive_int, + cv.Exclusive( + CONF_BYTE_OFFSET, + "offset", + f"{CONF_OFFSET} and {CONF_BYTE_OFFSET} can't be used together", + ): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): 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, + cv.Optional(CONF_RESPONSE_SIZE, default=0): cv.positive_int, + }, +) + + +def validate_modbus_register(config): + if CONF_CUSTOM_COMMAND not in config and CONF_ADDRESS not in config: + raise cv.Invalid( + f" {CONF_ADDRESS} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used" + ) + if CONF_CUSTOM_COMMAND in config and CONF_REGISTER_TYPE in config: + raise cv.Invalid( + f"can't use '{CONF_REGISTER_TYPE}:' together with '{CONF_CUSTOM_COMMAND}:'", + ) + + if CONF_CUSTOM_COMMAND not in config and CONF_REGISTER_TYPE not in config: + raise cv.Invalid( + f" {CONF_REGISTER_TYPE} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used" + ) + return config + + +def modbus_calc_properties(config): + byte_offset = 0 + reg_count = 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] + if CONF_REGISTER_COUNT in config: + reg_count = config[CONF_REGISTER_COUNT] + if CONF_VALUE_TYPE in config: + value_type = config[CONF_VALUE_TYPE] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] + if CONF_CUSTOM_COMMAND in config: + if CONF_ADDRESS not in config: + # generate a unique modbus address using the hash of the name + # CONF_NAME set even if only CONF_ID is used. + # a modbus register address is required to add the item to sensormap + value = config[CONF_NAME] + if isinstance(value, str): + value = value.encode() + config[CONF_ADDRESS] = binascii.crc_hqx(value, 0) + config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM + config[CONF_FORCE_NEW_RANGE] = True + return byte_offset, reg_count + + +async def add_modbus_base_properties( + var, config, sensor_type, lamdba_param_type=cg.float_, lamdba_return_type=float +): + if CONF_CUSTOM_COMMAND in config: + cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND])) + + if config[CONF_RESPONSE_SIZE] > 0: + cg.add(var.set_register_size(config[CONF_RESPONSE_SIZE])) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (sensor_type.operator("ptr"), "item"), + (lamdba_param_type, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(lamdba_return_type), + ) + cg.add(var.set_template(template_)) + + +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] 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..99d56fed67 --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -0,0 +1,60 @@ +from esphome.components import binary_sensor +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import CONF_ADDRESS, CONF_ID +from .. import ( + add_modbus_base_properties, + modbus_controller_ns, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + CONF_BITMASK, + 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.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.GenerateID(): cv.declare_id(ModbusBinarySensor), + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, _ = modbus_calc_properties(config) + 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)) + await add_modbus_base_properties(var, config, ModbusBinarySensor, cg.float_, bool) 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..c3eb3d4411 --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -0,0 +1,38 @@ +#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: + 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..baf72efb94 --- /dev/null +++ b/esphome/components/modbus_controller/const.py @@ -0,0 +1,15 @@ +CONF_BITMASK = "bitmask" +CONF_BYTE_OFFSET = "byte_offset" +CONF_COMMAND_THROTTLE = "command_throttle" +CONF_CUSTOM_COMMAND = "custom_command" +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_USE_WRITE_MULTIPLE = "use_write_multiple" +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..8b96c20691 --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -0,0 +1,586 @@ +#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(); + // remove from queue if no handler is defined or command was sent too often + if (!command->on_data_func || command->send_countdown < 1) { + ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X countdown=%d removed from queue after send", + this->address_, command->register_address, command->send_countdown); + 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(); + } +} + +std::map::iterator ModbusController::find_register_(ModbusRegisterType register_type, + uint16_t 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, "No matching range for sensor found - start_address : 0x%X", start_address); + } else { + auto map_it = sensormap_.find(vec_it->first_sensorkey); + if (map_it == sensormap_.end()) { + ESP_LOGE(TAG, "No sensor found in at start_address : 0x%X (0x%llX)", start_address, vec_it->first_sensorkey); + } else { + return sensormap_.find(vec_it->first_sensorkey); + } + } + // not found + return std::end(sensormap_); +} +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 map_it = find_register_(register_type, start_address); + // 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) { + // if a custom command is used the user supplied custom_data is only available in the SensorItem. + if (r.register_type == ModbusRegisterType::CUSTOM) { + auto it = this->find_register_(r.register_type, r.start_address); + if (it != sensormap_.end()) { + auto command_item = ModbusCommandItem::create_custom_command( + this, it->second->custom_data, + [this](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data); + }); + command_item.register_address = it->second->start_address; + command_item.register_count = it->second->register_count; + command_item.function_code = ModbusFunctionCode::CUSTOM; + queue_command(command_item); + } + } else { + queue_command(ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count)); + } + 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); + send_countdown--; + 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; + case SensorValueType::RAW: + result = NAN; + 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..f4948e6ff9 --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -0,0 +1,439 @@ +#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: + 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 (size_t 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; + +class SensorItem { + public: + virtual void parse_and_publish(const std::vector &data) = 0; + + void set_custom_data(const std::vector &data) { custom_data = data; } + uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } + size_t virtual get_register_size() const { + if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) + return 1; + else // if CONF_RESPONSE_BYTES is used override the default + return response_bytes > 0 ? response_bytes : register_count * 2; + } + // Override register size for modbus devices not using 1 register for one dword + void set_register_size(uint8_t register_size) { response_bytes = register_size; } + ModbusRegisterType register_type; + SensorValueType sensor_value_type; + uint16_t start_address; + uint32_t bitmask; + uint8_t offset; + uint8_t register_count; + uint8_t response_bytes{0}; + uint8_t skip_updates; + std::vector custom_data{}; + bool force_new_range{false}; +}; + +class ModbusCommandItem { + public: + static const size_t MAX_PAYLOAD_BYTES = 240; + static const uint8_t MAX_SEND_REPEATS = 5; + ModbusController *modbusdevice; + uint16_t register_address; + uint16_t register_count; + ModbusFunctionCode function_code; + ModbusRegisterType register_type; + std::function &data)> + on_data_func; + std::vector payload = {}; + bool send(); + // wrong commands (esp. custom commands) can block the send queue + // limit the number of repeats + uint8_t send_countdown{MAX_SEND_REPEATS}; + /// 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_(); + // find register in sensormap. Returns iterator with all registers having the same start address + std::map::iterator find_register_(ModbusRegisterType register_type, uint16_t start_address); + /// 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..4ad6601fee --- /dev/null +++ b/esphome/components/modbus_controller/number/__init__.py @@ -0,0 +1,121 @@ +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_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MULTIPLY, + CONF_STEP, +) + +from .. import ( + add_modbus_base_properties, + modbus_controller_ns, + modbus_calc_properties, + ModbusItemBaseSchema, + SensorItem, + SENSOR_VALUE_TYPE, +) + +from ..const import ( + CONF_BITMASK, + CONF_CUSTOM_COMMAND, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusNumber = modbus_controller_ns.class_( + "ModbusNumber", cg.Component, number.Number, SensorItem +) + + +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 + + +def validate_modbus_number(config): + if CONF_CUSTOM_COMMAND not in config and CONF_ADDRESS not in config: + raise cv.Invalid( + f" {CONF_ADDRESS} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used" + ) + return config + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema) + .extend( + { + cv.GenerateID(): cv.declare_id(ModbusNumber), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + # 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_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ) + .extend(cv.polling_component_schema("60s")), + validate_min_max, + validate_modbus_number, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + 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)) + await add_modbus_base_properties(var, config, ModbusNumber) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + 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..5e977f5df4 --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -0,0 +1,77 @@ +#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) { + float result = payload_to_float(data, *this) / multiply_by_; + + // 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) { + std::vector data; + float write_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"); + write_value = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } else { + write_value = multiply_by_ * write_value; + } + + // lambda didn't set payload + if (data.empty()) { + data = float_to_payload(write_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, write_value); + + // Create and send the write command + ModbusCommandItem write_cmd; + if (this->register_count == 1 && !this->use_write_multiple_) { + write_cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, data[0]); + } else { + 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..c678cd00cc --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -0,0 +1,50 @@ +#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; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + + protected: + void control(float value) override; + optional transform_func_; + optional write_transform_func_; + ModbusController *parent_; + float multiply_by_{1.0}; + bool use_write_multiple_{false}; +}; + +} // 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..a26d05a18b --- /dev/null +++ b/esphome/components/modbus_controller/output/__init__.py @@ -0,0 +1,71 @@ +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, +) + +from .. import ( + modbus_controller_ns, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, +) + +from ..const import ( + CONF_MODBUS_CONTROLLER_ID, + CONF_USE_WRITE_MULTIPLE, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +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(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusOutput), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + config[CONF_VALUE_TYPE], + reg_count, + ) + 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_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + 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..4c2e5775b9 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -0,0 +1,62 @@ +#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) { + 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 + // Create and send the write command + ModbusCommandItem write_cmd; + if (this->register_count == 1 && !this->use_write_multiple_) { + write_cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, + 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..78d3474ad6 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -0,0 +1,48 @@ +#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, int register_count) + : output::FloatOutput(), Component() { + this->register_type = ModbusRegisterType::HOLDING; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->register_count = register_count; + 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; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + + protected: + void write_state(float value) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_; + float multiply_by_{1.0}; + bool use_write_multiple_; +}; + +} // 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..da7b8928b4 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -0,0 +1,68 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import CONF_ID, CONF_ADDRESS +from .. import ( + add_modbus_base_properties, + modbus_controller_ns, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, + MODBUS_REGISTER_TYPE, + SENSOR_VALUE_TYPE, +) +from ..const import ( + CONF_BITMASK, + 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 +) + +CONFIG_SCHEMA = cv.All( + sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.GenerateID(): cv.declare_id(ModbusSensor), + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + value_type = config[CONF_VALUE_TYPE] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + 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)) + await add_modbus_base_properties(var, config, ModbusSensor) 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..a21fd91032 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -0,0 +1,31 @@ + +#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) { + 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..37ea9d0dd0 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -0,0 +1,36 @@ +#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..9858d45617 --- /dev/null +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -0,0 +1,76 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg + + +from esphome.const import CONF_ID, CONF_ADDRESS +from .. import ( + add_modbus_base_properties, + modbus_controller_ns, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + CONF_BITMASK, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_USE_WRITE_MULTIPLE, + CONF_WRITE_LAMBDA, +) + +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.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.GenerateID(): cv.declare_id(ModbusSwitch), + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, _ = modbus_calc_properties(config) + 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(var.set_parent(paren)) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + cg.add(paren.add_sensor_item(var)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusSwitch.operator("ptr"), "item"), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), + ], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_write_template(template_)) + await add_modbus_base_properties(var, config, ModbusSwitch, bool, bool) 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..ca8d0be720 --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -0,0 +1,98 @@ + +#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; + std::vector data; + // 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, state, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + state = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } + if (!data.empty()) { + ESP_LOGV(TAG, "Modbus Switch write raw: %s", format_hex_pretty(data).c_str()); + cmd = ModbusCommandItem::create_custom_command( + this->parent_, data, + [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->parent_->on_write_register_response(cmd.register_type, this->start_address, data); + }); + } else { + 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); + if (this->register_type == ModbusRegisterType::COIL) { + // offset for coil and discrete inputs is the coil/register number not bytes + if (this->use_write_multiple_) { + std::vector states{state}; + cmd = ModbusCommandItem::create_write_multiple_coils(parent_, this->start_address + this->offset, states); + } else { + cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); + } + } else { + // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 + if (this->use_write_multiple_) { + std::vector bool_states(1, state ? (0xFFFF & this->bitmask) : 0); + cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, 1, + bool_states); + } else { + cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, + state ? 0xFFFF & this->bitmask : 0u); + } + } + } + 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..5ac2af01a1 --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -0,0 +1,49 @@ +#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 &)>; + using write_transform_func_t = std::function(ModbusSwitch *, bool, std::vector &)>; + void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + + protected: + ModbusController *parent_; + bool use_write_multiple_; + optional publish_transform_func_{nullopt}; + optional write_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..5cc85af5bc --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -0,0 +1,83 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg + + +from esphome.const import CONF_ADDRESS, CONF_ID +from .. import ( + add_modbus_base_properties, + modbus_controller_ns, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + 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.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.GenerateID(): cv.declare_id(ModbusTextSensor), + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int, + cv.Optional(CONF_RAW_ENCODE, default="NONE"): cv.enum(RAW_ENCODING), + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + 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)) + await add_modbus_base_properties( + var, config, ModbusTextSensor, cg.std_string, cg.std_string + ) 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..25b79474e8 --- /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..3db4d94a45 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -0,0 +1,44 @@ +#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; + } + + 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_; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/monochromatic/light.py b/esphome/components/monochromatic/light.py index 79faacff6c..8f13f58f89 100644 --- a/esphome/components/monochromatic/light.py +++ b/esphome/components/monochromatic/light.py @@ -3,18 +3,22 @@ import esphome.config_validation as cv from esphome.components import light, output from esphome.const import CONF_OUTPUT_ID, CONF_OUTPUT -monochromatic_ns = cg.esphome_ns.namespace('monochromatic') -MonochromaticLightOutput = monochromatic_ns.class_('MonochromaticLightOutput', light.LightOutput) +monochromatic_ns = cg.esphome_ns.namespace("monochromatic") +MonochromaticLightOutput = monochromatic_ns.class_( + "MonochromaticLightOutput", light.LightOutput +) -CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(MonochromaticLightOutput), - cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), -}) +CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(MonochromaticLightOutput), + cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield light.register_light(var, config) + await light.register_light(var, config) - out = yield cg.get_variable(config[CONF_OUTPUT]) + out = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(out)) 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/__init__.py b/esphome/components/mpr121/__init__.py index b1ef9eaef5..dabfb47ad6 100644 --- a/esphome/components/mpr121/__init__.py +++ b/esphome/components/mpr121/__init__.py @@ -8,28 +8,38 @@ CONF_RELEASE_THRESHOLD = "release_threshold" CONF_TOUCH_DEBOUNCE = "touch_debounce" CONF_RELEASE_DEBOUNCE = "release_debounce" -DEPENDENCIES = ['i2c'] -AUTO_LOAD = ['binary_sensor'] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["binary_sensor"] -mpr121_ns = cg.esphome_ns.namespace('mpr121') -CONF_MPR121_ID = 'mpr121_id' -MPR121Component = mpr121_ns.class_('MPR121Component', cg.Component, i2c.I2CDevice) +mpr121_ns = cg.esphome_ns.namespace("mpr121") +CONF_MPR121_ID = "mpr121_id" +MPR121Component = mpr121_ns.class_("MPR121Component", cg.Component, i2c.I2CDevice) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MPR121Component), - cv.Optional(CONF_RELEASE_DEBOUNCE, default=0): cv.int_range(min=0, max=7), - cv.Optional(CONF_TOUCH_DEBOUNCE, default=0): cv.int_range(min=0, max=7), - cv.Optional(CONF_TOUCH_THRESHOLD, default=0x0b): cv.int_range(min=0x05, max=0x30), - cv.Optional(CONF_RELEASE_THRESHOLD, default=0x06): cv.int_range(min=0x05, max=0x30), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x5A)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MPR121Component), + cv.Optional(CONF_RELEASE_DEBOUNCE, default=0): cv.int_range(min=0, max=7), + cv.Optional(CONF_TOUCH_DEBOUNCE, default=0): cv.int_range(min=0, max=7), + cv.Optional(CONF_TOUCH_THRESHOLD, default=0x0B): cv.int_range( + min=0x05, max=0x30 + ), + cv.Optional(CONF_RELEASE_THRESHOLD, default=0x06): cv.int_range( + min=0x05, max=0x30 + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x5A)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_touch_debounce(config[CONF_TOUCH_DEBOUNCE])) cg.add(var.set_release_debounce(config[CONF_RELEASE_DEBOUNCE])) cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) cg.add(var.set_release_threshold(config[CONF_RELEASE_THRESHOLD])) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mpr121/binary_sensor.py b/esphome/components/mpr121/binary_sensor.py index dddfeb40e1..20b80e063e 100644 --- a/esphome/components/mpr121/binary_sensor.py +++ b/esphome/components/mpr121/binary_sensor.py @@ -2,25 +2,32 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor from esphome.const import CONF_CHANNEL, CONF_ID -from . import mpr121_ns, MPR121Component, CONF_MPR121_ID, CONF_TOUCH_THRESHOLD, \ - CONF_RELEASE_THRESHOLD +from . import ( + mpr121_ns, + MPR121Component, + CONF_MPR121_ID, + CONF_TOUCH_THRESHOLD, + CONF_RELEASE_THRESHOLD, +) -DEPENDENCIES = ['mpr121'] -MPR121Channel = mpr121_ns.class_('MPR121Channel', binary_sensor.BinarySensor) +DEPENDENCIES = ["mpr121"] +MPR121Channel = mpr121_ns.class_("MPR121Channel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(MPR121Channel), - cv.GenerateID(CONF_MPR121_ID): cv.use_id(MPR121Component), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=11), - cv.Optional(CONF_TOUCH_THRESHOLD): cv.int_range(min=0x05, max=0x30), - cv.Optional(CONF_RELEASE_THRESHOLD): cv.int_range(min=0x05, max=0x30), -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MPR121Channel), + cv.GenerateID(CONF_MPR121_ID): cv.use_id(MPR121Component), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=11), + cv.Optional(CONF_TOUCH_THRESHOLD): cv.int_range(min=0x05, max=0x30), + cv.Optional(CONF_RELEASE_THRESHOLD): cv.int_range(min=0x05, max=0x30), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) - hub = yield cg.get_variable(config[CONF_MPR121_ID]) + await binary_sensor.register_binary_sensor(var, config) + hub = await cg.get_variable(config[CONF_MPR121_ID]) cg.add(var.set_channel(config[CONF_CHANNEL])) if CONF_TOUCH_THRESHOLD in config: diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 2025bc5b3f..7ba3da7b4d 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -1,10 +1,11 @@ #include "mpr121.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace mpr121 { -static const char *TAG = "mpr121"; +static const char *const TAG = "mpr121"; void MPR121Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MPR121..."); diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index 06f8951bf5..8a0756e63c 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace mpu6050 { -static const char *TAG = "mpu6050"; +static const char *const TAG = "mpu6050"; const uint8_t MPU6050_REGISTER_WHO_AM_I = 0x75; const uint8_t MPU6050_REGISTER_POWER_MANAGEMENT_1 = 0x6B; diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index 73c78e7f16..f9b61dcadc 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -1,53 +1,84 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, CONF_TEMPERATURE, \ - ICON_BRIEFCASE_DOWNLOAD, UNIT_METER_PER_SECOND_SQUARED, \ - ICON_SCREEN_ROTATION, UNIT_DEGREE_PER_SECOND, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + ICON_BRIEFCASE_DOWNLOAD, + STATE_CLASS_MEASUREMENT, + UNIT_METER_PER_SECOND_SQUARED, + ICON_SCREEN_ROTATION, + UNIT_DEGREE_PER_SECOND, + UNIT_CELSIUS, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -CONF_ACCEL_X = 'accel_x' -CONF_ACCEL_Y = 'accel_y' -CONF_ACCEL_Z = 'accel_z' -CONF_GYRO_X = 'gyro_x' -CONF_GYRO_Y = 'gyro_y' -CONF_GYRO_Z = 'gyro_z' +CONF_ACCEL_X = "accel_x" +CONF_ACCEL_Y = "accel_y" +CONF_ACCEL_Z = "accel_z" +CONF_GYRO_X = "gyro_x" +CONF_GYRO_Y = "gyro_y" +CONF_GYRO_Z = "gyro_z" -mpu6050_ns = cg.esphome_ns.namespace('mpu6050') -MPU6050Component = mpu6050_ns.class_('MPU6050Component', cg.PollingComponent, i2c.I2CDevice) +mpu6050_ns = cg.esphome_ns.namespace("mpu6050") +MPU6050Component = mpu6050_ns.class_( + "MPU6050Component", cg.PollingComponent, i2c.I2CDevice +) -accel_schema = sensor.sensor_schema(UNIT_METER_PER_SECOND_SQUARED, ICON_BRIEFCASE_DOWNLOAD, 2) -gyro_schema = sensor.sensor_schema(UNIT_DEGREE_PER_SECOND, ICON_SCREEN_ROTATION, 2) -temperature_schema = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1) +accel_schema = sensor.sensor_schema( + 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_of_measurement=UNIT_DEGREE_PER_SECOND, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, +) +temperature_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MPU6050Component), - cv.Optional(CONF_ACCEL_X): accel_schema, - cv.Optional(CONF_ACCEL_Y): accel_schema, - cv.Optional(CONF_ACCEL_Z): accel_schema, - cv.Optional(CONF_GYRO_X): gyro_schema, - cv.Optional(CONF_GYRO_Y): gyro_schema, - cv.Optional(CONF_GYRO_Z): gyro_schema, - cv.Optional(CONF_TEMPERATURE): temperature_schema, -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x68)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MPU6050Component), + cv.Optional(CONF_ACCEL_X): accel_schema, + cv.Optional(CONF_ACCEL_Y): accel_schema, + cv.Optional(CONF_ACCEL_Z): accel_schema, + cv.Optional(CONF_GYRO_X): gyro_schema, + cv.Optional(CONF_GYRO_Y): gyro_schema, + cv.Optional(CONF_GYRO_Z): gyro_schema, + cv.Optional(CONF_TEMPERATURE): temperature_schema, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x68)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) - for d in ['x', 'y', 'z']: - accel_key = f'accel_{d}' + for d in ["x", "y", "z"]: + accel_key = f"accel_{d}" if accel_key in config: - sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, f'set_accel_{d}_sensor')(sens)) - accel_key = f'gyro_{d}' + sens = await sensor.new_sensor(config[accel_key]) + cg.add(getattr(var, f"set_accel_{d}_sensor")(sens)) + accel_key = f"gyro_{d}" if accel_key in config: - sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, f'set_gyro_{d}_sensor')(sens)) + sens = await sensor.new_sensor(config[accel_key]) + cg.add(getattr(var, f"set_gyro_{d}_sensor")(sens)) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 2f0ed0f8e2..d677d54d23 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -5,17 +5,44 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition from esphome.components import logger -from esphome.const import CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CLIENT_ID, \ - CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, \ - CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, CONF_LOG_TOPIC, CONF_ON_JSON_MESSAGE, CONF_ON_MESSAGE, \ - CONF_PASSWORD, CONF_PAYLOAD, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PORT, \ - CONF_QOS, CONF_REBOOT_TIMEOUT, CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SSL_FINGERPRINTS, \ - CONF_STATE_TOPIC, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_TRIGGER_ID, CONF_USERNAME, \ - CONF_WILL_MESSAGE -from esphome.core import coroutine_with_priority, coroutine, CORE +from esphome.const import ( + CONF_AVAILABILITY, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CLIENT_ID, + CONF_COMMAND_TOPIC, + CONF_DISCOVERY, + CONF_DISCOVERY_PREFIX, + CONF_DISCOVERY_RETAIN, + CONF_DISCOVERY_UNIQUE_ID_GENERATOR, + CONF_ID, + CONF_KEEPALIVE, + CONF_LEVEL, + CONF_LOG_TOPIC, + CONF_ON_JSON_MESSAGE, + CONF_ON_MESSAGE, + CONF_PASSWORD, + CONF_PAYLOAD, + CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, + CONF_PORT, + CONF_QOS, + CONF_REBOOT_TIMEOUT, + CONF_RETAIN, + CONF_SHUTDOWN_MESSAGE, + CONF_SSL_FINGERPRINTS, + CONF_STATE_TOPIC, + CONF_TOPIC, + CONF_TOPIC_PREFIX, + CONF_TRIGGER_ID, + CONF_USE_ABBREVIATIONS, + CONF_USERNAME, + CONF_WILL_MESSAGE, +) +from esphome.core import coroutine_with_priority, CORE -DEPENDENCIES = ['network'] -AUTO_LOAD = ['json', 'async_tcp'] +DEPENDENCIES = ["network"] +AUTO_LOAD = ["json", "async_tcp"] def validate_message_just_topic(value): @@ -23,39 +50,58 @@ def validate_message_just_topic(value): return MQTT_MESSAGE_BASE({CONF_TOPIC: value}) -MQTT_MESSAGE_BASE = cv.Schema({ - cv.Required(CONF_TOPIC): cv.publish_topic, - cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, - cv.Optional(CONF_RETAIN, default=True): cv.boolean, -}) +MQTT_MESSAGE_BASE = cv.Schema( + { + cv.Required(CONF_TOPIC): cv.publish_topic, + cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, + cv.Optional(CONF_RETAIN, default=True): cv.boolean, + } +) -MQTT_MESSAGE_TEMPLATE_SCHEMA = cv.Any(None, MQTT_MESSAGE_BASE, validate_message_just_topic) +MQTT_MESSAGE_TEMPLATE_SCHEMA = cv.Any( + None, MQTT_MESSAGE_BASE, validate_message_just_topic +) -MQTT_MESSAGE_SCHEMA = cv.Any(None, MQTT_MESSAGE_BASE.extend({ - cv.Required(CONF_PAYLOAD): cv.mqtt_payload, -})) +MQTT_MESSAGE_SCHEMA = cv.Any( + None, + MQTT_MESSAGE_BASE.extend( + { + cv.Required(CONF_PAYLOAD): cv.mqtt_payload, + } + ), +) -mqtt_ns = cg.esphome_ns.namespace('mqtt') -MQTTMessage = mqtt_ns.struct('MQTTMessage') -MQTTClientComponent = mqtt_ns.class_('MQTTClientComponent', cg.Component) -MQTTPublishAction = mqtt_ns.class_('MQTTPublishAction', automation.Action) -MQTTPublishJsonAction = mqtt_ns.class_('MQTTPublishJsonAction', automation.Action) -MQTTMessageTrigger = mqtt_ns.class_('MQTTMessageTrigger', - automation.Trigger.template(cg.std_string), - cg.Component) -MQTTJsonMessageTrigger = mqtt_ns.class_('MQTTJsonMessageTrigger', - automation.Trigger.template(cg.JsonObjectConstRef)) -MQTTComponent = mqtt_ns.class_('MQTTComponent', cg.Component) -MQTTConnectedCondition = mqtt_ns.class_('MQTTConnectedCondition', Condition) +mqtt_ns = cg.esphome_ns.namespace("mqtt") +MQTTMessage = mqtt_ns.struct("MQTTMessage") +MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component) +MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action) +MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action) +MQTTMessageTrigger = mqtt_ns.class_( + "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component +) +MQTTJsonMessageTrigger = mqtt_ns.class_( + "MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConstRef) +) +MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) +MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) -MQTTBinarySensorComponent = mqtt_ns.class_('MQTTBinarySensorComponent', MQTTComponent) -MQTTClimateComponent = mqtt_ns.class_('MQTTClimateComponent', MQTTComponent) -MQTTCoverComponent = mqtt_ns.class_('MQTTCoverComponent', MQTTComponent) -MQTTFanComponent = mqtt_ns.class_('MQTTFanComponent', MQTTComponent) -MQTTJSONLightComponent = mqtt_ns.class_('MQTTJSONLightComponent', MQTTComponent) -MQTTSensorComponent = mqtt_ns.class_('MQTTSensorComponent', MQTTComponent) -MQTTSwitchComponent = mqtt_ns.class_('MQTTSwitchComponent', MQTTComponent) -MQTTTextSensor = mqtt_ns.class_('MQTTTextSensor', MQTTComponent) +MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent) +MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent) +MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent) +MQTTFanComponent = mqtt_ns.class_("MQTTFanComponent", MQTTComponent) +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) +MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) + +MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") +MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = { + "legacy": MQTTDiscoveryUniqueIdGenerator.MQTT_LEGACY_UNIQUE_ID_GENERATOR, + "mac": MQTTDiscoveryUniqueIdGenerator.MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR, +} def validate_config(value): @@ -64,28 +110,28 @@ def validate_config(value): topic_prefix = value[CONF_TOPIC_PREFIX] if CONF_BIRTH_MESSAGE not in value: out[CONF_BIRTH_MESSAGE] = { - CONF_TOPIC: f'{topic_prefix}/status', - CONF_PAYLOAD: 'online', + CONF_TOPIC: f"{topic_prefix}/status", + CONF_PAYLOAD: "online", CONF_QOS: 0, CONF_RETAIN: True, } if CONF_WILL_MESSAGE not in value: out[CONF_WILL_MESSAGE] = { - CONF_TOPIC: f'{topic_prefix}/status', - CONF_PAYLOAD: 'offline', + CONF_TOPIC: f"{topic_prefix}/status", + CONF_PAYLOAD: "offline", CONF_QOS: 0, CONF_RETAIN: True, } if CONF_SHUTDOWN_MESSAGE not in value: out[CONF_SHUTDOWN_MESSAGE] = { - CONF_TOPIC: f'{topic_prefix}/status', - CONF_PAYLOAD: 'offline', + CONF_TOPIC: f"{topic_prefix}/status", + CONF_PAYLOAD: "offline", CONF_QOS: 0, CONF_RETAIN: True, } if CONF_LOG_TOPIC not in value: out[CONF_LOG_TOPIC] = { - CONF_TOPIC: f'{topic_prefix}/debug', + CONF_TOPIC: f"{topic_prefix}/debug", CONF_QOS: 0, CONF_RETAIN: True, } @@ -94,46 +140,73 @@ def validate_config(value): def validate_fingerprint(value): value = cv.string(value) - if re.match(r'^[0-9a-f]{40}$', value) is None: + if re.match(r"^[0-9a-f]{40}$", value) is None: raise cv.Invalid("fingerprint must be valid SHA1 hash") return value -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(MQTTClientComponent), - cv.Required(CONF_BROKER): cv.string_strict, - cv.Optional(CONF_PORT, default=1883): cv.port, - cv.Optional(CONF_USERNAME, default=''): cv.string, - cv.Optional(CONF_PASSWORD, default=''): cv.string, - cv.Optional(CONF_CLIENT_ID): cv.string, - cv.Optional(CONF_DISCOVERY, default=True): cv.Any(cv.boolean, cv.one_of("CLEAN", upper=True)), - cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, - cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.publish_topic, - - cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA, - cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA, - cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA, - cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic, - cv.Optional(CONF_LOG_TOPIC): cv.Any(None, MQTT_MESSAGE_BASE.extend({ - cv.Optional(CONF_LEVEL): logger.is_log_level, - }), validate_message_just_topic), - - cv.Optional(CONF_SSL_FINGERPRINTS): cv.All(cv.only_on_esp8266, - cv.ensure_list(validate_fingerprint)), - cv.Optional(CONF_KEEPALIVE, default='15s'): cv.positive_time_period_seconds, - cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger), - cv.Required(CONF_TOPIC): cv.subscribe_topic, - cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, - cv.Optional(CONF_PAYLOAD): cv.string_strict, - }), - cv.Optional(CONF_ON_JSON_MESSAGE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTJsonMessageTrigger), - cv.Required(CONF_TOPIC): cv.subscribe_topic, - cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, - }), -}), validate_config) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MQTTClientComponent), + cv.Required(CONF_BROKER): cv.string_strict, + cv.Optional(CONF_PORT, default=1883): cv.port, + cv.Optional(CONF_USERNAME, default=""): cv.string, + cv.Optional(CONF_PASSWORD, default=""): cv.string, + cv.Optional(CONF_CLIENT_ID): cv.string, + cv.Optional(CONF_DISCOVERY, default=True): cv.Any( + cv.boolean, cv.one_of("CLEAN", upper=True) + ), + cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, + cv.Optional( + CONF_DISCOVERY_PREFIX, default="homeassistant" + ): cv.publish_topic, + cv.Optional(CONF_DISCOVERY_UNIQUE_ID_GENERATOR, default="legacy"): cv.enum( + MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS + ), + cv.Optional(CONF_USE_ABBREVIATIONS, default=True): cv.boolean, + cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA, + cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA, + cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA, + cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic, + cv.Optional(CONF_LOG_TOPIC): cv.Any( + None, + MQTT_MESSAGE_BASE.extend( + { + cv.Optional(CONF_LEVEL): logger.is_log_level, + } + ), + validate_message_just_topic, + ), + cv.Optional(CONF_SSL_FINGERPRINTS): cv.All( + cv.only_on_esp8266, cv.ensure_list(validate_fingerprint) + ), + cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger), + cv.Required(CONF_TOPIC): cv.subscribe_topic, + cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, + cv.Optional(CONF_PAYLOAD): cv.string_strict, + } + ), + cv.Optional(CONF_ON_JSON_MESSAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + MQTTJsonMessageTrigger + ), + cv.Required(CONF_TOPIC): cv.subscribe_topic, + cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, + } + ), + } + ), + validate_config, + cv.only_with_arduino, +) def exp_mqtt_message(config): @@ -141,22 +214,22 @@ def exp_mqtt_message(config): return cg.optional(cg.TemplateArguments(MQTTMessage)) exp = cg.StructInitializer( MQTTMessage, - ('topic', config[CONF_TOPIC]), - ('payload', config.get(CONF_PAYLOAD, "")), - ('qos', config[CONF_QOS]), - ('retain', config[CONF_RETAIN]) + ("topic", config[CONF_TOPIC]), + ("payload", config.get(CONF_PAYLOAD, "")), + ("qos", config[CONF_QOS]), + ("retain", config[CONF_RETAIN]), ) return exp @coroutine_with_priority(40.0) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, 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_define('USE_MQTT') + cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6") + cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) cg.add(var.set_broker_address(config[CONF_BROKER])) @@ -169,16 +242,28 @@ def to_code(config): discovery = config[CONF_DISCOVERY] discovery_retain = config[CONF_DISCOVERY_RETAIN] discovery_prefix = config[CONF_DISCOVERY_PREFIX] + discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR] if not discovery: cg.add(var.disable_discovery()) elif discovery == "CLEAN": - cg.add(var.set_discovery_info(discovery_prefix, discovery_retain, True)) + cg.add( + var.set_discovery_info( + discovery_prefix, discovery_unique_id_generator, discovery_retain, True + ) + ) elif CONF_DISCOVERY_RETAIN in config or CONF_DISCOVERY_PREFIX in config: - cg.add(var.set_discovery_info(discovery_prefix, discovery_retain)) + cg.add( + var.set_discovery_info( + discovery_prefix, discovery_unique_id_generator, discovery_retain + ) + ) cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX])) + if config[CONF_USE_ABBREVIATIONS]: + cg.add_define("USE_MQTT_ABBREVIATIONS") + birth_message = config[CONF_BIRTH_MESSAGE] if not birth_message: cg.add(var.disable_birth_message()) @@ -206,9 +291,11 @@ 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)] + arr = [ + 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') + cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") cg.add(var.set_keep_alive(config[CONF_KEEPALIVE])) @@ -219,76 +306,83 @@ def to_code(config): cg.add(trig.set_qos(conf[CONF_QOS])) if CONF_PAYLOAD in conf: cg.add(trig.set_payload(conf[CONF_PAYLOAD])) - yield cg.register_component(trig, conf) - yield automation.build_automation(trig, [(cg.std_string, 'x')], conf) + await cg.register_component(trig, conf) + await automation.build_automation(trig, [(cg.std_string, "x")], conf) for conf in config.get(CONF_ON_JSON_MESSAGE, []): trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS]) - yield automation.build_automation(trig, [(cg.JsonObjectConstRef, 'x')], conf) + await automation.build_automation(trig, [(cg.JsonObjectConstRef, "x")], conf) -MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(MQTTClientComponent), - cv.Required(CONF_TOPIC): cv.templatable(cv.publish_topic), - cv.Required(CONF_PAYLOAD): cv.templatable(cv.mqtt_payload), - cv.Optional(CONF_QOS, default=0): cv.templatable(cv.mqtt_qos), - cv.Optional(CONF_RETAIN, default=False): cv.templatable(cv.boolean), -}) +MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + cv.Required(CONF_TOPIC): cv.templatable(cv.publish_topic), + cv.Required(CONF_PAYLOAD): cv.templatable(cv.mqtt_payload), + cv.Optional(CONF_QOS, default=0): cv.templatable(cv.mqtt_qos), + cv.Optional(CONF_RETAIN, default=False): cv.templatable(cv.boolean), + } +) -@automation.register_action('mqtt.publish', MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA) -def mqtt_publish_action_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA +) +async def mqtt_publish_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_TOPIC], args, cg.std_string) + template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string) cg.add(var.set_topic(template_)) - template_ = yield cg.templatable(config[CONF_PAYLOAD], args, cg.std_string) + template_ = await cg.templatable(config[CONF_PAYLOAD], args, cg.std_string) cg.add(var.set_payload(template_)) - template_ = yield cg.templatable(config[CONF_QOS], args, cg.uint8) + template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) cg.add(var.set_qos(template_)) - template_ = yield cg.templatable(config[CONF_RETAIN], args, bool) + template_ = await cg.templatable(config[CONF_RETAIN], args, bool) cg.add(var.set_retain(template_)) - yield var + return var -MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(MQTTClientComponent), - cv.Required(CONF_TOPIC): cv.templatable(cv.publish_topic), - cv.Required(CONF_PAYLOAD): cv.lambda_, - cv.Optional(CONF_QOS, default=0): cv.templatable(cv.mqtt_qos), - cv.Optional(CONF_RETAIN, default=False): cv.templatable(cv.boolean), -}) +MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + cv.Required(CONF_TOPIC): cv.templatable(cv.publish_topic), + cv.Required(CONF_PAYLOAD): cv.lambda_, + cv.Optional(CONF_QOS, default=0): cv.templatable(cv.mqtt_qos), + cv.Optional(CONF_RETAIN, default=False): cv.templatable(cv.boolean), + } +) -@automation.register_action('mqtt.publish_json', MQTTPublishJsonAction, - MQTT_PUBLISH_JSON_ACTION_SCHEMA) -def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "mqtt.publish_json", MQTTPublishJsonAction, MQTT_PUBLISH_JSON_ACTION_SCHEMA +) +async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_TOPIC], args, cg.std_string) + template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string) cg.add(var.set_topic(template_)) - args_ = args + [(cg.JsonObjectRef, 'root')] - lambda_ = yield cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void) + args_ = args + [(cg.JsonObjectRef, "root")] + lambda_ = await cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void) cg.add(var.set_payload(lambda_)) - template_ = yield cg.templatable(config[CONF_QOS], args, cg.uint8) + template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) cg.add(var.set_qos(template_)) - template_ = yield cg.templatable(config[CONF_RETAIN], args, bool) + template_ = await cg.templatable(config[CONF_RETAIN], args, bool) cg.add(var.set_retain(template_)) - yield var + return var def get_default_topic_for(data, component_type, name, suffix): - whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' - sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in whitelist) - return '{}/{}/{}/{}'.format(data.topic_prefix, component_type, - sanitized_name, suffix) + allowlist = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + sanitized_name = "".join( + x for x in name.lower().replace(" ", "_") if x in allowlist + ) + return f"{data.topic_prefix}/{component_type}/{sanitized_name}/{suffix}" -@coroutine -def register_mqtt_component(var, config): - yield cg.register_component(var, {}) +async def register_mqtt_component(var, config): + await cg.register_component(var, {}) if CONF_RETAIN in config: cg.add(var.set_retain(config[CONF_RETAIN])) @@ -303,14 +397,24 @@ def register_mqtt_component(var, config): if not availability: cg.add(var.disable_availability()) else: - cg.add(var.set_availability(availability[CONF_TOPIC], - availability[CONF_PAYLOAD_AVAILABLE], - availability[CONF_PAYLOAD_NOT_AVAILABLE])) + cg.add( + var.set_availability( + availability[CONF_TOPIC], + availability[CONF_PAYLOAD_AVAILABLE], + availability[CONF_PAYLOAD_NOT_AVAILABLE], + ) + ) -@automation.register_condition('mqtt.connected', MQTTConnectedCondition, cv.Schema({ - cv.GenerateID(): cv.use_id(MQTTClientComponent), -})) -def mqtt_connected_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren) +@automation.register_condition( + "mqtt.connected", + MQTTConnectedCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_connected_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/mqtt/custom_mqtt_device.cpp b/esphome/components/mqtt/custom_mqtt_device.cpp index 8b17c5f17f..787cc1153f 100644 --- a/esphome/components/mqtt/custom_mqtt_device.cpp +++ b/esphome/components/mqtt/custom_mqtt_device.cpp @@ -1,10 +1,13 @@ #include "custom_mqtt_device.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.custom"; +static const char *const TAG = "mqtt.custom"; bool CustomMQTTDevice::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) { return global_mqtt_client->publish(topic, payload, qos, retain); @@ -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 edabcb398c..0a161f89a1 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -1,14 +1,18 @@ #include "mqtt_binary_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_BINARY_SENSOR namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.binary_sensor"; +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,15 +28,14 @@ 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()) - root["device_class"] = this->binary_sensor_->get_device_class(); + root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); if (this->binary_sensor_->is_status_binary_sensor()) - root["payload_on"] = mqtt::global_mqtt_client->get_availability().payload_available; + root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) - root["payload_off"] = mqtt::global_mqtt_client->get_availability().payload_not_available; + root[MQTT_PAYLOAD_OFF] = mqtt::global_mqtt_client->get_availability().payload_not_available; config.command_topic = false; } bool MQTTBinarySensorComponent::send_initial_state() { @@ -42,7 +45,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 +57,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_button.cpp b/esphome/components/mqtt/mqtt_button.cpp new file mode 100644 index 0000000000..5f3aaa1dd9 --- /dev/null +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -0,0 +1,46 @@ +#include "mqtt_button.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_BUTTON + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.button"; + +using namespace esphome::button; + +MQTTButtonComponent::MQTTButtonComponent(button::Button *button) : MQTTComponent(), button_(button) {} + +void MQTTButtonComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + if (payload == "PRESS") { + this->button_->press(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + this->status_momentary_warning("state", 5000); + } + }); +} +void MQTTButtonComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Button '%s': ", this->button_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true); +} + +void MQTTButtonComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + config.state_topic = false; + if (!this->button_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); +} + +std::string MQTTButtonComponent::component_type() const { return "button"; } +const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_button.h b/esphome/components/mqtt/mqtt_button.h new file mode 100644 index 0000000000..66e4b2609f --- /dev/null +++ b/esphome/components/mqtt/mqtt_button.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_BUTTON + +#include "esphome/components/button/button.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTButtonComponent : public mqtt::MQTTComponent { + public: + explicit MQTTButtonComponent(button::Button *button); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + + /// Buttons do not send a state so just return true. + bool send_initial_state() override { return true; } + + void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + + protected: + /// "button" component type. + std::string component_type() const override; + const EntityBase *get_entity() const override; + + button::Button *button_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 2eb1c52153..43c49e9f7f 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -1,8 +1,12 @@ #include "mqtt_client.h" -#include "esphome/core/log.h" + +#ifdef USE_MQTT + #include "esphome/core/application.h" #include "esphome/core/helpers.h" -#include "esphome/core/util.h" +#include "esphome/core/log.h" +#include "esphome/components/network/util.h" +#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -13,7 +17,7 @@ namespace esphome { namespace mqtt { -static const char *TAG = "mqtt"; +static const char *const TAG = "mqtt"; MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; @@ -23,11 +27,19 @@ 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) { - std::string payload_s(payload, len); - std::string topic_s(topic); - this->on_message(topic_s, payload_s); + if (index == 0) + this->payload_buffer_.reserve(total); + + // append new payload, may contain incomplete MQTT message + this->payload_buffer_.append(payload, len); + + // MQTT fully received + if (len + index == total) { + this->on_message(topic, this->payload_buffer_); + this->payload_buffer_.clear(); + } }); this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { this->state_ = MQTT_CLIENT_DISCONNECTED; @@ -50,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()) { @@ -77,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 @@ -89,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; @@ -106,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); @@ -133,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) { @@ -145,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..."); @@ -173,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()); @@ -211,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(); } @@ -332,7 +344,7 @@ void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t ca this->subscriptions_.push_back(subscription); } -void MQTTClientComponent::subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos) { +void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) { auto f = [callback](const std::string &topic, const std::string &payload) { json::parse_json(payload, [topic, callback](JsonObject &root) { callback(topic, root); }); }; @@ -347,6 +359,26 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, mqtt_json_cal this->subscriptions_.push_back(subscription); } +void MQTTClientComponent::unsubscribe(const std::string &topic) { + uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str()); + yield(); + if (ret != 0) { + ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str()); + } else { + delay(5); + ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str()); + this->status_momentary_warning("unsubscribe", 1000); + } + + auto it = subscriptions_.begin(); + while (it != subscriptions_.end()) { + if (it->topic == topic) + it = subscriptions_.erase(it); + else + ++it; + } +} + // Publish bool MQTTClientComponent::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) { return this->publish(topic, payload.data(), payload.size(), qos, retain); @@ -447,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]() { @@ -455,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 } @@ -503,8 +535,10 @@ void MQTTClientComponent::set_birth_message(MQTTMessage &&message) { void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->shutdown_message_ = std::move(message); } -void MQTTClientComponent::set_discovery_info(std::string &&prefix, bool retain, bool clean) { +void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, + bool retain, bool clean) { this->discovery_info_.prefix = std::move(prefix); + this->discovery_info_.unique_id_generator = unique_id_generator; this->discovery_info_.retain = retain; this->discovery_info_.clean = clean; } @@ -530,22 +564,23 @@ void MQTTClientComponent::add_ssl_fingerprint(const std::arrayqos_ = qos; } void MQTTMessageTrigger::set_payload(const std::string &payload) { this->payload_ = payload; } void MQTTMessageTrigger::setup() { - global_mqtt_client->subscribe(this->topic_, - [this](const std::string &topic, const std::string &payload) { - if (this->payload_.has_value() && payload != *this->payload_) { - return; - } + global_mqtt_client->subscribe( + this->topic_, + [this](const std::string &topic, const std::string &payload) { + if (this->payload_.has_value() && payload != *this->payload_) { + return; + } - this->trigger(payload); - }, - this->qos_); + this->trigger(payload); + }, + this->qos_); } void MQTTMessageTrigger::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Message Trigger:"); @@ -556,3 +591,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 6f14b0c92c..d6194da794 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" @@ -51,6 +55,12 @@ struct Availability { std::string payload_not_available; }; +/// available discovery unique_id generators +enum MQTTDiscoveryUniqueIdGenerator { + MQTT_LEGACY_UNIQUE_ID_GENERATOR = 0, + MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR, +}; + /** Internal struct for MQTT Home Assistant discovery * * See MQTT Discovery. @@ -59,6 +69,7 @@ struct MQTTDiscoveryInfo { std::string prefix; ///< The Home Assistant discovery prefix. Empty means disabled. bool retain; ///< Whether to retain discovery messages. bool clean; + MQTTDiscoveryUniqueIdGenerator unique_id_generator; }; enum MQTTClientState { @@ -94,9 +105,11 @@ class MQTTClientComponent : public Component { * * See MQTT Discovery. * @param prefix The Home Assistant discovery prefix. + * @param unique_id_generator Controls how UniqueId is generated. * @param retain Whether to retain discovery messages. */ - void set_discovery_info(std::string &&prefix, bool retain, bool clean = false); + void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, bool retain, + bool clean = false); /// Get Home Assistant discovery info. const MQTTDiscoveryInfo &get_discovery_info() const; /// Globally disable Home Assistant discovery. @@ -157,7 +170,16 @@ class MQTTClientComponent : public Component { * received. * @param qos The QoS of this subscription. */ - void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0); + void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0); + + /** Unsubscribe from an MQTT topic. + * + * If multiple existing subscriptions to the same topic exist, all of them will be removed. + * + * @param topic The topic to unsubscribe from. + * Must match the topic in the original subscribe or subscribe_json call exactly. + */ + void unsubscribe(const std::string &topic); /** Publish a MQTTMessage * @@ -217,7 +239,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); @@ -250,12 +272,13 @@ class MQTTClientComponent : public Component { }; std::string topic_prefix_{}; MQTTMessage log_message_; + std::string payload_buffer_; int log_level_{ESPHOME_LOG_LEVEL}; 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_; @@ -265,11 +288,11 @@ class MQTTClientComponent : public Component { optional disconnect_reason_{}; }; -extern MQTTClientComponent *global_mqtt_client; +extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) class MQTTMessageTrigger : public Trigger, public Component { public: - explicit MQTTMessageTrigger(const std::string &topic); + explicit MQTTMessageTrigger(std::string topic); void set_qos(uint8_t qos); void set_payload(const std::string &payload); @@ -316,6 +339,7 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(bool, retain) void set_payload(std::function payload) { this->payload_ = payload; } + void play(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); @@ -341,3 +365,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 2a95ed2f64..ebc708f444 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,12 +1,15 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_CLIMATE namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.climate"; +static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; @@ -15,14 +18,14 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC // current_temperature_topic if (traits.get_supports_current_temperature()) { // current_temperature_topic - root["curr_temp_t"] = this->get_current_temperature_state_topic(); + root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } // mode_command_topic - root["mode_cmd_t"] = this->get_mode_command_topic(); + root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); // mode_state_topic - root["mode_stat_t"] = this->get_mode_state_topic(); + root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); // modes - JsonArray &modes = root.createNestedArray("modes"); + JsonArray &modes = root.createNestedArray(MQTT_MODES); // sort array for nice UI in HA if (traits.supports_mode(CLIMATE_MODE_AUTO)) modes.add("auto"); @@ -35,46 +38,50 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC modes.add("fan_only"); if (traits.supports_mode(CLIMATE_MODE_DRY)) modes.add("dry"); + if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL)) + modes.add("heat_cool"); if (traits.get_supports_two_point_target_temperature()) { // temperature_low_command_topic - root["temp_lo_cmd_t"] = this->get_target_temperature_low_command_topic(); + root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic(); // temperature_low_state_topic - root["temp_lo_stat_t"] = this->get_target_temperature_low_state_topic(); + root[MQTT_TEMPERATURE_LOW_STATE_TOPIC] = this->get_target_temperature_low_state_topic(); // temperature_high_command_topic - root["temp_hi_cmd_t"] = this->get_target_temperature_high_command_topic(); + root[MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC] = this->get_target_temperature_high_command_topic(); // temperature_high_state_topic - root["temp_hi_stat_t"] = this->get_target_temperature_high_state_topic(); + root[MQTT_TEMPERATURE_HIGH_STATE_TOPIC] = this->get_target_temperature_high_state_topic(); } else { // temperature_command_topic - root["temp_cmd_t"] = this->get_target_temperature_command_topic(); + root[MQTT_TEMPERATURE_COMMAND_TOPIC] = this->get_target_temperature_command_topic(); // temperature_state_topic - root["temp_stat_t"] = this->get_target_temperature_state_topic(); + root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } // min_temp - root["min_temp"] = traits.get_visual_min_temperature(); + root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp - root["max_temp"] = traits.get_visual_max_temperature(); + root[MQTT_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[MQTT_TEMPERATURE_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(); + root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic(); // away_mode_state_topic - root["away_mode_stat_t"] = this->get_away_state_topic(); + root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic(); } if (traits.get_supports_action()) { // action_topic - root["act_t"] = this->get_action_state_topic(); + root[MQTT_ACTION_TOPIC] = 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(); + root[MQTT_FAN_MODE_COMMAND_TOPIC] = this->get_fan_mode_command_topic(); // fan_mode_state_topic - root["fan_mode_stat_t"] = this->get_fan_mode_state_topic(); + root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); // fan_modes JsonArray &fan_modes = root.createNestedArray("fan_modes"); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) @@ -95,13 +102,15 @@ 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()) { // swing_mode_command_topic - root["swing_mode_cmd_t"] = this->get_swing_mode_command_topic(); + root[MQTT_SWING_MODE_COMMAND_TOPIC] = this->get_swing_mode_command_topic(); // swing_mode_state_topic - root["swing_mode_stat_t"] = this->get_swing_mode_state_topic(); + root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); // swing_modes JsonArray &swing_modes = root.createNestedArray("swing_modes"); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) @@ -128,7 +137,7 @@ void MQTTClimateComponent::setup() { if (traits.get_supports_two_point_target_temperature()) { this->subscribe(this->get_target_temperature_low_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); return; @@ -139,7 +148,7 @@ void MQTTClimateComponent::setup() { }); this->subscribe(this->get_target_temperature_high_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); return; @@ -151,7 +160,7 @@ void MQTTClimateComponent::setup() { } else { this->subscribe(this->get_target_temperature_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); return; @@ -162,19 +171,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: @@ -205,9 +214,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 @@ -231,12 +240,15 @@ bool MQTTClimateComponent::publish_state_() { case CLIMATE_MODE_DRY: mode_s = "dry"; break; + case CLIMATE_MODE_HEAT_COOL: + mode_s = "heat_cool"; + break; } bool success = true; 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; @@ -254,8 +266,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; } @@ -286,36 +298,39 @@ bool MQTTClimateComponent::publish_state_() { } if (traits.get_supports_fan_modes()) { - const char *payload = ""; - switch (this->device_->fan_mode) { - 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; } @@ -347,3 +362,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 4201d41c44..bf9f5e34b8 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -1,18 +1,23 @@ #include "mqtt_component.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/version.h" +#include "mqtt_const.h" + namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.component"; +static const char *const TAG = "mqtt.component"; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { - std::string sanitized_name = sanitize_string_whitelist(App.get_name(), HOSTNAME_CHARACTER_WHITELIST); + std::string sanitized_name = str_sanitize(App.get_name()); return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + this->get_default_object_id_() + "/config"; } @@ -65,47 +70,69 @@ bool MQTTComponent::send_discovery_() { this->send_discovery(root, config); - std::string name = this->friendly_name(); - root["name"] = name; + // Fields from EntityBase + root[MQTT_NAME] = this->friendly_name(); + if (this->is_disabled_by_default()) + root[MQTT_ENABLED_BY_DEFAULT] = false; + if (!this->get_icon().empty()) + root[MQTT_ICON] = this->get_icon(); + + switch (this->get_entity()->get_entity_category()) { + case ENTITY_CATEGORY_NONE: + break; + case ENTITY_CATEGORY_CONFIG: + root[MQTT_ENTITY_CATEGORY] = "config"; + break; + case ENTITY_CATEGORY_DIAGNOSTIC: + root[MQTT_ENTITY_CATEGORY] = "diagnostic"; + break; + } + if (config.state_topic) - root["state_topic"] = this->get_state_topic_(); + root[MQTT_STATE_TOPIC] = this->get_state_topic_(); if (config.command_topic) - root["command_topic"] = this->get_command_topic_(); + root[MQTT_COMMAND_TOPIC] = this->get_command_topic_(); if (this->availability_ == nullptr) { if (!global_mqtt_client->get_availability().topic.empty()) { - root["availability_topic"] = global_mqtt_client->get_availability().topic; + root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic; if (global_mqtt_client->get_availability().payload_available != "online") - root["payload_available"] = global_mqtt_client->get_availability().payload_available; + root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available; if (global_mqtt_client->get_availability().payload_not_available != "offline") - root["payload_not_available"] = global_mqtt_client->get_availability().payload_not_available; + root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available; } } else if (!this->availability_->topic.empty()) { - root["availability_topic"] = this->availability_->topic; + root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic; if (this->availability_->payload_available != "online") - root["payload_available"] = this->availability_->payload_available; + root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available; if (this->availability_->payload_not_available != "offline") - root["payload_not_available"] = this->availability_->payload_not_available; + root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; } const std::string &node_name = App.get_name(); std::string unique_id = this->unique_id(); if (!unique_id.empty()) { - root["unique_id"] = unique_id; + root[MQTT_UNIQUE_ID] = unique_id; } else { - // default to almost-unique ID. It's a hack but the only way to get that - // gorgeous device registry view. - root["unique_id"] = "ESP" + this->component_type() + this->get_default_object_id_(); + const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); + if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { + char friendly_name_hash[9]; + sprintf(friendly_name_hash, "%08x", fnv1_hash(this->friendly_name())); + friendly_name_hash[8] = 0; // ensure the hash-string ends with null + root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; + } else { + // default to almost-unique ID. It's a hack but the only way to get that + // gorgeous device registry view. + root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); + } } - JsonObject &device_info = root.createNestedObject("device"); - 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["manufacturer"] = "espressif"; + JsonObject &device_info = root.createNestedObject(MQTT_DEVICE); + device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); + device_info[MQTT_DEVICE_NAME] = node_name; + device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); + device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; + device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; }, 0, discovery_info.retain); } @@ -117,15 +144,15 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return sanitize_string_whitelist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_WHITELIST); + return str_sanitize(str_snake_case(this->friendly_name())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { global_mqtt_client->subscribe(topic, std::move(callback), qos); } -void MQTTComponent::subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos) { - global_mqtt_client->subscribe_json(topic, std::move(callback), qos); +void MQTTComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) { + global_mqtt_client->subscribe_json(topic, callback, qos); } MQTTComponent::MQTTComponent() = default; @@ -141,8 +168,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 +215,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 43465af498..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); @@ -127,7 +136,7 @@ class MQTTComponent : public Component { * received. * @param qos The MQTT quality of service. Defaults to 0. */ - void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0); + void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0); protected: /// Helper method to get the discovery topic for this component. @@ -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_const.h b/esphome/components/mqtt/mqtt_const.h new file mode 100644 index 0000000000..8134a6b53e --- /dev/null +++ b/esphome/components/mqtt/mqtt_const.h @@ -0,0 +1,522 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT + +namespace esphome { +namespace mqtt { + +#ifdef USE_MQTT_ABBREVIATIONS + +constexpr const char *const MQTT_ACTION_TOPIC = "act_t"; +constexpr const char *const MQTT_ACTION_TEMPLATE = "act_tpl"; +constexpr const char *const MQTT_AUTOMATION_TYPE = "atype"; +constexpr const char *const MQTT_AUX_COMMAND_TOPIC = "aux_cmd_t"; +constexpr const char *const MQTT_AUX_STATE_TEMPLATE = "aux_stat_tpl"; +constexpr const char *const MQTT_AUX_STATE_TOPIC = "aux_stat_t"; +constexpr const char *const MQTT_AVAILABILITY = "avty"; +constexpr const char *const MQTT_AVAILABILITY_MODE = "avty_mode"; +constexpr const char *const MQTT_AVAILABILITY_TOPIC = "avty_t"; +constexpr const char *const MQTT_AWAY_MODE_COMMAND_TOPIC = "away_mode_cmd_t"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TEMPLATE = "away_mode_stat_tpl"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TOPIC = "away_mode_stat_t"; +constexpr const char *const MQTT_BLUE_TEMPLATE = "b_tpl"; +constexpr const char *const MQTT_BRIGHTNESS_COMMAND_TOPIC = "bri_cmd_t"; +constexpr const char *const MQTT_BRIGHTNESS_SCALE = "bri_scl"; +constexpr const char *const MQTT_BRIGHTNESS_STATE_TOPIC = "bri_stat_t"; +constexpr const char *const MQTT_BRIGHTNESS_TEMPLATE = "bri_tpl"; +constexpr const char *const MQTT_BRIGHTNESS_VALUE_TEMPLATE = "bri_val_tpl"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TEMPLATE = "clr_temp_cmd_tpl"; +constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "bat_lev_t"; +constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "bat_lev_tpl"; +constexpr const char *const MQTT_CONFIGURATION_URL = "cu"; +constexpr const char *const MQTT_CHARGING_TOPIC = "chrg_t"; +constexpr const char *const MQTT_CHARGING_TEMPLATE = "chrg_tpl"; +constexpr const char *const MQTT_COLOR_MODE = "clrm"; +constexpr const char *const MQTT_COLOR_MODE_STATE_TOPIC = "clrm_stat_t"; +constexpr const char *const MQTT_COLOR_MODE_VALUE_TEMPLATE = "clrm_val_tpl"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TOPIC = "clr_temp_cmd_t"; +constexpr const char *const MQTT_COLOR_TEMP_STATE_TOPIC = "clr_temp_stat_t"; +constexpr const char *const MQTT_COLOR_TEMP_TEMPLATE = "clr_temp_tpl"; +constexpr const char *const MQTT_COLOR_TEMP_VALUE_TEMPLATE = "clr_temp_val_tpl"; +constexpr const char *const MQTT_CLEANING_TOPIC = "cln_t"; +constexpr const char *const MQTT_CLEANING_TEMPLATE = "cln_tpl"; +constexpr const char *const MQTT_COMMAND_OFF_TEMPLATE = "cmd_off_tpl"; +constexpr const char *const MQTT_COMMAND_ON_TEMPLATE = "cmd_on_tpl"; +constexpr const char *const MQTT_COMMAND_TOPIC = "cmd_t"; +constexpr const char *const MQTT_COMMAND_TEMPLATE = "cmd_tpl"; +constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req"; +constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; +constexpr const char *const MQTT_DEVICE = "dev"; +constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; +constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; +constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl"; +constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "en"; +constexpr const char *const MQTT_ERROR_TOPIC = "err_t"; +constexpr const char *const MQTT_ERROR_TEMPLATE = "err_tpl"; +constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fanspd_t"; +constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fanspd_tpl"; +constexpr const char *const MQTT_FAN_SPEED_LIST = "fanspd_lst"; +constexpr const char *const MQTT_FLASH_TIME_LONG = "flsh_tlng"; +constexpr const char *const MQTT_FLASH_TIME_SHORT = "flsh_tsht"; +constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t"; +constexpr const char *const MQTT_EFFECT_LIST = "fx_list"; +constexpr const char *const MQTT_EFFECT_STATE_TOPIC = "fx_stat_t"; +constexpr const char *const MQTT_EFFECT_TEMPLATE = "fx_tpl"; +constexpr const char *const MQTT_EFFECT_VALUE_TEMPLATE = "fx_val_tpl"; +constexpr const char *const MQTT_EXPIRE_AFTER = "exp_aft"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_cmd_tpl"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_cmd_t"; +constexpr const char *const MQTT_FAN_MODE_STATE_TEMPLATE = "fan_mode_stat_tpl"; +constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC = "fan_mode_stat_t"; +constexpr const char *const MQTT_FORCE_UPDATE = "frc_upd"; +constexpr const char *const MQTT_GREEN_TEMPLATE = "g_tpl"; +constexpr const char *const MQTT_HOLD_COMMAND_TEMPLATE = "hold_cmd_tpl"; +constexpr const char *const MQTT_HOLD_COMMAND_TOPIC = "hold_cmd_t"; +constexpr const char *const MQTT_HOLD_STATE_TEMPLATE = "hold_stat_tpl"; +constexpr const char *const MQTT_HOLD_STATE_TOPIC = "hold_stat_t"; +constexpr const char *const MQTT_HS_COMMAND_TOPIC = "hs_cmd_t"; +constexpr const char *const MQTT_HS_STATE_TOPIC = "hs_stat_t"; +constexpr const char *const MQTT_HS_VALUE_TEMPLATE = "hs_val_tpl"; +constexpr const char *const MQTT_ICON = "ic"; +constexpr const char *const MQTT_INITIAL = "init"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "hum_cmd_t"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "hum_cmd_tpl"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "hum_stat_t"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "hum_state_tpl"; +constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attr"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attr_t"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TEMPLATE = "json_attr_tpl"; +constexpr const char *const MQTT_LAST_RESET_TOPIC = "lrst_t"; +constexpr const char *const MQTT_LAST_RESET_VALUE_TEMPLATE = "lrst_val_tpl"; +constexpr const char *const MQTT_MAX = "max"; +constexpr const char *const MQTT_MIN = "min"; +constexpr const char *const MQTT_MAX_HUMIDITY = "max_hum"; +constexpr const char *const MQTT_MIN_HUMIDITY = "min_hum"; +constexpr const char *const MQTT_MAX_MIREDS = "max_mirs"; +constexpr const char *const MQTT_MIN_MIREDS = "min_mirs"; +constexpr const char *const MQTT_MAX_TEMP = "max_temp"; +constexpr const char *const MQTT_MIN_TEMP = "min_temp"; +constexpr const char *const MQTT_MODE_COMMAND_TEMPLATE = "mode_cmd_tpl"; +constexpr const char *const MQTT_MODE_COMMAND_TOPIC = "mode_cmd_t"; +constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_stat_t"; +constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_stat_tpl"; +constexpr const char *const MQTT_MODES = "modes"; +constexpr const char *const MQTT_NAME = "name"; +constexpr const char *const MQTT_OFF_DELAY = "off_dly"; +constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_cmd_type"; +constexpr const char *const MQTT_OPTIONS = "ops"; +constexpr const char *const MQTT_OPTIMISTIC = "opt"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "osc_cmd_t"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TEMPLATE = "osc_cmd_tpl"; +constexpr const char *const MQTT_OSCILLATION_STATE_TOPIC = "osc_stat_t"; +constexpr const char *const MQTT_OSCILLATION_VALUE_TEMPLATE = "osc_val_tpl"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TOPIC = "pct_cmd_t"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "pct_cmd_tpl"; +constexpr const char *const MQTT_PERCENTAGE_STATE_TOPIC = "pct_stat_t"; +constexpr const char *const MQTT_PERCENTAGE_VALUE_TEMPLATE = "pct_val_tpl"; +constexpr const char *const MQTT_PAYLOAD = "pl"; +constexpr const char *const MQTT_PAYLOAD_ARM_AWAY = "pl_arm_away"; +constexpr const char *const MQTT_PAYLOAD_ARM_HOME = "pl_arm_home"; +constexpr const char *const MQTT_PAYLOAD_ARM_NIGHT = "pl_arm_nite"; +constexpr const char *const MQTT_PAYLOAD_ARM_VACATION = "pl_arm_vacation"; +constexpr const char *const MQTT_PAYLOAD_ARM_CUSTOM_BYPASS = "pl_arm_custom_b"; +constexpr const char *const MQTT_PAYLOAD_AVAILABLE = "pl_avail"; +constexpr const char *const MQTT_PAYLOAD_CLEAN_SPOT = "pl_cln_sp"; +constexpr const char *const MQTT_PAYLOAD_CLOSE = "pl_cls"; +constexpr const char *const MQTT_PAYLOAD_DISARM = "pl_disarm"; +constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd"; +constexpr const char *const MQTT_PAYLOAD_HOME = "pl_home"; +constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock"; +constexpr const char *const MQTT_PAYLOAD_LOCATE = "pl_loc"; +constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd"; +constexpr const char *const MQTT_PAYLOAD_MEDIUM_SPEED = "pl_med_spd"; +constexpr const char *const MQTT_PAYLOAD_NOT_AVAILABLE = "pl_not_avail"; +constexpr const char *const MQTT_PAYLOAD_NOT_HOME = "pl_not_home"; +constexpr const char *const MQTT_PAYLOAD_OFF = "pl_off"; +constexpr const char *const MQTT_PAYLOAD_OFF_SPEED = "pl_off_spd"; +constexpr const char *const MQTT_PAYLOAD_ON = "pl_on"; +constexpr const char *const MQTT_PAYLOAD_OPEN = "pl_open"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_OFF = "pl_osc_off"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_ON = "pl_osc_on"; +constexpr const char *const MQTT_PAYLOAD_PAUSE = "pl_paus"; +constexpr const char *const MQTT_PAYLOAD_RESET = "pl_rst"; +constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "pl_rst_hum"; +constexpr const char *const MQTT_PAYLOAD_RESET_MODE = "pl_rst_mode"; +constexpr const char *const MQTT_PAYLOAD_RESET_PERCENTAGE = "pl_rst_pct"; +constexpr const char *const MQTT_PAYLOAD_RESET_PRESET_MODE = "pl_rst_pr_mode"; +constexpr const char *const MQTT_PAYLOAD_STOP = "pl_stop"; +constexpr const char *const MQTT_PAYLOAD_START = "pl_strt"; +constexpr const char *const MQTT_PAYLOAD_START_PAUSE = "pl_stpa"; +constexpr const char *const MQTT_PAYLOAD_RETURN_TO_BASE = "pl_ret"; +constexpr const char *const MQTT_PAYLOAD_TURN_OFF = "pl_toff"; +constexpr const char *const MQTT_PAYLOAD_TURN_ON = "pl_ton"; +constexpr const char *const MQTT_PAYLOAD_UNLOCK = "pl_unlk"; +constexpr const char *const MQTT_POSITION_CLOSED = "pos_clsd"; +constexpr const char *const MQTT_POSITION_OPEN = "pos_open"; +constexpr const char *const MQTT_POWER_COMMAND_TOPIC = "pow_cmd_t"; +constexpr const char *const MQTT_POWER_STATE_TOPIC = "pow_stat_t"; +constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "pow_stat_tpl"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "pr_mode_cmd_t"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TEMPLATE = "pr_mode_cmd_tpl"; +constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "pr_mode_stat_t"; +constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "pr_mode_val_tpl"; +constexpr const char *const MQTT_PRESET_MODES = "pr_modes"; +constexpr const char *const MQTT_RED_TEMPLATE = "r_tpl"; +constexpr const char *const MQTT_RETAIN = "ret"; +constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_cmd_tpl"; +constexpr const char *const MQTT_RGB_COMMAND_TOPIC = "rgb_cmd_t"; +constexpr const char *const MQTT_RGB_STATE_TOPIC = "rgb_stat_t"; +constexpr const char *const MQTT_RGB_VALUE_TEMPLATE = "rgb_val_tpl"; +constexpr const char *const MQTT_RGBW_COMMAND_TEMPLATE = "rgbw_cmd_tpl"; +constexpr const char *const MQTT_RGBW_COMMAND_TOPIC = "rgbw_cmd_t"; +constexpr const char *const MQTT_RGBW_STATE_TOPIC = "rgbw_stat_t"; +constexpr const char *const MQTT_RGBW_VALUE_TEMPLATE = "rgbw_val_tpl"; +constexpr const char *const MQTT_RGBWW_COMMAND_TEMPLATE = "rgbww_cmd_tpl"; +constexpr const char *const MQTT_RGBWW_COMMAND_TOPIC = "rgbww_cmd_t"; +constexpr const char *const MQTT_RGBWW_STATE_TOPIC = "rgbww_stat_t"; +constexpr const char *const MQTT_RGBWW_VALUE_TEMPLATE = "rgbww_val_tpl"; +constexpr const char *const MQTT_SEND_COMMAND_TOPIC = "send_cmd_t"; +constexpr const char *const MQTT_SEND_IF_OFF = "send_if_off"; +constexpr const char *const MQTT_SET_FAN_SPEED_TOPIC = "set_fan_spd_t"; +constexpr const char *const MQTT_SET_POSITION_TEMPLATE = "set_pos_tpl"; +constexpr const char *const MQTT_SET_POSITION_TOPIC = "set_pos_t"; +constexpr const char *const MQTT_POSITION_TOPIC = "pos_t"; +constexpr const char *const MQTT_POSITION_TEMPLATE = "pos_tpl"; +constexpr const char *const MQTT_SPEED_COMMAND_TOPIC = "spd_cmd_t"; +constexpr const char *const MQTT_SPEED_STATE_TOPIC = "spd_stat_t"; +constexpr const char *const MQTT_SPEED_RANGE_MIN = "spd_rng_min"; +constexpr const char *const MQTT_SPEED_RANGE_MAX = "spd_rng_max"; +constexpr const char *const MQTT_SPEED_VALUE_TEMPLATE = "spd_val_tpl"; +constexpr const char *const MQTT_SPEEDS = "spds"; +constexpr const char *const MQTT_SOURCE_TYPE = "src_type"; +constexpr const char *const MQTT_STATE_CLASS = "stat_cla"; +constexpr const char *const MQTT_STATE_CLOSED = "stat_clsd"; +constexpr const char *const MQTT_STATE_CLOSING = "stat_closing"; +constexpr const char *const MQTT_STATE_OFF = "stat_off"; +constexpr const char *const MQTT_STATE_ON = "stat_on"; +constexpr const char *const MQTT_STATE_OPEN = "stat_open"; +constexpr const char *const MQTT_STATE_OPENING = "stat_opening"; +constexpr const char *const MQTT_STATE_STOPPED = "stat_stopped"; +constexpr const char *const MQTT_STATE_LOCKED = "stat_locked"; +constexpr const char *const MQTT_STATE_UNLOCKED = "stat_unlocked"; +constexpr const char *const MQTT_STATE_TOPIC = "stat_t"; +constexpr const char *const MQTT_STATE_TEMPLATE = "stat_tpl"; +constexpr const char *const MQTT_STATE_VALUE_TEMPLATE = "stat_val_tpl"; +constexpr const char *const MQTT_STEP = "step"; +constexpr const char *const MQTT_SUBTYPE = "stype"; +constexpr const char *const MQTT_SUPPORTED_FEATURES = "sup_feat"; +constexpr const char *const MQTT_SUPPORTED_COLOR_MODES = "sup_clrm"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_cmd_tpl"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC = "swing_mode_cmd_t"; +constexpr const char *const MQTT_SWING_MODE_STATE_TEMPLATE = "swing_mode_stat_tpl"; +constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC = "swing_mode_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temp_cmd_tpl"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temp_cmd_t"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temp_hi_cmd_tpl"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC = "temp_hi_cmd_t"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE = "temp_hi_stat_tpl"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC = "temp_hi_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE = "temp_lo_cmd_tpl"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC = "temp_lo_cmd_t"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TEMPLATE = "temp_lo_stat_tpl"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC = "temp_lo_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temp_stat_tpl"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC = "temp_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_UNIT = "temp_unit"; +constexpr const char *const MQTT_TILT_CLOSED_VALUE = "tilt_clsd_val"; +constexpr const char *const MQTT_TILT_COMMAND_TOPIC = "tilt_cmd_t"; +constexpr const char *const MQTT_TILT_COMMAND_TEMPLATE = "tilt_cmd_tpl"; +constexpr const char *const MQTT_TILT_INVERT_STATE = "tilt_inv_stat"; +constexpr const char *const MQTT_TILT_MAX = "tilt_max"; +constexpr const char *const MQTT_TILT_MIN = "tilt_min"; +constexpr const char *const MQTT_TILT_OPENED_VALUE = "tilt_opnd_val"; +constexpr const char *const MQTT_TILT_OPTIMISTIC = "tilt_opt"; +constexpr const char *const MQTT_TILT_STATUS_TOPIC = "tilt_status_t"; +constexpr const char *const MQTT_TILT_STATUS_TEMPLATE = "tilt_status_tpl"; +constexpr const char *const MQTT_TOPIC = "t"; +constexpr const char *const MQTT_UNIQUE_ID = "uniq_id"; +constexpr const char *const MQTT_UNIT_OF_MEASUREMENT = "unit_of_meas"; +constexpr const char *const MQTT_VALUE_TEMPLATE = "val_tpl"; +constexpr const char *const MQTT_WHITE_COMMAND_TOPIC = "whit_cmd_t"; +constexpr const char *const MQTT_WHITE_SCALE = "whit_scl"; +constexpr const char *const MQTT_WHITE_VALUE_COMMAND_TOPIC = "whit_val_cmd_t"; +constexpr const char *const MQTT_WHITE_VALUE_SCALE = "whit_val_scl"; +constexpr const char *const MQTT_WHITE_VALUE_STATE_TOPIC = "whit_val_stat_t"; +constexpr const char *const MQTT_WHITE_VALUE_TEMPLATE = "whit_val_tpl"; +constexpr const char *const MQTT_XY_COMMAND_TOPIC = "xy_cmd_t"; +constexpr const char *const MQTT_XY_STATE_TOPIC = "xy_stat_t"; +constexpr const char *const MQTT_XY_VALUE_TEMPLATE = "xy_val_tpl"; + +constexpr const char *const MQTT_DEVICE_CONNECTIONS = "cns"; +constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "ids"; +constexpr const char *const MQTT_DEVICE_NAME = "name"; +constexpr const char *const MQTT_DEVICE_MANUFACTURER = "mf"; +constexpr const char *const MQTT_DEVICE_MODEL = "mdl"; +constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; +constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; + +#else + +constexpr const char *const MQTT_ACTION_TOPIC = "action_topic"; +constexpr const char *const MQTT_ACTION_TEMPLATE = "action_template"; +constexpr const char *const MQTT_AUTOMATION_TYPE = "automation_type"; +constexpr const char *const MQTT_AUX_COMMAND_TOPIC = "aux_command_topic"; +constexpr const char *const MQTT_AUX_STATE_TEMPLATE = "aux_state_template"; +constexpr const char *const MQTT_AUX_STATE_TOPIC = "aux_state_topic"; +constexpr const char *const MQTT_AVAILABILITY = "availability"; +constexpr const char *const MQTT_AVAILABILITY_MODE = "availability_mode"; +constexpr const char *const MQTT_AVAILABILITY_TOPIC = "availability_topic"; +constexpr const char *const MQTT_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic"; +constexpr const char *const MQTT_BLUE_TEMPLATE = "blue_template"; +constexpr const char *const MQTT_BRIGHTNESS_COMMAND_TOPIC = "brightness_command_topic"; +constexpr const char *const MQTT_BRIGHTNESS_SCALE = "brightness_scale"; +constexpr const char *const MQTT_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic"; +constexpr const char *const MQTT_BRIGHTNESS_TEMPLATE = "brightness_template"; +constexpr const char *const MQTT_BRIGHTNESS_VALUE_TEMPLATE = "brightness_value_template"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template"; +constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "battery_level_topic"; +constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "battery_level_template"; +constexpr const char *const MQTT_CONFIGURATION_URL = "configuration_url"; +constexpr const char *const MQTT_CHARGING_TOPIC = "charging_topic"; +constexpr const char *const MQTT_CHARGING_TEMPLATE = "charging_template"; +constexpr const char *const MQTT_COLOR_MODE = "color_mode"; +constexpr const char *const MQTT_COLOR_MODE_STATE_TOPIC = "color_mode_state_topic"; +constexpr const char *const MQTT_COLOR_MODE_VALUE_TEMPLATE = "color_mode_value_template"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic"; +constexpr const char *const MQTT_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic"; +constexpr const char *const MQTT_COLOR_TEMP_TEMPLATE = "color_temp_template"; +constexpr const char *const MQTT_COLOR_TEMP_VALUE_TEMPLATE = "color_temp_value_template"; +constexpr const char *const MQTT_CLEANING_TOPIC = "cleaning_topic"; +constexpr const char *const MQTT_CLEANING_TEMPLATE = "cleaning_template"; +constexpr const char *const MQTT_COMMAND_OFF_TEMPLATE = "command_off_template"; +constexpr const char *const MQTT_COMMAND_ON_TEMPLATE = "command_on_template"; +constexpr const char *const MQTT_COMMAND_TOPIC = "command_topic"; +constexpr const char *const MQTT_COMMAND_TEMPLATE = "command_template"; +constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required"; +constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; +constexpr const char *const MQTT_DEVICE = "device"; +constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; +constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; +constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template"; +constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "enabled_by_default"; +constexpr const char *const MQTT_ERROR_TOPIC = "error_topic"; +constexpr const char *const MQTT_ERROR_TEMPLATE = "error_template"; +constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fan_speed_topic"; +constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fan_speed_template"; +constexpr const char *const MQTT_FAN_SPEED_LIST = "fan_speed_list"; +constexpr const char *const MQTT_FLASH_TIME_LONG = "flash_time_long"; +constexpr const char *const MQTT_FLASH_TIME_SHORT = "flash_time_short"; +constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic"; +constexpr const char *const MQTT_EFFECT_LIST = "effect_list"; +constexpr const char *const MQTT_EFFECT_STATE_TOPIC = "effect_state_topic"; +constexpr const char *const MQTT_EFFECT_TEMPLATE = "effect_template"; +constexpr const char *const MQTT_EFFECT_VALUE_TEMPLATE = "effect_value_template"; +constexpr const char *const MQTT_EXPIRE_AFTER = "expire_after"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic"; +constexpr const char *const MQTT_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template"; +constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic"; +constexpr const char *const MQTT_FORCE_UPDATE = "force_update"; +constexpr const char *const MQTT_GREEN_TEMPLATE = "green_template"; +constexpr const char *const MQTT_HOLD_COMMAND_TEMPLATE = "hold_command_template"; +constexpr const char *const MQTT_HOLD_COMMAND_TOPIC = "hold_command_topic"; +constexpr const char *const MQTT_HOLD_STATE_TEMPLATE = "hold_state_template"; +constexpr const char *const MQTT_HOLD_STATE_TOPIC = "hold_state_topic"; +constexpr const char *const MQTT_HS_COMMAND_TOPIC = "hs_command_topic"; +constexpr const char *const MQTT_HS_STATE_TOPIC = "hs_state_topic"; +constexpr const char *const MQTT_HS_VALUE_TEMPLATE = "hs_value_template"; +constexpr const char *const MQTT_ICON = "icon"; +constexpr const char *const MQTT_INITIAL = "initial"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"; +constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attributes"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attributes_topic"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TEMPLATE = "json_attributes_template"; +constexpr const char *const MQTT_LAST_RESET_TOPIC = "last_reset_topic"; +constexpr const char *const MQTT_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"; +constexpr const char *const MQTT_MAX = "max"; +constexpr const char *const MQTT_MIN = "min"; +constexpr const char *const MQTT_MAX_HUMIDITY = "max_humidity"; +constexpr const char *const MQTT_MIN_HUMIDITY = "min_humidity"; +constexpr const char *const MQTT_MAX_MIREDS = "max_mireds"; +constexpr const char *const MQTT_MIN_MIREDS = "min_mireds"; +constexpr const char *const MQTT_MAX_TEMP = "max_temp"; +constexpr const char *const MQTT_MIN_TEMP = "min_temp"; +constexpr const char *const MQTT_MODE_COMMAND_TEMPLATE = "mode_command_template"; +constexpr const char *const MQTT_MODE_COMMAND_TOPIC = "mode_command_topic"; +constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_state_topic"; +constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_state_template"; +constexpr const char *const MQTT_MODES = "modes"; +constexpr const char *const MQTT_NAME = "name"; +constexpr const char *const MQTT_OFF_DELAY = "off_delay"; +constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_command_type"; +constexpr const char *const MQTT_OPTIONS = "options"; +constexpr const char *const MQTT_OPTIMISTIC = "optimistic"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"; +constexpr const char *const MQTT_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"; +constexpr const char *const MQTT_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"; +constexpr const char *const MQTT_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"; +constexpr const char *const MQTT_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"; +constexpr const char *const MQTT_PAYLOAD = "payload"; +constexpr const char *const MQTT_PAYLOAD_ARM_AWAY = "payload_arm_away"; +constexpr const char *const MQTT_PAYLOAD_ARM_HOME = "payload_arm_home"; +constexpr const char *const MQTT_PAYLOAD_ARM_NIGHT = "payload_arm_night"; +constexpr const char *const MQTT_PAYLOAD_ARM_VACATION = "payload_arm_vacation"; +constexpr const char *const MQTT_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"; +constexpr const char *const MQTT_PAYLOAD_AVAILABLE = "payload_available"; +constexpr const char *const MQTT_PAYLOAD_CLEAN_SPOT = "payload_clean_spot"; +constexpr const char *const MQTT_PAYLOAD_CLOSE = "payload_close"; +constexpr const char *const MQTT_PAYLOAD_DISARM = "payload_disarm"; +constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed"; +constexpr const char *const MQTT_PAYLOAD_HOME = "payload_home"; +constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock"; +constexpr const char *const MQTT_PAYLOAD_LOCATE = "payload_locate"; +constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed"; +constexpr const char *const MQTT_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed"; +constexpr const char *const MQTT_PAYLOAD_NOT_AVAILABLE = "payload_not_available"; +constexpr const char *const MQTT_PAYLOAD_NOT_HOME = "payload_not_home"; +constexpr const char *const MQTT_PAYLOAD_OFF = "payload_off"; +constexpr const char *const MQTT_PAYLOAD_OFF_SPEED = "payload_off_speed"; +constexpr const char *const MQTT_PAYLOAD_ON = "payload_on"; +constexpr const char *const MQTT_PAYLOAD_OPEN = "payload_open"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"; +constexpr const char *const MQTT_PAYLOAD_PAUSE = "payload_pause"; +constexpr const char *const MQTT_PAYLOAD_RESET = "payload_reset"; +constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidity"; +constexpr const char *const MQTT_PAYLOAD_RESET_MODE = "payload_reset_mode"; +constexpr const char *const MQTT_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"; +constexpr const char *const MQTT_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"; +constexpr const char *const MQTT_PAYLOAD_STOP = "payload_stop"; +constexpr const char *const MQTT_PAYLOAD_START = "payload_start"; +constexpr const char *const MQTT_PAYLOAD_START_PAUSE = "payload_start_pause"; +constexpr const char *const MQTT_PAYLOAD_RETURN_TO_BASE = "payload_return_to_base"; +constexpr const char *const MQTT_PAYLOAD_TURN_OFF = "payload_turn_off"; +constexpr const char *const MQTT_PAYLOAD_TURN_ON = "payload_turn_on"; +constexpr const char *const MQTT_PAYLOAD_UNLOCK = "payload_unlock"; +constexpr const char *const MQTT_POSITION_CLOSED = "position_closed"; +constexpr const char *const MQTT_POSITION_OPEN = "position_open"; +constexpr const char *const MQTT_POWER_COMMAND_TOPIC = "power_command_topic"; +constexpr const char *const MQTT_POWER_STATE_TOPIC = "power_state_topic"; +constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "power_state_template"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"; +constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"; +constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"; +constexpr const char *const MQTT_PRESET_MODES = "preset_modes"; +constexpr const char *const MQTT_RED_TEMPLATE = "red_template"; +constexpr const char *const MQTT_RETAIN = "retain"; +constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_command_template"; +constexpr const char *const MQTT_RGB_COMMAND_TOPIC = "rgb_command_topic"; +constexpr const char *const MQTT_RGB_STATE_TOPIC = "rgb_state_topic"; +constexpr const char *const MQTT_RGB_VALUE_TEMPLATE = "rgb_value_template"; +constexpr const char *const MQTT_RGBW_COMMAND_TEMPLATE = "rgbw_command_template"; +constexpr const char *const MQTT_RGBW_COMMAND_TOPIC = "rgbw_command_topic"; +constexpr const char *const MQTT_RGBW_STATE_TOPIC = "rgbw_state_topic"; +constexpr const char *const MQTT_RGBW_VALUE_TEMPLATE = "rgbw_value_template"; +constexpr const char *const MQTT_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template"; +constexpr const char *const MQTT_RGBWW_COMMAND_TOPIC = "rgbww_command_topic"; +constexpr const char *const MQTT_RGBWW_STATE_TOPIC = "rgbww_state_topic"; +constexpr const char *const MQTT_RGBWW_VALUE_TEMPLATE = "rgbww_value_template"; +constexpr const char *const MQTT_SEND_COMMAND_TOPIC = "send_command_topic"; +constexpr const char *const MQTT_SEND_IF_OFF = "send_if_off"; +constexpr const char *const MQTT_SET_FAN_SPEED_TOPIC = "set_fan_speed_topic"; +constexpr const char *const MQTT_SET_POSITION_TEMPLATE = "set_position_template"; +constexpr const char *const MQTT_SET_POSITION_TOPIC = "set_position_topic"; +constexpr const char *const MQTT_POSITION_TOPIC = "position_topic"; +constexpr const char *const MQTT_POSITION_TEMPLATE = "position_template"; +constexpr const char *const MQTT_SPEED_COMMAND_TOPIC = "speed_command_topic"; +constexpr const char *const MQTT_SPEED_STATE_TOPIC = "speed_state_topic"; +constexpr const char *const MQTT_SPEED_RANGE_MIN = "speed_range_min"; +constexpr const char *const MQTT_SPEED_RANGE_MAX = "speed_range_max"; +constexpr const char *const MQTT_SPEED_VALUE_TEMPLATE = "speed_value_template"; +constexpr const char *const MQTT_SPEEDS = "speeds"; +constexpr const char *const MQTT_SOURCE_TYPE = "source_type"; +constexpr const char *const MQTT_STATE_CLASS = "state_class"; +constexpr const char *const MQTT_STATE_CLOSED = "state_closed"; +constexpr const char *const MQTT_STATE_CLOSING = "state_closing"; +constexpr const char *const MQTT_STATE_OFF = "state_off"; +constexpr const char *const MQTT_STATE_ON = "state_on"; +constexpr const char *const MQTT_STATE_OPEN = "state_open"; +constexpr const char *const MQTT_STATE_OPENING = "state_opening"; +constexpr const char *const MQTT_STATE_STOPPED = "state_stopped"; +constexpr const char *const MQTT_STATE_LOCKED = "state_locked"; +constexpr const char *const MQTT_STATE_UNLOCKED = "state_unlocked"; +constexpr const char *const MQTT_STATE_TOPIC = "state_topic"; +constexpr const char *const MQTT_STATE_TEMPLATE = "state_template"; +constexpr const char *const MQTT_STATE_VALUE_TEMPLATE = "state_value_template"; +constexpr const char *const MQTT_STEP = "step"; +constexpr const char *const MQTT_SUBTYPE = "subtype"; +constexpr const char *const MQTT_SUPPORTED_FEATURES = "supported_features"; +constexpr const char *const MQTT_SUPPORTED_COLOR_MODES = "supported_color_modes"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic"; +constexpr const char *const MQTT_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template"; +constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temperature_command_template"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temperature_command_topic"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC = "temperature_high_command_topic"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE = "temperature_high_state_template"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC = "temperature_high_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE = "temperature_low_command_template"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC = "temperature_low_command_topic"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TEMPLATE = "temperature_low_state_template"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC = "temperature_low_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temperature_state_template"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC = "temperature_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_UNIT = "temperature_unit"; +constexpr const char *const MQTT_TILT_CLOSED_VALUE = "tilt_closed_value"; +constexpr const char *const MQTT_TILT_COMMAND_TOPIC = "tilt_command_topic"; +constexpr const char *const MQTT_TILT_COMMAND_TEMPLATE = "tilt_command_template"; +constexpr const char *const MQTT_TILT_INVERT_STATE = "tilt_invert_state"; +constexpr const char *const MQTT_TILT_MAX = "tilt_max"; +constexpr const char *const MQTT_TILT_MIN = "tilt_min"; +constexpr const char *const MQTT_TILT_OPENED_VALUE = "tilt_opened_value"; +constexpr const char *const MQTT_TILT_OPTIMISTIC = "tilt_optimistic"; +constexpr const char *const MQTT_TILT_STATUS_TOPIC = "tilt_status_topic"; +constexpr const char *const MQTT_TILT_STATUS_TEMPLATE = "tilt_status_template"; +constexpr const char *const MQTT_TOPIC = "topic"; +constexpr const char *const MQTT_UNIQUE_ID = "unique_id"; +constexpr const char *const MQTT_UNIT_OF_MEASUREMENT = "unit_of_measurement"; +constexpr const char *const MQTT_VALUE_TEMPLATE = "value_template"; +constexpr const char *const MQTT_WHITE_COMMAND_TOPIC = "white_command_topic"; +constexpr const char *const MQTT_WHITE_SCALE = "white_scale"; +constexpr const char *const MQTT_WHITE_VALUE_COMMAND_TOPIC = "white_value_command_topic"; +constexpr const char *const MQTT_WHITE_VALUE_SCALE = "white_value_scale"; +constexpr const char *const MQTT_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic"; +constexpr const char *const MQTT_WHITE_VALUE_TEMPLATE = "white_value_template"; +constexpr const char *const MQTT_XY_COMMAND_TOPIC = "xy_command_topic"; +constexpr const char *const MQTT_XY_STATE_TOPIC = "xy_state_topic"; +constexpr const char *const MQTT_XY_VALUE_TEMPLATE = "xy_value_template"; + +constexpr const char *const MQTT_DEVICE_CONNECTIONS = "connections"; +constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "identifiers"; +constexpr const char *const MQTT_DEVICE_NAME = "name"; +constexpr const char *const MQTT_DEVICE_MANUFACTURER = "manufacturer"; +constexpr const char *const MQTT_DEVICE_MODEL = "model"; +constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; +constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; +#endif + +// Additional MQTT fields where no abbreviation is defined in HA source +constexpr const char *const MQTT_ENTITY_CATEGORY = "entity_category"; +constexpr const char *const MQTT_MODE = "mode"; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index a414c261f0..7e42abcd05 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,12 +1,15 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_COVER namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.cover"; +static const char *const TAG = "mqtt.cover"; using namespace esphome::cover; @@ -21,7 +24,7 @@ void MQTTCoverComponent::setup() { }); if (traits.get_supports_position()) { this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto value = parse_float(payload); + auto value = parse_number(payload); if (!value.has_value()) { ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str()); return; @@ -33,7 +36,7 @@ void MQTTCoverComponent::setup() { } if (traits.get_supports_tilt()) { this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto value = parse_float(payload); + auto value = parse_number(payload); if (!value.has_value()) { ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str()); return; @@ -61,17 +64,21 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->cover_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); + auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { - root["optimistic"] = true; + root[MQTT_OPTIMISTIC] = true; } if (traits.get_supports_position()) { - root["position_topic"] = this->get_position_state_topic(); - root["set_position_topic"] = this->get_position_command_topic(); + config.state_topic = false; + root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } if (traits.get_supports_tilt()) { - root["tilt_status_topic"] = this->get_tilt_state_topic(); - root["tilt_command_topic"] = this->get_tilt_command_topic(); + root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic(); + root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic(); } if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; @@ -79,9 +86,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 +119,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 f115fe1bac..d58e3abc88 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,12 +1,16 @@ #include "mqtt_fan.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_FAN +#include "esphome/components/fan/fan_helpers.h" namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.fan"; +static const char *const TAG = "mqtt.fan"; using namespace esphome::fan; @@ -14,6 +18,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()); @@ -62,28 +68,70 @@ 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_number(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(); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->state_->make_call() + .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) + .perform(); +#pragma GCC diagnostic pop }); } auto f = std::bind(&MQTTFanComponent::publish_state, this); this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); } -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) { + +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()) { - root["oscillation_command_topic"] = this->get_oscillation_command_topic(); - root["oscillation_state_topic"] = this->get_oscillation_state_topic(); + 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()) { - root["speed_command_topic"] = this->get_speed_command_topic(); - root["speed_state_topic"] = this->get_speed_state_topic(); + 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(); } + +void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (this->state_->get_traits().supports_oscillation()) { + root[MQTT_OSCILLATION_COMMAND_TOPIC] = this->get_oscillation_command_topic(); + root[MQTT_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[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic(); + root[MQTT_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); @@ -94,23 +142,33 @@ bool MQTTFanComponent::publish_state() { this->state_->oscillating ? "oscillate_on" : "oscillate_off"); failed = failed || !success; } - if (this->state_->get_traits().supports_speed()) { + 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; - switch (this->state_->speed) { - case FAN_SPEED_LOW: { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) + switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { + 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; } } +#pragma GCC diagnostic pop bool success = this->publish(this->get_speed_state_topic(), payload); failed = failed || !success; } @@ -122,3 +180,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 d392169435..54204a9e7f 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,20 +1,27 @@ #include "mqtt_light.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.light"; +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,31 +31,48 @@ 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[MQTT_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"); + JsonArray &effect_list = root.createNestedArray(MQTT_EFFECT_LIST); for (auto *effect : this->state_->get_effects()) effect_list.add(effect->get_name()); effect_list.add("None"); } } 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 +82,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..18e3a61417 --- /dev/null +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -0,0 +1,78 @@ +#include "mqtt_number.h" +#include "esphome/core/log.h" + +#include "mqtt_const.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_number(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[MQTT_MIN] = traits.get_min_value(); + root[MQTT_MAX] = traits.get_max_value(); + root[MQTT_STEP] = traits.get_step(); + if (!this->number_->traits.get_unit_of_measurement().empty()) + root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement(); + switch (this->number_->traits.get_mode()) { + case NUMBER_MODE_AUTO: + break; + case NUMBER_MODE_BOX: + root[MQTT_MODE] = "box"; + break; + case NUMBER_MODE_SLIDER: + root[MQTT_MODE] = "slider"; + break; + } + + 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..b8371de00e --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -0,0 +1,59 @@ +#include "mqtt_select.h" +#include "esphome/core/log.h" + +#include "mqtt_const.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(MQTT_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 f87e7651b9..dd6423e8f3 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -1,6 +1,9 @@ #include "mqtt_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_SENSOR #ifdef USE_DEEP_SLEEP @@ -10,7 +13,7 @@ namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.sensor"; +static const char *const TAG = "mqtt.sensor"; using namespace esphome::sensor; @@ -29,34 +32,31 @@ 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[MQTT_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(); + root[MQTT_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(); + root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; if (this->sensor_->get_force_update()) - root["force_update"] = true; + root[MQTT_FORCE_UPDATE] = true; + + if (this->sensor_->get_state_class() != STATE_CLASS_NONE) + root[MQTT_STATE_CLASS] = state_class_to_string(this->sensor_->get_state_class()); config.command_topic = false; } @@ -67,7 +67,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 +77,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 a01d6a6c6e..edaa6e7859 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -1,12 +1,15 @@ #include "mqtt_switch.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + +#ifdef USE_MQTT #ifdef USE_SWITCH namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.switch"; +static const char *const TAG = "mqtt.switch"; using namespace esphome::switch_; @@ -40,15 +43,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; + root[MQTT_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 +59,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 37d475d25d..7b89915649 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -1,20 +1,18 @@ #include "mqtt_text_sensor.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_TEXT_SENSOR namespace esphome { namespace mqtt { -static const char *TAG = "mqtt.text_sensor"; +static const char *const TAG = "mqtt.text_sensor"; 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/__init__.py b/esphome/components/mqtt_subscribe/__init__.py index 82e77f2f78..b17f321372 100644 --- a/esphome/components/mqtt_subscribe/__init__.py +++ b/esphome/components/mqtt_subscribe/__init__.py @@ -1,3 +1,3 @@ import esphome.codegen as cg -mqtt_subscribe_ns = cg.esphome_ns.namespace('mqtt_subscribe') +mqtt_subscribe_ns = cg.esphome_ns.namespace("mqtt_subscribe") diff --git a/esphome/components/mqtt_subscribe/sensor/__init__.py b/esphome/components/mqtt_subscribe/sensor/__init__.py index dedc2ac9a7..420d4f152c 100644 --- a/esphome/components/mqtt_subscribe/sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/sensor/__init__.py @@ -1,28 +1,44 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import mqtt, sensor -from esphome.const import CONF_ID, CONF_QOS, CONF_TOPIC, UNIT_EMPTY, ICON_EMPTY +from esphome.const import ( + CONF_ID, + CONF_QOS, + CONF_TOPIC, + STATE_CLASS_NONE, +) from .. import mqtt_subscribe_ns -DEPENDENCIES = ['mqtt'] +DEPENDENCIES = ["mqtt"] -CONF_MQTT_PARENT_ID = 'mqtt_parent_id' -MQTTSubscribeSensor = mqtt_subscribe_ns.class_('MQTTSubscribeSensor', sensor.Sensor, cg.Component) +CONF_MQTT_PARENT_ID = "mqtt_parent_id" +MQTTSubscribeSensor = mqtt_subscribe_ns.class_( + "MQTTSubscribeSensor", sensor.Sensor, cg.Component +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1).extend({ - cv.GenerateID(): cv.declare_id(MQTTSubscribeSensor), - cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), - cv.Required(CONF_TOPIC): cv.subscribe_topic, - cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MQTTSubscribeSensor), + cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), + cv.Required(CONF_TOPIC): cv.subscribe_topic, + cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - parent = yield cg.get_variable(config[CONF_MQTT_PARENT_ID]) + parent = await cg.get_variable(config[CONF_MQTT_PARENT_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_topic(config[CONF_TOPIC])) cg.add(var.set_qos(config[CONF_QOS])) diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp index 50977e98ca..273de10376 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp @@ -1,24 +1,28 @@ #include "mqtt_subscribe_sensor.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" namespace esphome { namespace mqtt_subscribe { -static const char *TAG = "mqtt_subscribe.sensor"; +static const char *const TAG = "mqtt_subscribe.sensor"; void MQTTSubscribeSensor::setup() { - mqtt::global_mqtt_client->subscribe(this->topic_, - [this](const std::string &topic, std::string payload) { - auto val = parse_float(payload); - if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); - this->publish_state(NAN); - return; - } + mqtt::global_mqtt_client->subscribe( + this->topic_, + [this](const std::string &topic, const std::string &payload) { + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + this->publish_state(NAN); + return; + } - this->publish_state(*val); - }, - this->qos_); + this->publish_state(*val); + }, + this->qos_); } float MQTTSubscribeSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } @@ -30,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/__init__.py b/esphome/components/mqtt_subscribe/text_sensor/__init__.py index c80909669b..477e4dec45 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/text_sensor/__init__.py @@ -4,26 +4,29 @@ from esphome.components import text_sensor, mqtt from esphome.const import CONF_ID, CONF_QOS, CONF_TOPIC from .. import mqtt_subscribe_ns -DEPENDENCIES = ['mqtt'] +DEPENDENCIES = ["mqtt"] -CONF_MQTT_PARENT_ID = 'mqtt_parent_id' -MQTTSubscribeTextSensor = mqtt_subscribe_ns.class_('MQTTSubscribeTextSensor', - text_sensor.TextSensor, cg.Component) +CONF_MQTT_PARENT_ID = "mqtt_parent_id" +MQTTSubscribeTextSensor = mqtt_subscribe_ns.class_( + "MQTTSubscribeTextSensor", text_sensor.TextSensor, cg.Component +) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(MQTTSubscribeTextSensor), - cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), - cv.Required(CONF_TOPIC): cv.subscribe_topic, - cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MQTTSubscribeTextSensor), + cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), + cv.Required(CONF_TOPIC): cv.subscribe_topic, + cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) - parent = yield cg.get_variable(config[CONF_MQTT_PARENT_ID]) + parent = await cg.get_variable(config[CONF_MQTT_PARENT_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_topic(config[CONF_TOPIC])) cg.add(var.set_qos(config[CONF_QOS])) 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 fdab5cf6d7..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,15 +1,19 @@ #include "mqtt_subscribe_text_sensor.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" +#include namespace esphome { namespace mqtt_subscribe { -static const char *TAG = "mqtt_subscribe.text_sensor"; +static const char *const TAG = "mqtt_subscribe.text_sensor"; void MQTTSubscribeTextSensor::setup() { - this->parent_->subscribe(this->topic_, - [this](const std::string &topic, std::string payload) { this->publish_state(payload); }, - this->qos_); + this->parent_->subscribe( + this->topic_, [this](const std::string &topic, const std::string &payload) { this->publish_state(payload); }, + this->qos_); } float MQTTSubscribeTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } void MQTTSubscribeTextSensor::set_qos(uint8_t qos) { this->qos_ = qos; } @@ -20,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 39bce9f32c..4b34e1d71a 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -1,10 +1,11 @@ #include "ms5611.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ms5611 { -static const char *TAG = "ms5611"; +static const char *const TAG = "ms5611"; static const uint8_t MS5611_ADDRESS = 0x77; static const uint8_t MS5611_CMD_ADC_READ = 0x00; @@ -74,30 +75,48 @@ void MS5611Component::read_pressure_(uint32_t raw_temperature) { const uint32_t raw_pressure = (uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2])); this->calculate_values_(raw_temperature, raw_pressure); } + +// Calculations are taken from the datasheet which can be found here: +// https://www.te.com/commerce/DocumentDelivery/DDEController?Action=showdoc&DocId=Data+Sheet%7FMS5611-01BA03%7FB3%7Fpdf%7FEnglish%7FENG_DS_MS5611-01BA03_B3.pdf%7FCAT-BLPS0036 +// Sections PRESSURE AND TEMPERATURE CALCULATION and SECOND ORDER TEMPERATURE COMPENSATION +// Variable names below match variable names from the datasheet but lowercased void MS5611Component::calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure) { - const int32_t d_t = int32_t(raw_temperature) - (uint32_t(this->prom_[4]) << 8); - float temperature = (2000 + (int64_t(d_t) * this->prom_[5]) / 8388608.0f) / 100.0f; + const uint32_t c1 = uint32_t(this->prom_[0]); + const uint32_t c2 = uint32_t(this->prom_[1]); + const uint16_t c3 = uint16_t(this->prom_[2]); + const uint16_t c4 = uint16_t(this->prom_[3]); + const int32_t c5 = int32_t(this->prom_[4]); + const uint16_t c6 = uint16_t(this->prom_[5]); + const uint32_t d1 = raw_pressure; + const int32_t d2 = raw_temperature; - float pressure_offset = (uint32_t(this->prom_[1]) << 16) + ((this->prom_[3] * d_t) >> 7); - float pressure_sensitivity = (uint32_t(this->prom_[0]) << 15) + ((this->prom_[2] * d_t) >> 8); + // Promote dt to 64 bit here to make the math below cleaner + const int64_t dt = d2 - (c5 << 8); + int32_t temp = (2000 + ((dt * c6) >> 23)); - if (temperature < 20.0f) { - const float t2 = (d_t * d_t) / 2147483648.0f; - const float temp20 = (temperature - 20.0f) * 100.0f; - float pressure_offset_2 = 2.5f * temp20 * temp20; - float pressure_sensitivity_2 = 1.25f * temp20 * temp20; - if (temp20 < -15.0f) { - const float temp15 = (temperature + 15.0f) * 100.0f; - pressure_offset_2 += 7.0f * temp15; - pressure_sensitivity_2 += 5.5f * temp15; + int64_t off = (c2 << 16) + ((dt * c4) >> 7); + int64_t sens = (c1 << 15) + ((dt * c3) >> 8); + + if (temp < 2000) { + const int32_t t2 = (dt * dt) >> 31; + int32_t off2 = ((5 * (temp - 2000) * (temp - 2000)) >> 1); + int32_t sens2 = ((5 * (temp - 2000) * (temp - 2000)) >> 2); + if (temp < -1500) { + off2 = (off2 + 7 * (temp + 1500) * (temp + 1500)); + sens2 = sens2 + ((11 * (temp + 1500) * (temp + 1500)) >> 1); } - temperature -= t2; - pressure_offset -= pressure_offset_2; - pressure_sensitivity -= pressure_sensitivity_2; + temp = temp - t2; + off = off - off2; + sens = sens - sens2; } - const float pressure = ((raw_pressure * pressure_sensitivity) / 2097152.0f - pressure_offset) / 3276800.0f; + // Here we multiply unsigned 32-bit by signed 64-bit using signed 64-bit math. + // Possible ranges of D1 and SENS from the datasheet guarantee + // that this multiplication does not overflow + const int32_t p = ((((d1 * sens) >> 21) - off) >> 15); + const float temperature = temp / 100.0f; + const float pressure = p / 100.0f; ESP_LOGD(TAG, "Got temperature=%0.02f°C pressure=%0.01fhPa", temperature, pressure); if (this->temperature_sensor_ != nullptr) diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py index ab9aac6d5f..5decb13436 100644 --- a/esphome/components/ms5611/sensor.py +++ b/esphome/components/ms5611/sensor.py @@ -1,31 +1,58 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, CONF_PRESSURE, \ - CONF_TEMPERATURE, ICON_THERMOMETER, UNIT_CELSIUS, ICON_GAUGE, \ - UNIT_HECTOPASCAL +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + ICON_GAUGE, + UNIT_HECTOPASCAL, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -ms5611_ns = cg.esphome_ns.namespace('ms5611') -MS5611Component = ms5611_ns.class_('MS5611Component', cg.PollingComponent, i2c.I2CDevice) +ms5611_ns = cg.esphome_ns.namespace("ms5611") +MS5611Component = ms5611_ns.class_( + "MS5611Component", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MS5611Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_PRESSURE): sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x77)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MS5611Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_PRESSURE in config: - sens = yield sensor.new_sensor(config[CONF_PRESSURE]) + sens = await sensor.new_sensor(config[CONF_PRESSURE]) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/my9231/__init__.py b/esphome/components/my9231/__init__.py index 7ca0a30cab..58419450cd 100644 --- a/esphome/components/my9231/__init__.py +++ b/esphome/components/my9231/__init__.py @@ -1,31 +1,39 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import (CONF_BIT_DEPTH, CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID, - CONF_NUM_CHANNELS, CONF_NUM_CHIPS) +from esphome.const import ( + CONF_BIT_DEPTH, + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, + CONF_NUM_CHANNELS, + CONF_NUM_CHIPS, +) -AUTO_LOAD = ['output'] -my9231_ns = cg.esphome_ns.namespace('my9231') -MY9231OutputComponent = my9231_ns.class_('MY9231OutputComponent', cg.Component) +AUTO_LOAD = ["output"] +my9231_ns = cg.esphome_ns.namespace("my9231") +MY9231OutputComponent = my9231_ns.class_("MY9231OutputComponent", cg.Component) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MY9231OutputComponent), - cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_NUM_CHANNELS, default=6): cv.int_range(min=3, max=1020), - cv.Optional(CONF_NUM_CHIPS, default=2): cv.int_range(min=1, max=255), - cv.Optional(CONF_BIT_DEPTH, default=16): cv.one_of(8, 12, 14, 16, int=True), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MY9231OutputComponent), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHANNELS, default=6): cv.int_range(min=3, max=1020), + cv.Optional(CONF_NUM_CHIPS, default=2): cv.int_range(min=1, max=255), + cv.Optional(CONF_BIT_DEPTH, default=16): cv.one_of(8, 12, 14, 16, int=True), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - di = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + di = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) cg.add(var.set_pin_di(di)) - dcki = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + dcki = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) cg.add(var.set_pin_dcki(dcki)) cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS])) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index 4b4603d56b..a97587b7be 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace my9231 { -static const char *TAG = "my9231.output"; +static const char *const TAG = "my9231.output"; // One-shot select (frame cycle repeat mode / frame cycle One-shot mode) static const uint8_t MY9231_CMD_ONE_SHOT_DISABLE = 0x0 << 6; 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/my9231/output.py b/esphome/components/my9231/output.py index c69649fd5e..a3c16fd49a 100644 --- a/esphome/components/my9231/output.py +++ b/esphome/components/my9231/output.py @@ -4,23 +4,24 @@ from esphome.components import output from esphome.const import CONF_CHANNEL, CONF_ID from . import MY9231OutputComponent -DEPENDENCIES = ['my9231'] +DEPENDENCIES = ["my9231"] -Channel = MY9231OutputComponent.class_('Channel', output.FloatOutput) +Channel = MY9231OutputComponent.class_("Channel", output.FloatOutput) -CONF_MY9231_ID = 'my9231_id' -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.GenerateID(CONF_MY9231_ID): cv.use_id(MY9231OutputComponent), - - cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.uint16_t, -}).extend(cv.COMPONENT_SCHEMA) +CONF_MY9231_ID = "my9231_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_MY9231_ID): cv.use_id(MY9231OutputComponent), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.uint16_t, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield output.register_output(var, config) + await output.register_output(var, config) - parent = yield cg.get_variable(config[CONF_MY9231_ID]) + parent = await cg.get_variable(config[CONF_MY9231_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py new file mode 100644 index 0000000000..4e3c3ca778 --- /dev/null +++ b/esphome/components/neopixelbus/_methods.py @@ -0,0 +1,418 @@ +from dataclasses import dataclass +from typing import Any, List +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_CHANNEL, + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_METHOD, + CONF_PIN, + CONF_SPEED, +) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32C3, +) +from esphome.core import CORE +from .const import ( + CONF_ASYNC, + CONF_BUS, + CHIP_400KBPS, + CHIP_800KBPS, + CHIP_APA106, + CHIP_DOTSTAR, + CHIP_LC8812, + CHIP_LPD6803, + CHIP_LPD8806, + CHIP_P9813, + CHIP_SK6812, + CHIP_TM1814, + CHIP_TM1829, + CHIP_TM1914, + CHIP_WS2801, + CHIP_WS2811, + CHIP_WS2812, + CHIP_WS2812X, + CHIP_WS2813, + ONE_WIRE_CHIPS, + TWO_WIRE_CHIPS, +) + +METHOD_BIT_BANG = "bit_bang" +METHOD_ESP8266_UART = "esp8266_uart" +METHOD_ESP8266_DMA = "esp8266_dma" +METHOD_ESP32_RMT = "esp32_rmt" +METHOD_ESP32_I2S = "esp32_i2s" +METHOD_SPI = "spi" + +CHANNEL_DYNAMIC = "dynamic" +BUS_DYNAMIC = "dynamic" +SPI_BUS_VSPI = "vspi" +SPI_BUS_HSPI = "hspi" +SPI_SPEEDS = [40e6, 20e6, 10e6, 5e6, 2e6, 1e6, 500e3] + + +def _esp32_rmt_default_channel(): + return { + VARIANT_ESP32S2: 1, + VARIANT_ESP32C3: 1, + }.get(get_esp32_variant(), 6) + + +def _validate_esp32_rmt_channel(value): + if isinstance(value, str) and value.lower() == CHANNEL_DYNAMIC: + value = CHANNEL_DYNAMIC + else: + value = cv.int_(value) + variant_channels = { + VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7, CHANNEL_DYNAMIC], + VARIANT_ESP32S2: [0, 1, 2, 3, CHANNEL_DYNAMIC], + VARIANT_ESP32C3: [0, 1, CHANNEL_DYNAMIC], + } + variant = get_esp32_variant() + if variant not in variant_channels: + raise cv.Invalid(f"{variant} does not support the rmt method") + if value not in variant_channels[variant]: + raise cv.Invalid(f"{variant} does not support rmt channel {value}") + return value + + +def _esp32_i2s_default_bus(): + return { + VARIANT_ESP32: 1, + VARIANT_ESP32S2: 0, + }.get(get_esp32_variant(), 0) + + +def _validate_esp32_i2s_bus(value): + if isinstance(value, str) and value.lower() == BUS_DYNAMIC: + value = BUS_DYNAMIC + else: + value = cv.int_(value) + variant_buses = { + VARIANT_ESP32: [0, 1, BUS_DYNAMIC], + VARIANT_ESP32S2: [0, BUS_DYNAMIC], + } + variant = get_esp32_variant() + if variant not in variant_buses: + raise cv.Invalid(f"{variant} does not support the i2s method") + if value not in variant_buses[variant]: + raise cv.Invalid(f"{variant} does not support i2s bus {value}") + return value + + +neo_ns = cg.global_ns + + +def _bit_bang_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEspBitBangMethod.h + # Some chips are only aliases + chip = { + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_TM1914: CHIP_TM1814, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + CHIP_WS2811: (neo_ns.NeoEspBitBangSpeedWs2811, False), + CHIP_WS2812X: (neo_ns.NeoEspBitBangSpeedWs2812x, False), + CHIP_SK6812: (neo_ns.NeoEspBitBangSpeedSk6812, False), + CHIP_TM1814: (neo_ns.NeoEspBitBangSpeedTm1814, True), + CHIP_TM1829: (neo_ns.NeoEspBitBangSpeedTm1829, True), + CHIP_800KBPS: (neo_ns.NeoEspBitBangSpeed800Kbps, False), + CHIP_400KBPS: (neo_ns.NeoEspBitBangSpeed400Kbps, False), + CHIP_APA106: (neo_ns.NeoEspBitBangSpeedApa106, False), + } + # For tm variants opposite of inverted is needed + speed, pinset_inverted = lookup[chip] + pinset = { + False: neo_ns.NeoEspPinset, + True: neo_ns.NeoEspPinsetInverted, + }[inverted != pinset_inverted] + return neo_ns.NeoEspBitBangMethodBase.template(speed, pinset) + + +def _bit_bang_extra_validate(config): + pin = config[CONF_PIN] + if CORE.is_esp8266 and not (0 <= pin <= 15): + # Due to use of w1ts + raise cv.Invalid("Bit bang only supports pins GPIO0-GPIO15 on ESP8266") + if CORE.is_esp32 and not (0 <= pin <= 31): + raise cv.Invalid("Bit bang only supports pins GPIO0-GPIO31 on ESP32") + + +def _esp8266_uart_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp8266UartMethod.h + uart_context, uart_base = { + False: (neo_ns.NeoEsp8266UartContext, neo_ns.NeoEsp8266Uart), + True: (neo_ns.NeoEsp8266UartInterruptContext, neo_ns.NeoEsp8266AsyncUart), + }[config[CONF_ASYNC]] + uart_feature = { + 0: neo_ns.UartFeature0, + 1: neo_ns.UartFeature1, + }[config[CONF_BUS]] + # Some chips are only aliases + chip = { + CHIP_WS2811: CHIP_WS2812X, + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_TM1914: CHIP_TM1814, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + CHIP_WS2812X: (neo_ns.NeoEsp8266UartSpeedWs2812x, False), + CHIP_SK6812: (neo_ns.NeoEsp8266UartSpeedSk6812, False), + CHIP_TM1814: (neo_ns.NeoEsp8266UartSpeedTm1814, True), + CHIP_TM1829: (neo_ns.NeoEsp8266UartSpeedTm1829, True), + CHIP_800KBPS: (neo_ns.NeoEsp8266UartSpeed800Kbps, False), + CHIP_400KBPS: (neo_ns.NeoEsp8266UartSpeed400Kbps, False), + CHIP_APA106: (neo_ns.NeoEsp8266UartSpeedApa106, False), + } + speed, uart_inverted = lookup[chip] + # For tm variants opposite of inverted is needed + inv = { + False: neo_ns.NeoEsp8266UartNotInverted, + True: neo_ns.NeoEsp8266UartInverted, + }[inverted != uart_inverted] + return neo_ns.NeoEsp8266UartMethodBase.template( + speed, uart_base.template(uart_feature, uart_context), inv + ) + + +def _esp8266_uart_extra_validate(config): + pin = config[CONF_PIN] + bus = config[CONF_METHOD][CONF_BUS] + right_pin = { + 0: 1, # U0TXD + 1: 2, # U1TXD + }[bus] + if pin != right_pin: + raise cv.Invalid(f"ESP8266 uart bus {bus} only supports pin GPIO{right_pin}") + + +def _esp8266_dma_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp8266DmaMethod.h + # Some chips are only aliases + chip = { + CHIP_WS2811: CHIP_WS2812X, + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_TM1914: CHIP_TM1814, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + (CHIP_WS2812X, False): neo_ns.NeoEsp8266DmaSpeedWs2812x, + (CHIP_SK6812, False): neo_ns.NeoEsp8266DmaSpeedSk6812, + (CHIP_TM1814, True): neo_ns.NeoEsp8266DmaInvertedSpeedTm1814, + (CHIP_TM1829, True): neo_ns.NeoEsp8266DmaInvertedSpeedTm1829, + (CHIP_800KBPS, False): neo_ns.NeoEsp8266DmaSpeed800Kbps, + (CHIP_400KBPS, False): neo_ns.NeoEsp8266DmaSpeed400Kbps, + (CHIP_APA106, False): neo_ns.NeoEsp8266DmaSpeedApa106, + (CHIP_WS2812X, True): neo_ns.NeoEsp8266DmaInvertedSpeedWs2812x, + (CHIP_SK6812, True): neo_ns.NeoEsp8266DmaInvertedSpeedSk6812, + (CHIP_TM1814, False): neo_ns.NeoEsp8266DmaSpeedTm1814, + (CHIP_TM1829, False): neo_ns.NeoEsp8266DmaSpeedTm1829, + (CHIP_800KBPS, True): neo_ns.NeoEsp8266DmaInvertedSpeed800Kbps, + (CHIP_400KBPS, True): neo_ns.NeoEsp8266DmaInvertedSpeed400Kbps, + (CHIP_APA106, True): neo_ns.NeoEsp8266DmaInvertedSpeedApa106, + } + speed = lookup[(chip, inverted)] + return neo_ns.NeoEsp8266DmaMethodBase.template(speed) + + +def _esp8266_dma_extra_validate(config): + if config[CONF_PIN] != 3: + raise cv.Invalid("ESP8266 dma method only supports pin GPIO3") + + +def _esp32_rmt_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp32RmtMethod.h + channel = { + 0: neo_ns.NeoEsp32RmtChannel0, + 1: neo_ns.NeoEsp32RmtChannel1, + 2: neo_ns.NeoEsp32RmtChannel2, + 3: neo_ns.NeoEsp32RmtChannel3, + 4: neo_ns.NeoEsp32RmtChannel4, + 5: neo_ns.NeoEsp32RmtChannel5, + 6: neo_ns.NeoEsp32RmtChannel6, + 7: neo_ns.NeoEsp32RmtChannel7, + CHANNEL_DYNAMIC: neo_ns.NeoEsp32RmtChannelN, + }[config[CONF_CHANNEL]] + # Some chips are only aliases + chip = { + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + (CHIP_WS2811, False): neo_ns.NeoEsp32RmtSpeedWs2811, + (CHIP_WS2812X, False): neo_ns.NeoEsp32RmtSpeedWs2812x, + (CHIP_SK6812, False): neo_ns.NeoEsp32RmtSpeedSk6812, + (CHIP_TM1814, False): neo_ns.NeoEsp32RmtSpeedTm1814, + (CHIP_TM1829, False): neo_ns.NeoEsp32RmtSpeedTm1829, + (CHIP_TM1914, False): neo_ns.NeoEsp32RmtSpeedTm1914, + (CHIP_800KBPS, False): neo_ns.NeoEsp32RmtSpeed800Kbps, + (CHIP_400KBPS, False): neo_ns.NeoEsp32RmtSpeed400Kbps, + (CHIP_APA106, False): neo_ns.NeoEsp32RmtSpeedApa106, + (CHIP_WS2811, True): neo_ns.NeoEsp32RmtInvertedSpeedWs2811, + (CHIP_WS2812X, True): neo_ns.NeoEsp32RmtInvertedSpeedWs2812x, + (CHIP_SK6812, True): neo_ns.NeoEsp32RmtInvertedSpeedSk6812, + (CHIP_TM1814, True): neo_ns.NeoEsp32RmtInvertedSpeedTm1814, + (CHIP_TM1829, True): neo_ns.NeoEsp32RmtInvertedSpeedTm1829, + (CHIP_TM1914, True): neo_ns.NeoEsp32RmtInvertedSpeedTm1914, + (CHIP_800KBPS, True): neo_ns.NeoEsp32RmtInvertedSpeed800Kbps, + (CHIP_400KBPS, True): neo_ns.NeoEsp32RmtInvertedSpeed400Kbps, + (CHIP_APA106, True): neo_ns.NeoEsp32RmtInvertedSpeedApa106, + } + speed = lookup[(chip, inverted)] + return neo_ns.NeoEsp32RmtMethodBase.template(speed, channel) + + +def _esp32_i2s_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp32I2sMethod.h + bus = { + 0: neo_ns.NeoEsp32I2sBusZero, + 1: neo_ns.NeoEsp32I2sBusOne, + BUS_DYNAMIC: neo_ns.NeoEsp32I2sBusN, + }[config[CONF_BUS]] + # Some chips are only aliases + chip = { + CHIP_WS2811: CHIP_WS2812X, + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + CHIP_WS2812X: (neo_ns.NeoEsp32I2sSpeedWs2812x, False), + CHIP_SK6812: (neo_ns.NeoEsp32I2sSpeedSk6812, False), + CHIP_TM1814: (neo_ns.NeoEsp32I2sSpeedTm1814, True), + CHIP_TM1914: (neo_ns.NeoEsp32I2sSpeedTm1914, True), + CHIP_TM1829: (neo_ns.NeoEsp32I2sSpeedTm1829, True), + CHIP_800KBPS: (neo_ns.NeoEsp32I2sSpeed800Kbps, False), + CHIP_400KBPS: (neo_ns.NeoEsp32I2sSpeed400Kbps, False), + CHIP_APA106: (neo_ns.NeoEsp32I2sSpeedApa106, False), + } + speed, inv_inverted = lookup[chip] + # For tm variants opposite of inverted is needed + inv = { + False: neo_ns.NeoEsp32I2sNotInverted, + True: neo_ns.NeoEsp32I2sInverted, + }[inverted != inv_inverted] + return neo_ns.NeoEsp32I2sMethodBase.template(speed, bus, inv) + + +def _spi_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/TwoWireSpiImple.h + spi_imple = { + None: neo_ns.TwoWireSpiImple, + SPI_BUS_VSPI: neo_ns.TwoWireSpiImple, + SPI_BUS_HSPI: neo_ns.TwoWireHspiImple, + }[config.get(CONF_BUS)] + spi_speed = { + 40e6: neo_ns.SpiSpeed40Mhz, + 20e6: neo_ns.SpiSpeed20Mhz, + 10e6: neo_ns.SpiSpeed10Mhz, + 5e6: neo_ns.SpiSpeed5Mhz, + 2e6: neo_ns.SpiSpeed2Mhz, + 1e6: neo_ns.SpiSpeed1Mhz, + 500e3: neo_ns.SpiSpeed500Khz, + }[config[CONF_SPEED]] + chip_method_base = { + CHIP_DOTSTAR: neo_ns.DotStarMethodBase, + CHIP_LPD6803: neo_ns.Lpd6803MethodBase, + CHIP_LPD8806: neo_ns.Lpd8806MethodBase, + CHIP_WS2801: neo_ns.Ws2801MethodBase, + CHIP_P9813: neo_ns.P9813MethodBase, + }[chip] + return chip_method_base.template(spi_imple.template(spi_speed)) + + +def _spi_extra_validate(config): + if CORE.is_esp32: + return + + if config[CONF_DATA_PIN] != 13 and config[CONF_CLOCK_PIN] != 14: + raise cv.Invalid( + "SPI only supports pins GPIO13 for data and GPIO14 for clock on ESP8266" + ) + + +@dataclass +class MethodDescriptor: + method_schema: Any + to_code: Any + supported_chips: List[str] + extra_validate: Any = None + + +METHODS = { + METHOD_BIT_BANG: MethodDescriptor( + method_schema={}, + to_code=_bit_bang_to_code, + extra_validate=_bit_bang_extra_validate, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP8266_UART: MethodDescriptor( + method_schema=cv.All( + cv.only_on_esp8266, + { + cv.Optional(CONF_ASYNC, default=False): cv.boolean, + cv.Optional(CONF_BUS, default=1): cv.int_range(min=0, max=1), + }, + ), + extra_validate=_esp8266_uart_extra_validate, + to_code=_esp8266_uart_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP8266_DMA: MethodDescriptor( + method_schema=cv.All(cv.only_on_esp8266, {}), + extra_validate=_esp8266_dma_extra_validate, + to_code=_esp8266_dma_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP32_RMT: MethodDescriptor( + method_schema=cv.All( + cv.only_on_esp32, + { + cv.Optional( + CONF_CHANNEL, default=_esp32_rmt_default_channel + ): _validate_esp32_rmt_channel, + }, + ), + to_code=_esp32_rmt_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP32_I2S: MethodDescriptor( + method_schema=cv.All( + cv.only_on_esp32, + { + cv.Optional( + CONF_BUS, default=_esp32_i2s_default_bus + ): _validate_esp32_i2s_bus, + }, + ), + to_code=_esp32_i2s_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_SPI: MethodDescriptor( + method_schema={ + cv.Optional(CONF_BUS): cv.All( + cv.only_on_esp32, cv.one_of(SPI_BUS_VSPI, SPI_BUS_HSPI, lower=True) + ), + cv.Optional(CONF_SPEED, default="10MHz"): cv.All( + cv.frequency, cv.one_of(*SPI_SPEEDS) + ), + }, + to_code=_spi_to_code, + extra_validate=_spi_extra_validate, + supported_chips=TWO_WIRE_CHIPS, + ), +} diff --git a/esphome/components/neopixelbus/const.py b/esphome/components/neopixelbus/const.py new file mode 100644 index 0000000000..ec1bd74c29 --- /dev/null +++ b/esphome/components/neopixelbus/const.py @@ -0,0 +1,42 @@ +CHIP_DOTSTAR = "dotstar" +CHIP_WS2801 = "ws2801" +CHIP_WS2811 = "ws2811" +CHIP_WS2812 = "ws2812" +CHIP_WS2812X = "ws2812x" +CHIP_WS2813 = "ws2813" +CHIP_SK6812 = "sk6812" +CHIP_TM1814 = "tm1814" +CHIP_TM1829 = "tm1829" +CHIP_TM1914 = "tm1914" +CHIP_800KBPS = "800kbps" +CHIP_400KBPS = "400kbps" +CHIP_APA106 = "apa106" +CHIP_LC8812 = "lc8812" +CHIP_LPD8806 = "lpd8806" +CHIP_LPD6803 = "lpd6803" +CHIP_P9813 = "p9813" + +ONE_WIRE_CHIPS = [ + CHIP_WS2811, + CHIP_WS2812, + CHIP_WS2812X, + CHIP_WS2813, + CHIP_SK6812, + CHIP_TM1814, + CHIP_TM1829, + CHIP_TM1914, + CHIP_800KBPS, + CHIP_400KBPS, + CHIP_APA106, + CHIP_LC8812, +] +TWO_WIRE_CHIPS = [ + CHIP_DOTSTAR, + CHIP_WS2801, + CHIP_LPD6803, + CHIP_LPD8806, + CHIP_P9813, +] +CHIP_TYPES = [*ONE_WIRE_CHIPS, *TWO_WIRE_CHIPS] +CONF_ASYNC = "async" +CONF_BUS = "bus" diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 2b84882e59..6bb1bc8f99 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -2,183 +2,218 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import light -from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, CONF_NUM_LEDS, CONF_PIN, \ - CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID, CONF_INVERT +from esphome.const import ( + CONF_CHANNEL, + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_METHOD, + CONF_NUM_LEDS, + CONF_PIN, + CONF_TYPE, + CONF_VARIANT, + CONF_OUTPUT_ID, + CONF_INVERT, +) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32C3, +) from esphome.core import CORE +from ._methods import ( + METHODS, + METHOD_SPI, + METHOD_ESP8266_UART, + METHOD_BIT_BANG, + METHOD_ESP32_I2S, + METHOD_ESP32_RMT, + METHOD_ESP8266_DMA, +) +from .const import ( + CHIP_TYPES, + CONF_ASYNC, + CONF_BUS, + ONE_WIRE_CHIPS, +) -neopixelbus_ns = cg.esphome_ns.namespace('neopixelbus') -NeoPixelBusLightOutputBase = neopixelbus_ns.class_('NeoPixelBusLightOutputBase', - light.AddressableLight) -NeoPixelRGBLightOutput = neopixelbus_ns.class_('NeoPixelRGBLightOutput', NeoPixelBusLightOutputBase) -NeoPixelRGBWLightOutput = neopixelbus_ns.class_('NeoPixelRGBWLightOutput', - NeoPixelBusLightOutputBase) -ESPNeoPixelOrder = neopixelbus_ns.namespace('ESPNeoPixelOrder') +neopixelbus_ns = cg.esphome_ns.namespace("neopixelbus") +NeoPixelBusLightOutputBase = neopixelbus_ns.class_( + "NeoPixelBusLightOutputBase", light.AddressableLight +) +NeoPixelRGBLightOutput = neopixelbus_ns.class_( + "NeoPixelRGBLightOutput", NeoPixelBusLightOutputBase +) +NeoPixelRGBWLightOutput = neopixelbus_ns.class_( + "NeoPixelRGBWLightOutput", NeoPixelBusLightOutputBase +) +ESPNeoPixelOrder = neopixelbus_ns.namespace("ESPNeoPixelOrder") NeoRgbFeature = cg.global_ns.NeoRgbFeature NeoRgbwFeature = cg.global_ns.NeoRgbwFeature def validate_type(value): value = cv.string(value).upper() - if 'R' not in value: + if "R" not in value: raise cv.Invalid("Must have R in type") - if 'G' not in value: + if "G" not in value: raise cv.Invalid("Must have G in type") - if 'B' not in value: + if "B" not in value: raise cv.Invalid("Must have B in type") - rest = set(value) - set('RGBW') + 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 -def validate_variant(value): - value = cv.string(value).upper() - if value == 'WS2813': - value = 'WS2812X' - if value == 'WS2812': - value = '800KBPS' - if value == 'LC8812': - value = 'SK6812' - return cv.one_of(*VARIANTS)(value) +def _choose_default_method(config): + if CONF_METHOD in config: + return config + config = config.copy() + if CONF_PIN not in config: + config[CONF_METHOD] = _validate_method(METHOD_SPI) + return config - -def validate_method(value): - if value is None: - if CORE.is_esp32: - return 'ESP32_I2S_1' - if CORE.is_esp8266: - return 'ESP8266_DMA' - raise NotImplementedError + pin = config[CONF_PIN] + if CORE.is_esp8266: + if pin == 3: + config[CONF_METHOD] = _validate_method(METHOD_ESP8266_DMA) + elif pin == 1: + config[CONF_METHOD] = _validate_method( + { + CONF_TYPE: METHOD_ESP8266_UART, + CONF_BUS: 0, + } + ) + elif pin == 2: + config[CONF_METHOD] = _validate_method( + { + CONF_TYPE: METHOD_ESP8266_UART, + CONF_BUS: 1, + } + ) + else: + config[CONF_METHOD] = _validate_method(METHOD_BIT_BANG) if CORE.is_esp32: - return cv.one_of(*ESP32_METHODS, upper=True, space='_')(value) - if CORE.is_esp8266: - return cv.one_of(*ESP8266_METHODS, upper=True, space='_')(value) - raise NotImplementedError + if get_esp32_variant() == VARIANT_ESP32C3: + config[CONF_METHOD] = _validate_method(METHOD_ESP32_RMT) + else: + config[CONF_METHOD] = _validate_method(METHOD_ESP32_I2S) + + return config -def validate_method_pin(value): - method = value[CONF_METHOD] - method_pins = { - 'ESP8266_DMA': [3], - 'ESP8266_UART0': [1], - 'ESP8266_ASYNC_UART0': [1], - 'ESP8266_UART1': [2], - 'ESP8266_ASYNC_UART1': [2], - 'ESP32_I2S_0': list(range(0, 32)), - 'ESP32_I2S_1': list(range(0, 32)), - } - if CORE.is_esp8266: - method_pins['BIT_BANG'] = list(range(0, 16)) - elif CORE.is_esp32: - method_pins['BIT_BANG'] = list(range(0, 32)) - pins_ = method_pins.get(method) - if pins_ is None: - # all pins allowed for this method - return value +def _validate(config): + variant = config[CONF_VARIANT] + if variant in ONE_WIRE_CHIPS: + if CONF_PIN not in config: + raise cv.Invalid( + f"Chip {variant} is a 1-wire chip and needs the [pin] option." + ) + if CONF_CLOCK_PIN in config or CONF_DATA_PIN in config: + raise cv.Invalid( + f"Chip {variant} is a 1-wire chip, you need to set [pin] instead of ." + ) + else: + if CONF_PIN in config: + raise cv.Invalid( + f"Chip {variant} is a 2-wire chip and needs the [data_pin]+[clock_pin] option instead of [pin]." + ) + if CONF_CLOCK_PIN not in config or CONF_DATA_PIN not in config: + raise cv.Invalid( + f"Chip {variant} is a 2-wire chip, you need to set [data_pin]+[clock_pin]." + ) - 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_) - ), path=[CONF_METHOD]) - return value + method_type = config[CONF_METHOD][CONF_TYPE] + method_desc = METHODS[method_type] + if variant not in method_desc.supported_chips: + raise cv.Invalid(f"Method {method_type} does not support {variant}") + if method_desc.extra_validate is not None: + method_desc.extra_validate(config) + + return config -VARIANTS = { - 'WS2812X': 'Ws2812x', - 'SK6812': 'Sk6812', - '800KBPS': '800Kbps', - '400KBPS': '400Kbps', -} +def _validate_method(value): + if value is None: + # default method is determined afterwards because it depends on the chip type chosen + return None -ESP8266_METHODS = { - 'ESP8266_DMA': 'NeoEsp8266Dma{}Method', - 'ESP8266_UART0': 'NeoEsp8266Uart0{}Method', - 'ESP8266_UART1': 'NeoEsp8266Uart1{}Method', - 'ESP8266_ASYNC_UART0': 'NeoEsp8266AsyncUart0{}Method', - 'ESP8266_ASYNC_UART1': 'NeoEsp8266AsyncUart1{}Method', - 'BIT_BANG': 'NeoEsp8266BitBang{}Method', -} -ESP32_METHODS = { - 'ESP32_I2S_0': 'NeoEsp32I2s0{}Method', - 'ESP32_I2S_1': 'NeoEsp32I2s1{}Method', - 'ESP32_RMT_0': 'NeoEsp32Rmt0{}Method', - 'ESP32_RMT_1': 'NeoEsp32Rmt1{}Method', - 'ESP32_RMT_2': 'NeoEsp32Rmt2{}Method', - 'ESP32_RMT_3': 'NeoEsp32Rmt3{}Method', - 'ESP32_RMT_4': 'NeoEsp32Rmt4{}Method', - 'ESP32_RMT_5': 'NeoEsp32Rmt5{}Method', - 'ESP32_RMT_6': 'NeoEsp32Rmt6{}Method', - 'ESP32_RMT_7': 'NeoEsp32Rmt7{}Method', - 'BIT_BANG': 'NeoEsp32BitBang{}Method', -} + compat_methods = {} + for bus in [0, 1]: + for is_async in [False, True]: + compat_methods[f"ESP8266{'_ASYNC' if is_async else ''}_UART{bus}"] = { + CONF_TYPE: METHOD_ESP8266_UART, + CONF_BUS: bus, + CONF_ASYNC: is_async, + } + compat_methods[f"ESP32_I2S_{bus}"] = { + CONF_TYPE: METHOD_ESP32_I2S, + CONF_BUS: bus, + } + for channel in range(8): + compat_methods[f"ESP32_RMT_{channel}"] = { + CONF_TYPE: METHOD_ESP32_RMT, + CONF_CHANNEL: channel, + } + + if isinstance(value, str): + if value.upper() in compat_methods: + return _validate_method(compat_methods[value.upper()]) + return _validate_method({CONF_TYPE: value}) + return cv.typed_schema( + {k: v.method_schema for k, v in METHODS.items()}, lower=True + )(value) -def format_method(config): - variant = VARIANTS[config[CONF_VARIANT]] +CONFIG_SCHEMA = cv.All( + cv.only_with_arduino, + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(NeoPixelBusLightOutputBase), + cv.Optional(CONF_TYPE, default="GRB"): validate_type, + cv.Required(CONF_VARIANT): cv.one_of(*CHIP_TYPES, lower=True), + cv.Optional(CONF_METHOD): _validate_method, + cv.Optional(CONF_INVERT, default="no"): cv.boolean, + 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), + _choose_default_method, + _validate, +) + + +async def to_code(config): + has_white = "W" in config[CONF_TYPE] method = config[CONF_METHOD] - if config[CONF_INVERT]: - if method == 'ESP8266_DMA': - variant = 'Inverted' + variant - else: - variant += 'Inverted' + method_template = METHODS[method[CONF_TYPE]].to_code( + method, config[CONF_VARIANT], config[CONF_INVERT] + ) - if CORE.is_esp8266: - return ESP8266_METHODS[method].format(variant) - if CORE.is_esp32: - return ESP32_METHODS[method].format(variant) - raise NotImplementedError - - -def validate(config): - if CONF_PIN in config: - if CONF_CLOCK_PIN in config or CONF_DATA_PIN in config: - raise cv.Invalid("Cannot specify both 'pin' and 'clock_pin'+'data_pin'") - return config - if CONF_CLOCK_PIN in config: - if CONF_DATA_PIN not in config: - raise cv.Invalid("If you give clock_pin, you must also specify data_pin") - return config - raise cv.Invalid("Must specify at least one of 'pin' or 'clock_pin'+'data_pin'") - - -CONFIG_SCHEMA = cv.All(light.ADDRESSABLE_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(NeoPixelBusLightOutputBase), - - cv.Optional(CONF_TYPE, default='GRB'): validate_type, - 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.Required(CONF_NUM_LEDS): cv.positive_not_null_int, -}).extend(cv.COMPONENT_SCHEMA), validate, validate_method_pin) - - -def to_code(config): - has_white = 'W' in config[CONF_TYPE] - template = cg.TemplateArguments(getattr(cg.global_ns, format_method(config))) if has_white: - out_type = NeoPixelRGBWLightOutput.template(template) + out_type = NeoPixelRGBWLightOutput.template(method_template) else: - out_type = NeoPixelRGBLightOutput.template(template) + out_type = NeoPixelRGBLightOutput.template(method_template) rhs = out_type.new() var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, out_type) - yield light.register_light(var, config) - yield cg.register_component(var, config) + await light.register_light(var, config) + await cg.register_component(var, config) if CONF_PIN in config: cg.add(var.add_leds(config[CONF_NUM_LEDS], config[CONF_PIN])) else: - cg.add(var.add_leds(config[CONF_NUM_LEDS], config[CONF_CLOCK_PIN], config[CONF_DATA_PIN])) + cg.add( + var.add_leds( + config[CONF_NUM_LEDS], config[CONF_CLOCK_PIN], config[CONF_DATA_PIN] + ) + ) 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.5.2') + cg.add_library("makuna/NeoPixelBus", "2.6.9") diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 5e8097187e..34e10f2cfe 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -1,12 +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" @@ -67,23 +71,21 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight { void add_leds(uint16_t count_pixels) { this->add_leds(new NeoPixelBus(count_pixels)); } void add_leds(NeoPixelBus *controller) { this->controller_ = controller; - this->controller_->Begin(); + // controller gets initialised in setup() - avoid calling twice (crashes with RMT) + // this->controller_->Begin(); } // ========== INTERNAL METHODS ========== void setup() override { for (int i = 0; i < this->size(); i++) { - (*this)[i] = light::ESPColor(0, 0, 0, 0); + (*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(); @@ -113,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 1d90c92496..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_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 6003c59803..0000000000 --- a/esphome/components/nextion/binary_sensor.py +++ /dev/null @@ -1,31 +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, -}) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) - - hub = yield 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..bf6e74cb38 --- /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(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(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..b6b23ada85 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -0,0 +1,42 @@ +#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: + 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 30d7519380..d95810bfbe 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,28 +1,104 @@ 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 -from . import nextion_ns +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_BRIGHTNESS, + CONF_TRIGGER_ID, +) +from esphome.core import CORE +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, +) -DEPENDENCIES = ['uart'] -AUTO_LOAD = ['binary_sensor'] +CODEOWNERS = ["@senexcrenshaw"] -Nextion = nextion_ns.class_('Nextion', cg.PollingComponent, uart.UARTDevice) -NextionRef = Nextion.operator('ref') +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] -CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(Nextion), -}).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA) +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")) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) + if CONF_BRIGHTNESS in config: + cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(NextionRef, 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) - yield display.register_display(var, config) + if CONF_TFT_URL in config: + cg.add_define("USE_NEXTION_TFT_UPLOAD") + cg.add(var.set_tft_url(config[CONF_TFT_URL])) + if CORE.is_esp32: + cg.add_library("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) + + 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 e594e147f4..494765db4d 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,74 +1,179 @@ #include "nextion.h" +#include "esphome/core/util.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace nextion { -static const char *TAG = "nextion"; +static const char *const TAG = "nextion"; void Nextion::setup() { - this->send_command_no_ack(""); - this->send_command_printf("bkcmd=3"); - 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 (size_t 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); @@ -78,208 +183,914 @@ 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; } + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *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; + } + delete component; // NOLINT(cppcoreguidelines-owning-memory) + } + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); 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_) { + NextionComponentBase *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; + + delete component; // NOLINT(cppcoreguidelines-owning-memory) + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + + break; + } + ++index; + } + + if (found != -1) { + this->nextion_queue_.erase(this->nextion_queue_.begin() + found); + } else { + ESP_LOGW( + TAG, + "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!"); + } + } 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; + } + + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *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); + } + + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); + + 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; + } + + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *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); + } + + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); + + 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; + + // Get variable name + auto index = to_process.find('\0'); + if (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; + + auto index = to_process.find('\0'); + if (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; + + // Get variable name + auto index = to_process.find('\0'); + if (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; + + // Get variable name + auto index = to_process.find('\0'); + if (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; + delete component; // NOLINT(cppcoreguidelines-owning-memory) + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + 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 (size_t i = 0; i < this->nextion_queue_.size(); i++) { + NextionComponentBase *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; + } + delete component; // NOLINT(cppcoreguidelines-owning-memory) + } + + delete this->nextion_queue_[i]; // NOLINT(cppcoreguidelines-owning-memory) + + 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) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + + nextion_queue->queue_time = millis(); + + this->nextion_queue_.push_back(nextion_queue); + + ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); +} + +/** + * @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(NextionComponentBase *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(NextionComponentBase *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(NextionComponentBase *component) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return; + + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + 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(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(NextionComponentBase *component) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion_queue->component = new nextion::NextionComponentBase; + 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(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 bd37e241e9..285b3ac9a3 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,21 +594,56 @@ 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; void update() override; 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" @@ -383,27 +652,196 @@ 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(NextionComponentBase *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(NextionComponentBase *component, const std::string &state_value) override; + void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, + const std::string &state_value) override; + + void add_to_get_queue(NextionComponentBase *component) override; + + void add_addt_command_to_queue(NextionComponentBase *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..a24fd74060 --- /dev/null +++ b/esphome/components/nextion/nextion_base.h @@ -0,0 +1,58 @@ +#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(NextionComponentBase *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(NextionComponentBase *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(NextionComponentBase *component) = 0; + + virtual void add_to_get_queue(NextionComponentBase *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..71ad803bc4 --- /dev/null +++ b/esphome/components/nextion/nextion_component_base.h @@ -0,0 +1,95 @@ +#pragma once +#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; + NextionComponentBase *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..b16f2fe7eb --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,353 @@ +#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 (int i = 0; i < range; i += 4096) { + this->write_array(&this->transfer_buffer_[i], 4096); + this->content_length_ -= 4096; + ESP_LOGN(TAG, "this->content_length_ %d range %d range_end %d range_start %d", this->content_length_, range, + range_end, range_start); + + if (!this->upload_first_chunk_sent_) { + this->upload_first_chunk_sent_ = true; + delay(500); // NOLINT + App.feed_wdt(); + } + + this->recv_ret_string_(recv_string, 2048, true); + if (recv_string[0] == 0x08) { + uint32_t result = 0; + for (int i = 0; i < 4; ++i) { + result += static_cast(recv_string[i + 1]) << (8 * i); + } + if (result > 0) { + ESP_LOGD(TAG, "Nextion reported new range %d", result); + this->content_length_ = this->tft_size_ - result; + return result; + } + } + recv_string.clear(); + } + return range_end + 1; +} + +void Nextion::upload_tft() { + if (this->is_updating_) { + ESP_LOGD(TAG, "Currently updating"); + return; + } + + if (!network::is_connected()) { + ESP_LOGD(TAG, "network is not connected"); + return; + } + + this->is_updating_ = true; + + HTTPClient http; + http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + bool begin_status = false; +#ifdef USE_ESP32 + begin_status = http.begin(this->tft_url_.c_str()); +#endif +#ifdef USE_ESP8266 +#if 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 (size_t i = 0; i < response.length(); i++) { + ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); + } + + if (response.find(0x05) != std::string::npos) { + ESP_LOGD(TAG, "preparation for tft update done"); + } else { + ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str()); + this->upload_end_(); + } + + // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 +#ifdef USE_ESP32 + uint32_t chunk_size = 8192; + if (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()); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; + if (this->transfer_buffer_ == nullptr) { // Try a smaller size + ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->transfer_buffer_ = new uint8_t[chunk_size]; + + 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) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); + this->wifi_client_secure_->setInsecure(); + this->wifi_client_secure_->setBufferSizes(512, 512); + } + return this->wifi_client_secure_; + } + + if (this->wifi_client_ == nullptr) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_ = new WiFiClient(); + } + return this->wifi_client_; +} +#endif +} // namespace nextion +} // namespace esphome + +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/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..32bfccf9f8 --- /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() > (size_t) 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(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(this, (int) state_value); + } else { + this->nextion_->add_no_result_to_queue_with_set(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(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..e4dde9a513 --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -0,0 +1,49 @@ +#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: + 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..1f32ad3425 --- /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(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(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..1548287473 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -0,0 +1,34 @@ +#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: + 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..08f032df74 --- /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(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(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..5716d0a008 --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -0,0 +1,32 @@ +#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: + 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/__init__.py b/esphome/components/nfc/__init__.py new file mode 100644 index 0000000000..b795a5d5ca --- /dev/null +++ b/esphome/components/nfc/__init__.py @@ -0,0 +1,7 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@jesserockz"] + +nfc_ns = cg.esphome_ns.namespace("nfc") + +NfcTag = nfc_ns.class_("NfcTag") diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp new file mode 100644 index 0000000000..d7d134aedb --- /dev/null +++ b/esphome/components/nfc/ndef_message.cpp @@ -0,0 +1,104 @@ +#include "ndef_message.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.ndef_message"; + +NdefMessage::NdefMessage(std::vector &data) { + ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size()); + uint8_t index = 0; + while (index <= data.size()) { + uint8_t tnf_byte = data[index++]; + 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); + + uint8_t type_length = data[index++]; + uint32_t payload_length = 0; + if (sr) { + payload_length = data[index++]; + } else { + payload_length = (static_cast(data[index]) << 24) | (static_cast(data[index + 1]) << 16) | + (static_cast(data[index + 2]) << 8) | static_cast(data[index + 3]); + index += 4; + } + + uint8_t id_length = 0; + if (il) { + id_length = data[index++]; + } + + 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); + + index += type_length; + + std::string id_str = ""; + if (il) { + id_str = std::string(data.begin() + index, data.begin() + index + id_length); + index += id_length; + } + + 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); + } + + record->set_id(id_str); + + index += payload_length; + + 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(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_.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) { + return this->add_record(make_unique(encoding, text)); +} + +bool NdefMessage::add_uri_record(const std::string &uri) { return this->add_record(make_unique(uri)); } + +std::vector NdefMessage::encode() { + std::vector data; + + for (size_t i = 0; i < this->records_.size(); i++) { + auto encoded_record = this->records_[i]->encode(i == 0, (i + 1) == this->records_.size()); + data.insert(data.end(), encoded_record.begin(), encoded_record.end()); + } + return data; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h new file mode 100644 index 0000000000..5e44a06011 --- /dev/null +++ b/esphome/components/nfc/ndef_message.h @@ -0,0 +1,41 @@ +#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 { + +static const uint8_t MAX_NDEF_RECORDS = 4; + +class NdefMessage { + public: + 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()); + } + } + + const std::vector> &get_records() { return this->records_; }; + + 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); + + std::vector encode(); + + protected: + std::vector> records_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record.cpp b/esphome/components/nfc/ndef_record.cpp new file mode 100644 index 0000000000..8a3a7d375d --- /dev/null +++ b/esphome/components/nfc/ndef_record.cpp @@ -0,0 +1,65 @@ +#include "ndef_record.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.ndef_record"; + +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; + + // 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()); + + if (payload_length <= 255) { + data.push_back(payload_length); + } else { + data.push_back(0); + data.push_back(0); + data.push_back((payload_length >> 8) & 0xFF); + data.push_back(payload_length & 0xFF); + } + + if (this->id_.length()) { + data.push_back(this->id_.length()); + } + + data.insert(data.end(), this->type_.begin(), this->type_.end()); + + if (this->id_.length()) { + data.insert(data.end(), this->id_.begin(), this->id_.end()); + } + + data.insert(data.end(), payload_data.begin(), payload_data.end()); + return data; +} + +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; // Set MB bit + } + if (last) { + value = value | 0x40; // Set ME bit + } + if (payload_size <= 255) { + value = value | 0x10; // Set SR bit + } + if (this->id_.length()) { + value = value | 0x08; // Set IL bit + } + return value; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h new file mode 100644 index 0000000000..4fab1c03e4 --- /dev/null +++ b/esphome/components/nfc/ndef_record.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace nfc { + +static const uint8_t TNF_EMPTY = 0x00; +static const uint8_t TNF_WELL_KNOWN = 0x01; +static const uint8_t TNF_MIME_MEDIA = 0x02; +static const uint8_t TNF_ABSOLUTE_URI = 0x03; +static const uint8_t TNF_EXTERNAL_TYPE = 0x04; +static const uint8_t TNF_UNKNOWN = 0x05; +static const uint8_t TNF_UNCHANGED = 0x06; +static const uint8_t TNF_RESERVED = 0x07; + +class NdefRecord { + public: + NdefRecord(){}; + 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(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 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_; + std::string id_; + std::string payload_; +}; + +} // namespace nfc +} // namespace esphome 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 new file mode 100644 index 0000000000..09dbdcfe94 --- /dev/null +++ b/esphome/components/nfc/nfc.cpp @@ -0,0 +1,108 @@ +#include "nfc.h" +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc"; + +std::string format_uid(std::vector &uid) { + char buf[(uid.size() * 2) + uid.size() - 1]; + int offset = 0; + for (size_t i = 0; i < uid.size(); i++) { + const char *format = "%02X"; + if (i + 1 < uid.size()) + format = "%02X-"; + offset += sprintf(buf + offset, format, uid[i]); + } + return std::string(buf); +} + +std::string format_bytes(std::vector &bytes) { + char buf[(bytes.size() * 2) + bytes.size() - 1]; + int offset = 0; + for (size_t i = 0; i < bytes.size(); i++) { + const char *format = "%02X"; + if (i + 1 < bytes.size()) + format = "%02X "; + offset += sprintf(buf + offset, format, bytes[i]); + } + return std::string(buf); +} + +uint8_t guess_tag_type(uint8_t uid_length) { + if (uid_length == 4) { + return TAG_TYPE_MIFARE_CLASSIC; + } else { + return TAG_TYPE_2; + } +} + +uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { + for (uint8_t i = 0; i < MIFARE_CLASSIC_BLOCK_SIZE; i++) { + if (data[i] == 0x00) { + // Do nothing, skip + } else if (data[i] == 0x03) { + return i; + } else { + return -2; + } + } + return -1; +} + +bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { + uint8_t i = get_mifare_classic_ndef_start_index(data); + if (i < 0 || data[i] != 0x03) { + ESP_LOGE(TAG, "Error, Can't decode message length."); + return false; + } + if (data[i + 1] == 0xFF) { + message_length = ((0xFF & data[i + 2]) << 8) | (0xFF & data[i + 3]); + message_start_index = i + MIFARE_CLASSIC_LONG_TLV_SIZE; + } else { + message_length = data[i + 1]; + message_start_index = i + MIFARE_CLASSIC_SHORT_TLV_SIZE; + } + return true; +} + +uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length) { + uint32_t buffer_size = message_length + 2 + 1; + if (buffer_size % MIFARE_ULTRALIGHT_READ_SIZE != 0) + buffer_size = ((buffer_size / MIFARE_ULTRALIGHT_READ_SIZE) + 1) * MIFARE_ULTRALIGHT_READ_SIZE; + return buffer_size; +} + +uint32_t get_mifare_classic_buffer_size(uint32_t message_length) { + uint32_t buffer_size = message_length; + if (message_length < 255) { + buffer_size += MIFARE_CLASSIC_SHORT_TLV_SIZE + 1; + } else { + buffer_size += MIFARE_CLASSIC_LONG_TLV_SIZE + 1; + } + if (buffer_size % MIFARE_CLASSIC_BLOCK_SIZE != 0) { + buffer_size = ((buffer_size / MIFARE_CLASSIC_BLOCK_SIZE) + 1) * MIFARE_CLASSIC_BLOCK_SIZE; + } + return buffer_size; +} + +bool mifare_classic_is_first_block(uint8_t block_num) { + if (block_num < 128) { + return (block_num % 4 == 0); + } else { + return (block_num % 16 == 0); + } +} + +bool mifare_classic_is_trailer_block(uint8_t block_num) { + if (block_num < 128) { + return ((block_num + 1) % 4 == 0); + } else { + return ((block_num + 1) % 16 == 0); + } +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h new file mode 100644 index 0000000000..d482131057 --- /dev/null +++ b/esphome/components/nfc/nfc.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "ndef_record.h" +#include "ndef_message.h" +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; +static const uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; +static const uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; + +static const uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; +static const uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; +static const uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; +static const uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; + +static const uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; +static const uint8_t TAG_TYPE_1 = 1; +static const uint8_t TAG_TYPE_2 = 2; +static const uint8_t TAG_TYPE_3 = 3; +static const uint8_t TAG_TYPE_4 = 4; +static const uint8_t TAG_TYPE_UNKNOWN = 99; + +// Mifare Commands +static const uint8_t MIFARE_CMD_AUTH_A = 0x60; +static const uint8_t MIFARE_CMD_AUTH_B = 0x61; +static const uint8_t MIFARE_CMD_READ = 0x30; +static const uint8_t MIFARE_CMD_WRITE = 0xA0; +static const uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; + +static const char *const MIFARE_CLASSIC = "Mifare Classic"; +static const char *const NFC_FORUM_TYPE_2 = "NFC Forum Type 2"; +static const char *const ERROR = "Error"; + +static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; +static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; + +std::string format_uid(std::vector &uid); +std::string format_bytes(std::vector &bytes); + +uint8_t guess_tag_type(uint8_t uid_length); +uint8_t get_mifare_classic_ndef_start_index(std::vector &data); +bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index); +uint32_t get_mifare_classic_buffer_size(uint32_t message_length); + +bool mifare_classic_is_first_block(uint8_t block_num); +bool mifare_classic_is_trailer_block(uint8_t block_num); + +uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length); + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_tag.cpp b/esphome/components/nfc/nfc_tag.cpp new file mode 100644 index 0000000000..c5c15b00ec --- /dev/null +++ b/esphome/components/nfc/nfc_tag.cpp @@ -0,0 +1,9 @@ +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.tag"; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_tag.h b/esphome/components/nfc/nfc_tag.h new file mode 100644 index 0000000000..2dfc431428 --- /dev/null +++ b/esphome/components/nfc/nfc_tag.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "ndef_message.h" + +namespace esphome { +namespace nfc { + +class NfcTag { + public: + NfcTag() { + this->uid_ = {}; + this->tag_type_ = "Unknown"; + }; + NfcTag(std::vector &uid) { + this->uid_ = uid; + this->tag_type_ = "Unknown"; + }; + NfcTag(std::vector &uid, const std::string &tag_type) { + this->uid_ = uid; + this->tag_type_ = tag_type; + }; + 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_ = 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_ = 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; }; + 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_; + std::shared_ptr ndef_message_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 9446508b0b..333dbc5a75 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace ntc { -static const char *TAG = "ntc"; +static const char *const TAG = "ntc"; void NTC::setup() { this->sensor_->add_on_state_callback([this](float value) { this->process_(value); }); @@ -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 f2eb601ed2..660208635c 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -3,37 +3,50 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg from esphome.components import sensor -from esphome.const import CONF_CALIBRATION, CONF_ID, CONF_REFERENCE_RESISTANCE, \ - CONF_REFERENCE_TEMPERATURE, CONF_SENSOR, CONF_TEMPERATURE, CONF_VALUE, ICON_THERMOMETER, \ - UNIT_CELSIUS +from esphome.const import ( + CONF_CALIBRATION, + CONF_ID, + CONF_REFERENCE_RESISTANCE, + CONF_REFERENCE_TEMPERATURE, + CONF_SENSOR, + CONF_TEMPERATURE, + CONF_VALUE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) -ntc_ns = cg.esphome_ns.namespace('ntc') -NTC = ntc_ns.class_('NTC', cg.Component, sensor.Sensor) +ntc_ns = cg.esphome_ns.namespace("ntc") +NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) -CONF_B_CONSTANT = 'b_constant' -CONF_A = 'a' -CONF_B = 'b' -CONF_C = 'c' +CONF_B_CONSTANT = "b_constant" +CONF_A = "a" +CONF_B = "b" +CONF_C = "c" ZERO_POINT = 273.15 def validate_calibration_parameter(value): if isinstance(value, dict): - return cv.Schema({ - cv.Required(CONF_TEMPERATURE): cv.float_, - cv.Required(CONF_VALUE): cv.float_, - })(value) + return cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.float_, + cv.Required(CONF_VALUE): cv.float_, + } + )(value) value = cv.string(value) - parts = value.split('->') + parts = value.split("->") if len(parts) != 2: raise cv.Invalid("Calibration parameter must be of form 3000 -> 23°C") voltage = cv.resistance(parts[0].strip()) temperature = cv.temperature(parts[1].strip()) - return validate_calibration_parameter({ - CONF_TEMPERATURE: temperature, - CONF_VALUE: voltage, - }) + return validate_calibration_parameter( + { + CONF_TEMPERATURE: temperature, + CONF_VALUE: voltage, + } + ) def calc_steinhart_hart(value): @@ -48,16 +61,16 @@ def calc_steinhart_hart(value): l2 = log(r2) l3 = log(r3) - y1 = 1/t1 - y2 = 1/t2 - y3 = 1/t3 + y1 = 1 / t1 + y2 = 1 / t2 + y3 = 1 / t3 - g2 = (y2-y1)/(l2-l1) - g3 = (y3-y1)/(l3-l1) + g2 = (y2 - y1) / (l2 - l1) + g3 = (y3 - y1) / (l3 - l1) - c = (g3-g2)/(l3-l2) * 1/(l1+l2+l3) - b = g2 - c*(l1*l1 + l1*l2 + l2*l2) - a = y1 - (b + l1*l1*c) * l1 + c = (g3 - g2) / (l3 - l2) * 1 / (l1 + l2 + l3) + b = g2 - c * (l1 * l1 + l1 * l2 + l2 * l2) + a = y1 - (b + l1 * l1 * c) * l1 return a, b, c @@ -66,8 +79,8 @@ def calc_b(value): t0 = value[CONF_REFERENCE_TEMPERATURE] + ZERO_POINT r0 = value[CONF_REFERENCE_RESISTANCE] - a = (1/t0) - (1/beta) * log(r0) - b = 1/beta + a = (1 / t0) - (1 / beta) * log(r0) + b = 1 / beta c = 0 return a, b, c @@ -75,21 +88,25 @@ def calc_b(value): def process_calibration(value): if isinstance(value, dict): - value = cv.Schema({ - cv.Required(CONF_B_CONSTANT): cv.float_, - cv.Required(CONF_REFERENCE_TEMPERATURE): cv.temperature, - cv.Required(CONF_REFERENCE_RESISTANCE): cv.resistance, - })(value) + value = cv.Schema( + { + cv.Required(CONF_B_CONSTANT): cv.float_, + cv.Required(CONF_REFERENCE_TEMPERATURE): cv.temperature, + cv.Required(CONF_REFERENCE_RESISTANCE): cv.resistance, + } + )(value) a, b, c = calc_b(value) elif isinstance(value, list): if len(value) != 3: - raise cv.Invalid("Steinhart–Hart Calibration must consist of exactly three values") + raise cv.Invalid( + "Steinhart–Hart Calibration must consist of exactly three values" + ) value = cv.Schema([validate_calibration_parameter])(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))) + raise cv.Invalid( + f"Calibration parameter accepts either a list for steinhart-hart calibration, or mapping for b-constant calibration, not {type(value)}" + ) return { CONF_A: a, @@ -98,19 +115,30 @@ def process_calibration(value): } -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.GenerateID(): cv.declare_id(NTC), - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_CALIBRATION): process_calibration, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(NTC), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_CALIBRATION): process_calibration, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) calib = config[CONF_CALIBRATION] cg.add(var.set_a(calib[CONF_A])) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py new file mode 100644 index 0000000000..71e288a4cc --- /dev/null +++ b/esphome/components/number/__init__.py @@ -0,0 +1,177 @@ +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_MODE, + CONF_ON_VALUE, + CONF_ON_VALUE_RANGE, + CONF_TRIGGER_ID, + CONF_UNIT_OF_MEASUREMENT, + 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 +) + +NumberMode = number_ns.enum("NumberMode") + +NUMBER_MODES = { + "AUTO": NumberMode.NUMBER_MODE_AUTO, + "BOX": NumberMode.NUMBER_MODE_BOX, + "SLIDER": NumberMode.NUMBER_MODE_SLIDER, +} + +icon = cv.icon + +NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), + cv.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), + ), + cv.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string_strict, + cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), + } +) + + +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)) + + cg.add(var.traits.set_mode(config[CONF_MODE])) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(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_UNIT_OF_MEASUREMENT in config: + cg.add(var.traits.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + 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..99a2c04a22 --- /dev/null +++ b/esphome/components/number/number.cpp @@ -0,0 +1,56 @@ +#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)); +} + +std::string NumberTraits::get_unit_of_measurement() { + if (this->unit_of_measurement_.has_value()) + return *this->unit_of_measurement_; + return ""; +} +void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} + +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..40fdfceec1 --- /dev/null +++ b/esphome/components/number/number.h @@ -0,0 +1,109 @@ +#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()); \ + } \ + if (!(obj)->traits.get_unit_of_measurement().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->traits.get_unit_of_measurement().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_; +}; + +enum NumberMode : uint8_t { + NUMBER_MODE_AUTO = 0, + NUMBER_MODE_BOX = 1, + NUMBER_MODE_SLIDER = 2, +}; + +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_; } + + /// 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); + + // Get/set the frontend mode. + NumberMode get_mode() const { return this->mode_; } + void set_mode(NumberMode mode) { this->mode_ = mode; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + optional unit_of_measurement_; ///< Unit of measurement override + NumberMode mode_{NUMBER_MODE_AUTO}; +}; + +/** 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 869de777d6..b3d3b7ad23 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,33 +1,118 @@ +from esphome.cpp_generator import RawExpression import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_PASSWORD, CONF_PORT, CONF_SAFE_MODE +from esphome import automation +from esphome.const import ( + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_PASSWORD, + CONF_PORT, + CONF_REBOOT_TIMEOUT, + CONF_SAFE_MODE, + CONF_TRIGGER_ID, +) from esphome.core import CORE, coroutine_with_priority -DEPENDENCIES = ['network'] +CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["network"] +AUTO_LOAD = ["socket", "md5"] -ota_ns = cg.esphome_ns.namespace('ota') -OTAComponent = ota_ns.class_('OTAComponent', cg.Component) +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" -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, -}).extend(cv.COMPONENT_SCHEMA) +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()) + + +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): cv.string, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="5min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStateChangeTrigger), + } + ), + cv.Optional(CONF_ON_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) @coroutine_with_priority(50.0) -def to_code(config): +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") - yield cg.register_component(var, config) + await cg.register_component(var, config) if config[CONF_SAFE_MODE]: - cg.add(var.start_safe_mode()) + condition = var.should_enter_safe_mode( + config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT] + ) + cg.add(RawExpression(f"if ({condition}) return")) - if CORE.is_esp8266: - cg.add_library('Update', None) - elif CORE.is_esp32: - cg.add_library('Hash', None) + if 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..5c5b61a278 --- /dev/null +++ b/esphome/components/ota/ota_backend.h @@ -0,0 +1,19 @@ +#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; + virtual bool supports_compression() = 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..f86a70d678 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -0,0 +1,24 @@ +#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 { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; + bool supports_compression() override { return false; } +}; + +} // namespace ota +} // namespace esphome + +#endif // USE_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..329f2cf0f2 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -0,0 +1,31 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ARDUINO +#ifdef USE_ESP8266 + +#include "ota_component.h" +#include "ota_backend.h" +#include "esphome/core/macros.h" + +namespace esphome { +namespace ota { + +class ArduinoESP8266OTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) + bool supports_compression() override { return true; } +#else + bool supports_compression() override { return false; } +#endif +}; + +} // 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..336b3798d9 --- /dev/null +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -0,0 +1,78 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP_IDF + +#include "ota_backend_esp_idf.h" +#include "ota_component.h" +#include +#include "esphome/components/md5/md5.h" + +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; + } + this->md5_.init(); + return OTA_RESPONSE_OK; +} + +void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); } + +OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { + esp_err_t err = esp_ota_write(this->update_handle_, data, len); + this->md5_.add(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() { + this->md5_.calculate(); + if (!this->md5_.equals_hex(this->expected_bin_md5_)) { + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_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..af09d0d693 --- /dev/null +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -0,0 +1,31 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ESP_IDF + +#include "ota_component.h" +#include "ota_backend.h" +#include +#include "esphome/components/md5/md5.h" + +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; + bool supports_compression() override { return false; } + + private: + esp_ota_handle_t update_handle_{0}; + const esp_partition_t *partition_; + md5::MD5Digest md5_{}; + char expected_bin_md5_[32]; +}; + +} // namespace ota +} // namespace esphome +#endif diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index b614139e07..92256eb1b6 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -1,37 +1,93 @@ #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/md5/md5.h" +#include "esphome/components/network/util.h" +#include #include -#include -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#include namespace esphome { namespace ota { -static const char *TAG = "ota"; +static const char *const TAG = "ota"; -uint8_t OTA_VERSION_1_0 = 1; +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_); } @@ -48,98 +104,119 @@ void OTAComponent::loop() { } } +static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; + 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; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // 0x6C, 0x26, 0xF7, 0x5C, 0x45 if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) { ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3], buf[4]); error_code = OTA_RESPONSE_ERROR_MAGIC; - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // 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); + + backend = make_ota_backend(); // Read features - 1 byte - if (!this->wait_receive_(buf, 1)) { + if (!this->readall_(buf, 1)) { ESP_LOGW(TAG, "Reading features failed!"); - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT ESP_LOGV(TAG, "OTA features is 0x%02X", ota_features); // Acknowledge header - 1 byte - this->client_.write(OTA_RESPONSE_HEADER_OK); + buf[0] = OTA_RESPONSE_HEADER_OK; + if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { + buf[0] = OTA_RESPONSE_SUPPORTS_COMPRESSION; + } + this->writeall_(buf, 1); + +#ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { - this->client_.write(OTA_RESPONSE_REQUEST_AUTH); - MD5Builder md5_builder{}; - md5_builder.begin(); + buf[0] = OTA_RESPONSE_REQUEST_AUTH; + this->writeall_(buf, 1); + md5::MD5Digest md5{}; + md5.init(); sprintf(sbuf, "%08X", random_uint32()); - md5_builder.add(sbuf); - md5_builder.calculate(); - md5_builder.getChars(sbuf); + md5.add(sbuf, 8); + md5.calculate(); + md5.get_hex(sbuf); ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); // Send nonce, 32 bytes hex MD5 - if (this->client_.write(reinterpret_cast(sbuf), 32) != 32) { + if (!this->writeall_(reinterpret_cast(sbuf), 32)) { ESP_LOGW(TAG, "Auth: Writing nonce failed!"); - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // prepare challenge - md5_builder.begin(); - md5_builder.add(this->password_.c_str()); + md5.init(); + md5.add(this->password_.c_str(), this->password_.length()); // add nonce - md5_builder.add(sbuf); + md5.add(sbuf, 32); // 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; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf); // add cnonce - md5_builder.add(sbuf); + md5.add(sbuf, 32); // calculate result - md5_builder.calculate(); - md5_builder.getChars(sbuf); + md5.calculate(); + md5.get_hex(sbuf); 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; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[64 + 32] = '\0'; ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64); @@ -151,17 +228,19 @@ void OTAComponent::handle_() { if (!matches) { ESP_LOGW(TAG, "Auth failed! Passwords do not match!"); error_code = OTA_RESPONSE_ERROR_AUTH_INVALID; - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } } +#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; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_size = 0; for (uint8_t i = 0; i < 4; i++) { @@ -170,205 +249,219 @@ 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; - goto error; - } + error_code = backend->begin(ota_size); + if (error_code != OTA_RESPONSE_OK) + goto error; // NOLINT(cppcoreguidelines-avoid-goto) update_started = true; // Acknowledge prepare OK - 1 byte - 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; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } 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) { - goto error; + while (total < ota_size) { + // TODO: timeout check + size_t requested = std::min(sizeof(buf), ota_size - total); + ssize_t read = this->client_->read(buf, requested); + if (read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); + delay(1); + continue; + } + ESP_LOGW(TAG, "Error receiving data for update, errno: %d", errno); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } else if (read == 0) { + // $ man recv + // "When a stream socket peer has performed an orderly shutdown, the return value will + // be 0 (the traditional "end-of-file" return)." + ESP_LOGW(TAG, "Remote end closed connection"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - 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; - goto error; + error_code = backend->write(buf, read); + if (error_code != OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error writing binary data to flash!"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - 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); - // slow down OTA update to avoid getting killed by task watchdog (task_wdt) - delay(10); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(OTA_IN_PROGRESS, percentage, 0); +#endif + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); } } // Acknowledge receive OK - 1 byte - 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; - goto error; + error_code = backend->end(); + if (error_code != OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error ending OTA!"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // 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) { + App.feed_wdt(); + delay(1); + continue; + } + ESP_LOGW(TAG, "Failed to read %d bytes of data, errno: %d", len, errno); + return false; + } else if (read == 0) { + ESP_LOGW(TAG, "Remote closed connection"); + return false; } else { - success = true; + at += read; } + App.feed_wdt(); + 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) { + App.feed_wdt(); + delay(1); + continue; + } + ESP_LOGW(TAG, "Failed to write %d bytes of data, errno: %d", len, errno); + return false; + } else { + at += written; + } + App.feed_wdt(); + delay(1); + } + return true; +} float OTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } uint16_t OTAComponent::get_port() const { return this->port_; } void OTAComponent::set_port(uint16_t port) { this->port_ = port; } -void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) { + +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, []() { @@ -380,15 +473,17 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) { ESP_LOGI(TAG, "Waiting for OTA attempt."); - while (true) { - App.loop(); - } + return true; } else { // increment counter this->write_rtc_(this->safe_mode_rtc_value_ + 1); + return false; } } -void OTAComponent::write_rtc_(uint32_t val) { this->rtc_.save(&val); } +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)) @@ -397,9 +492,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 e37cb7160c..5647d52eeb 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 { @@ -18,6 +19,7 @@ enum OTAResponseTypes { OTA_RESPONSE_BIN_MD5_OK = 67, OTA_RESPONSE_RECEIVE_OK = 68, OTA_RESPONSE_UPDATE_END_OK = 69, + OTA_RESPONSE_SUPPORTS_COMPRESSION = 70, OTA_RESPONSE_ERROR_MAGIC = 128, OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129, @@ -29,25 +31,31 @@ 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); - void start_safe_mode(uint8_t num_attempts = 10, uint32_t enable_time = 120000); + 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) @@ -67,14 +75,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 +93,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 b406f62ee1..4f1fb33fe7 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -3,82 +3,107 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import power_supply -from esphome.const import CONF_ID, CONF_INVERTED, CONF_LEVEL, CONF_MAX_POWER, \ - CONF_MIN_POWER, CONF_POWER_SUPPLY -from esphome.core import CORE, coroutine +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_LEVEL, + CONF_MAX_POWER, + CONF_MIN_POWER, + CONF_POWER_SUPPLY, +) +from esphome.core import CORE + +CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True -BINARY_OUTPUT_SCHEMA = cv.Schema({ - cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), - cv.Optional(CONF_INVERTED): cv.boolean, -}) +CONF_ZERO_MEANS_ZERO = "zero_means_zero" -FLOAT_OUTPUT_SCHEMA = BINARY_OUTPUT_SCHEMA.extend({ - cv.Optional(CONF_MAX_POWER): cv.percentage, - cv.Optional(CONF_MIN_POWER): cv.percentage, -}) +BINARY_OUTPUT_SCHEMA = cv.Schema( + { + cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), + cv.Optional(CONF_INVERTED): cv.boolean, + } +) -output_ns = cg.esphome_ns.namespace('output') -BinaryOutput = output_ns.class_('BinaryOutput') -BinaryOutputPtr = BinaryOutput.operator('ptr') -FloatOutput = output_ns.class_('FloatOutput', BinaryOutput) -FloatOutputPtr = FloatOutput.operator('ptr') +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, + } +) + +output_ns = cg.esphome_ns.namespace("output") +BinaryOutput = output_ns.class_("BinaryOutput") +BinaryOutputPtr = BinaryOutput.operator("ptr") +FloatOutput = output_ns.class_("FloatOutput", BinaryOutput) +FloatOutputPtr = FloatOutput.operator("ptr") # Actions -TurnOffAction = output_ns.class_('TurnOffAction', automation.Action) -TurnOnAction = output_ns.class_('TurnOnAction', automation.Action) -SetLevelAction = output_ns.class_('SetLevelAction', automation.Action) +TurnOffAction = output_ns.class_("TurnOffAction", automation.Action) +TurnOnAction = output_ns.class_("TurnOnAction", automation.Action) +SetLevelAction = output_ns.class_("SetLevelAction", automation.Action) -@coroutine -def setup_output_platform_(obj, config): +async def setup_output_platform_(obj, config): if CONF_INVERTED in config: cg.add(obj.set_inverted(config[CONF_INVERTED])) if CONF_POWER_SUPPLY in config: - power_supply_ = yield cg.get_variable(config[CONF_POWER_SUPPLY]) + power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY]) cg.add(obj.set_power_supply(power_supply_)) if CONF_MAX_POWER in 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])) -@coroutine -def register_output(var, config): +async def register_output(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - yield setup_output_platform_(var, config) + await setup_output_platform_(var, config) -BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(BinaryOutput), -}) +BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(BinaryOutput), + } +) -@automation.register_action('output.turn_on', TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) -def output_turn_on_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) +@automation.register_action("output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +async def output_turn_on_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('output.turn_off', TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA) -def output_turn_off_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) +@automation.register_action( + "output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA +) +async def output_turn_off_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('output.set_level', SetLevelAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(FloatOutput), - cv.Required(CONF_LEVEL): cv.templatable(cv.percentage), -})) -def output_set_level_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "output.set_level", + SetLevelAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(FloatOutput), + cv.Required(CONF_LEVEL): cv.templatable(cv.percentage), + } + ), +) +async def output_set_level_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_ = yield cg.templatable(config[CONF_LEVEL], args, float) + template_ = await cg.templatable(config[CONF_LEVEL], args, float) cg.add(var.set_level(template_)) - yield var + return var -def to_code(config): +async def to_code(config): cg.add_global(output_ns.using) diff --git a/esphome/components/output/automation.cpp b/esphome/components/output/automation.cpp index 57d68dacf7..5533a6bee4 100644 --- a/esphome/components/output/automation.cpp +++ b/esphome/components/output/automation.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace output { -static const char *TAG = "output.automation"; +static const char *const TAG = "output.automation"; } // namespace output } // namespace esphome diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 8c8a5ab61b..51c2849702 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -33,6 +33,7 @@ template class SetLevelAction : public Action { SetLevelAction(FloatOutput *output) : output_(output) {} TEMPLATABLE_VALUE(float, level) + void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } protected: diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 0d536d0946..f120f86f1f 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace output { -static const char *TAG = "output.float"; +static const char *const TAG = "output.float"; void FloatOutput::set_max_power(float max_power) { this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0 @@ -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) { @@ -30,10 +32,12 @@ void FloatOutput::set_level(float state) { } #endif - float adjusted_value = (state * (this->max_power_ - this->min_power_)) + this->min_power_; + 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()) - adjusted_value = 1.0f - adjusted_value; - this->write_state(adjusted_value); + state = 1.0f - state; + 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 e3f852b3f6..3e2b3ada8d 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,9 +46,26 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); - /// Set the level of this float output, this is called from the front-end. + /** 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. + */ void set_level(float state); + /** Set the frequency of the output for PWM outputs. + * + * Implemented only by components which can set the output PWM frequency. + * + * @param frequence The new frequency. + */ + virtual void update_frequency(float frequency) {} + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -65,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/output/switch/__init__.py b/esphome/components/output/switch/__init__.py index 5795271f8b..11d073d28c 100644 --- a/esphome/components/output/switch/__init__.py +++ b/esphome/components/output/switch/__init__.py @@ -4,18 +4,20 @@ from esphome.components import output, switch from esphome.const import CONF_ID, CONF_OUTPUT from .. import output_ns -OutputSwitch = output_ns.class_('OutputSwitch', switch.Switch, cg.Component) +OutputSwitch = output_ns.class_("OutputSwitch", switch.Switch, cg.Component) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(OutputSwitch), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(OutputSwitch), + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield switch.register_switch(var, config) + await cg.register_component(var, config) + await switch.register_switch(var, config) - output_ = yield cg.get_variable(config[CONF_OUTPUT]) + output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/output/switch/output_switch.cpp b/esphome/components/output/switch/output_switch.cpp index e2c3b19f28..8db45f3a2b 100644 --- a/esphome/components/output/switch/output_switch.cpp +++ b/esphome/components/output/switch/output_switch.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace output { -static const char *TAG = "output.switch"; +static const char *const TAG = "output.switch"; void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); } void OutputSwitch::setup() { diff --git a/esphome/components/output/switch/output_switch.h b/esphome/components/output/switch/output_switch.h index fc9540fede..a184a342fe 100644 --- a/esphome/components/output/switch/output_switch.h +++ b/esphome/components/output/switch/output_switch.h @@ -12,7 +12,7 @@ class OutputSwitch : public switch_::Switch, public Component { void set_output(BinaryOutput *output) { output_ = output; } void setup() override; - float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } void dump_config() override; protected: diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py new file mode 100644 index 0000000000..7483d65b9d --- /dev/null +++ b/esphome/components/packages/__init__.py @@ -0,0 +1,173 @@ +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, + CONF_USERNAME, + CONF_PASSWORD, +) +import esphome.config_validation as cv + +DOMAIN = CONF_PACKAGES + + +def _merge_package(full_old, full_new): + def merge(old, new): + # pylint: disable=no-else-return + if isinstance(new, dict): + if not isinstance(old, dict): + return new + res = old.copy() + for k, v in new.items(): + res[k] = merge(old[k], v) if k in old else v + return res + elif isinstance(new, list): + if not isinstance(old, list): + return new + return old + new + + return 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.Optional(CONF_USERNAME): cv.string, + cv.Optional(CONF_PASSWORD): cv.string, + 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, + username=config.get(CONF_USERNAME), + password=config.get(CONF_PASSWORD), + ) + 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( + 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) + + del config[CONF_PACKAGES] + return config diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index ba1059e36b..822b7ac306 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -1,37 +1,122 @@ 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_FROM, CONF_ID, CONF_SEGMENTS, CONF_TO, CONF_OUTPUT_ID +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, +) -partitions_ns = cg.esphome_ns.namespace('partition') -AddressableSegment = partitions_ns.class_('AddressableSegment') -PartitionLightOutput = partitions_ns.class_('PartitionLightOutput', light.AddressableLight) +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]: - raise cv.Invalid("From ({}) must not be larger than to ({})" - "".format(value[CONF_FROM], value[CONF_TO])) + if CONF_ID in value and value[CONF_FROM] > value[CONF_TO]: + raise cv.Invalid( + f"From ({value[CONF_FROM]}) must not be larger than to ({value[CONF_TO]})" + ) return value -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({ +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, - }, validate_from_to), cv.Length(min=1)), -}) + 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.Any(ADDRESSABLE_SEGMENT_SCHEMA, NONADDRESSABLE_SEGMENT_SCHEMA), + validate_from_to, + ), + cv.Length(min=1), + ), + } +) + +FINAL_VALIDATE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SEGMENTS): [validate_segment], + }, + extra=cv.ALLOW_EXTRA, +) -def to_code(config): +async def to_code(config): segments = [] for conf in config[CONF_SEGMENTS]: - var = yield cg.get_variable(conf[CONF_ID]) - segments.append(AddressableSegment(var, conf[CONF_FROM], - conf[CONF_TO] - conf[CONF_FROM] + 1)) + 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) - yield cg.register_component(var, config) - yield light.register_light(var, config) + await cg.register_component(var, config) + await light.register_light(var, config) diff --git a/esphome/components/partition/light_partition.cpp b/esphome/components/partition/light_partition.cpp index 4bef9ba196..63c0d0186e 100644 --- a/esphome/components/partition/light_partition.cpp +++ b/esphome/components/partition/light_partition.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace partition { -static const char *TAG = "partition.light"; +static const char *const TAG = "partition.light"; } // namespace partition } // namespace esphome diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h index 8085c43720..f74001cf75 100644 --- a/esphome/components/partition/light_partition.h +++ b/esphome/components/partition/light_partition.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/light/addressable_light.h" @@ -8,25 +10,30 @@ namespace partition { class AddressableSegment { public: - AddressableSegment(light::LightState *src, int32_t src_offset, int32_t size) - : src_(static_cast(src->get_output())), src_offset_(src_offset), size_(size) {} + AddressableSegment(light::LightState *src, int32_t src_offset, int32_t size, bool reversed) + : src_(static_cast(src->get_output())), + src_offset_(src_offset), + size_(size), + reversed_(reversed) {} light::AddressableLight *get_src() const { return this->src_; } int32_t get_src_offset() const { return this->src_offset_; } int32_t get_size() const { return this->size_; } int32_t get_dst_offset() const { return this->dst_offset_; } void set_dst_offset(int32_t dst_offset) { this->dst_offset_ = dst_offset; } + bool is_reversed() const { return this->reversed_; } protected: light::AddressableLight *src_; int32_t src_offset_; int32_t size_; int32_t dst_offset_; + bool reversed_; }; class PartitionLightOutput : public light::AddressableLight { public: - explicit PartitionLightOutput(std::vector segments) : segments_(segments) { + explicit PartitionLightOutput(std::vector segments) : segments_(std::move(segments)) { int32_t off = 0; for (auto &seg : this->segments_) { seg.set_dst_offset(off); @@ -43,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: @@ -72,7 +77,12 @@ class PartitionLightOutput : public light::AddressableLight { // offset within the segment int32_t seg_off = index - seg.get_dst_offset(); // offset within the src - int32_t src_off = seg.get_src_offset() + seg_off; + int32_t src_off; + if (seg.is_reversed()) + src_off = seg.get_src_offset() + seg.get_size() - seg_off - 1; + else + src_off = seg.get_src_offset() + seg_off; + auto view = (*seg.get_src())[src_off]; view.raw_set_color_correction(&this->correction_); return view; diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py index 8e02bd78df..1a5ccc3247 100644 --- a/esphome/components/pca9685/__init__.py +++ b/esphome/components/pca9685/__init__.py @@ -3,20 +3,27 @@ import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_FREQUENCY, CONF_ID -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] MULTI_CONF = True -pca9685_ns = cg.esphome_ns.namespace('pca9685') -PCA9685Output = pca9685_ns.class_('PCA9685Output', cg.Component, i2c.I2CDevice) +pca9685_ns = cg.esphome_ns.namespace("pca9685") +PCA9685Output = pca9685_ns.class_("PCA9685Output", cg.Component, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(PCA9685Output), - cv.Required(CONF_FREQUENCY): cv.All(cv.frequency, - cv.Range(min=23.84, max=1525.88)), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x40)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PCA9685Output), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.Range(min=23.84, max=1525.88) + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_FREQUENCY]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pca9685/output.py b/esphome/components/pca9685/output.py index b5f4805611..b7681f9ba0 100644 --- a/esphome/components/pca9685/output.py +++ b/esphome/components/pca9685/output.py @@ -4,21 +4,23 @@ from esphome.components import output from esphome.const import CONF_CHANNEL, CONF_ID from . import PCA9685Output, pca9685_ns -DEPENDENCIES = ['pca9685'] +DEPENDENCIES = ["pca9685"] -PCA9685Channel = pca9685_ns.class_('PCA9685Channel', output.FloatOutput) -CONF_PCA9685_ID = 'pca9685_id' +PCA9685Channel = pca9685_ns.class_("PCA9685Channel", output.FloatOutput) +CONF_PCA9685_ID = "pca9685_id" -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(PCA9685Channel), - cv.GenerateID(CONF_PCA9685_ID): cv.use_id(PCA9685Output), - - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), -}) +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(PCA9685Channel), + cv.GenerateID(CONF_PCA9685_ID): cv.use_id(PCA9685Output), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), + } +) -def to_code(config): - paren = yield cg.get_variable(config[CONF_PCA9685_ID]) - rhs = paren.create_channel(config[CONF_CHANNEL]) - var = cg.Pvariable(config[CONF_ID], rhs) - yield output.register_output(var, config) +async def to_code(config): + paren = await cg.get_variable(config[CONF_PCA9685_ID]) + 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 72f5687bb9..957f4062fc 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -1,11 +1,12 @@ #include "pca9685_output.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace pca9685 { -static const char *TAG = "pca9685"; +static const char *const TAG = "pca9685"; const uint8_t PCA9685_MODE_INVERTED = 0x10; const uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; @@ -123,11 +124,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/pcd8544/display.py b/esphome/components/pcd8544/display.py index e47937e46a..d0184a58d3 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -3,37 +3,55 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import display, spi from esphome.const import ( - CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, CONF_CS_PIN, + CONF_DC_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_PAGES, + CONF_RESET_PIN, + CONF_CS_PIN, + CONF_CONTRAST, ) -DEPENDENCIES = ['spi'] +DEPENDENCIES = ["spi"] -pcd8544_ns = cg.esphome_ns.namespace('pcd8544') -PCD8544 = pcd8544_ns.class_('PCD8544', cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice) +pcd8544_ns = cg.esphome_ns.namespace("pcd8544") +PCD8544 = pcd8544_ns.class_( + "PCD8544", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice +) -CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(PCD8544), - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PCD8544), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE + cv.Optional(CONF_CONTRAST, default=0x7F): cv.int_, + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield display.register_display(var, config) - yield spi.register_spi_device(var, config) + await cg.register_component(var, config) + await display.register_display(var, config) + await spi.register_spi_device(var, config) - dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) - reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + cg.add(var.set_contrast(config[CONF_CONTRAST])) + if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index ed9d1bbd43..5651d60b15 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -6,7 +6,7 @@ namespace esphome { namespace pcd8544 { -static const char *TAG = "pcd_8544"; +static const char *const TAG = "pcd_8544"; void PCD8544::setup_pins_() { this->spi_setup(); @@ -35,8 +35,7 @@ void PCD8544::initialize() { this->command(this->PCD8544_SETBIAS | 0x04); // contrast - // TODO: in future version we may add a user a control over contrast - this->command(this->PCD8544_SETVOP | 0x7f); // Experimentally determined + this->command(this->PCD8544_SETVOP | this->contrast_); // normal mode this->command(this->PCD8544_FUNCTIONSET); @@ -85,14 +84,14 @@ void HOT PCD8544::display() { this->command(this->PCD8544_SETYADDR); } -void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) { return; } uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y % 8; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); @@ -117,8 +116,8 @@ void PCD8544::update() { this->display(); } -void PCD8544::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void PCD8544::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index a1c247bf7b..b57662bbd9 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -29,9 +29,11 @@ class PCD8544 : public PollingComponent, const uint8_t PCD8544_SETTEMP = 0x04; const uint8_t PCD8544_SETBIAS = 0x10; const uint8_t PCD8544_SETVOP = 0x80; + uint8_t contrast_; void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void set_contrast(uint8_t contrast) { this->contrast_ = contrast; } float get_setup_priority() const override { return setup_priority::PROCESSOR; } void command(uint8_t value); @@ -43,7 +45,7 @@ class PCD8544 : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -51,7 +53,7 @@ class PCD8544 : public PollingComponent, } protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index daf367c089..a5f963707f 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -2,59 +2,78 @@ 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'] +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, -} +pcf8574_ns = cg.esphome_ns.namespace("pcf8574") -PCF8574Component = pcf8574_ns.class_('PCF8574Component', cg.Component, i2c.I2CDevice) -PCF8574GPIOPin = pcf8574_ns.class_('PCF8574GPIOPin', cg.GPIOPin) +PCF8574Component = pcf8574_ns.class_("PCF8574Component", cg.Component, i2c.I2CDevice) +PCF8574GPIOPin = pcf8574_ns.class_("PCF8574GPIOPin", cg.GPIOPin) -CONF_PCF8574 = 'pcf8574' -CONF_PCF8575 = 'pcf8575' -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(PCF8574Component), - cv.Optional(CONF_PCF8575, default=False): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x21)) +CONF_PCF8574 = "pcf8574" +CONF_PCF8575 = "pcf8575" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PCF8574Component), + cv.Optional(CONF_PCF8575, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x21)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, 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({ - 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.Optional(CONF_INVERTED, default=False): cv.boolean, -}) +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_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)) -def pcf8574_pin_to_code(config): - parent = yield cg.get_variable(config[CONF_PCF8574]) - yield PCF8574GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) +@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]) + + 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 6cadf565de..6eaf73e8da 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace pcf8574 { -static const char *TAG = "pcf8574"; +static const char *const TAG = "pcf8574"; void PCF8574Component::setup() { ESP_LOGCONFIG(TAG, "Setting up PCF8574..."); @@ -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/__init__.py b/esphome/components/pid/__init__.py index e69de29bb2..71a87b6ae5 100644 --- a/esphome/components/pid/__init__.py +++ b/esphome/components/pid/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index a3e2299296..ffc6392ec2 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -4,53 +4,66 @@ from esphome import automation from esphome.components import climate, sensor, output from esphome.const import CONF_ID, CONF_SENSOR -pid_ns = cg.esphome_ns.namespace('pid') -PIDClimate = pid_ns.class_('PIDClimate', climate.Climate, cg.Component) -PIDAutotuneAction = pid_ns.class_('PIDAutotuneAction', automation.Action) +pid_ns = cg.esphome_ns.namespace("pid") +PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) +PIDAutotuneAction = pid_ns.class_("PIDAutotuneAction", automation.Action) +PIDResetIntegralTermAction = pid_ns.class_( + "PIDResetIntegralTermAction", automation.Action +) +PIDSetControlParametersAction = pid_ns.class_( + "PIDSetControlParametersAction", automation.Action +) -CONF_DEFAULT_TARGET_TEMPERATURE = 'default_target_temperature' +CONF_DEFAULT_TARGET_TEMPERATURE = "default_target_temperature" -CONF_KP = 'kp' -CONF_KI = 'ki' -CONF_KD = 'kd' -CONF_CONTROL_PARAMETERS = 'control_parameters' -CONF_COOL_OUTPUT = 'cool_output' -CONF_HEAT_OUTPUT = 'heat_output' -CONF_NOISEBAND = 'noiseband' -CONF_POSITIVE_OUTPUT = 'positive_output' -CONF_NEGATIVE_OUTPUT = 'negative_output' -CONF_MIN_INTEGRAL = 'min_integral' -CONF_MAX_INTEGRAL = 'max_integral' +CONF_KP = "kp" +CONF_KI = "ki" +CONF_KD = "kd" +CONF_CONTROL_PARAMETERS = "control_parameters" +CONF_COOL_OUTPUT = "cool_output" +CONF_HEAT_OUTPUT = "heat_output" +CONF_NOISEBAND = "noiseband" +CONF_POSITIVE_OUTPUT = "positive_output" +CONF_NEGATIVE_OUTPUT = "negative_output" +CONF_MIN_INTEGRAL = "min_integral" +CONF_MAX_INTEGRAL = "max_integral" -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(PIDClimate), - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, - cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), - cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), - cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema({ - cv.Required(CONF_KP): cv.float_, - cv.Optional(CONF_KI, default=0.0): cv.float_, - cv.Optional(CONF_KD, default=0.0): cv.float_, - cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, - cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_, - }), -}), cv.has_at_least_one_key(CONF_COOL_OUTPUT, CONF_HEAT_OUTPUT)) +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PIDClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, + cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), + cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema( + { + cv.Required(CONF_KP): cv.float_, + cv.Optional(CONF_KI, default=0.0): cv.float_, + cv.Optional(CONF_KD, default=0.0): cv.float_, + cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, + cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_, + } + ), + } + ), + cv.has_at_least_one_key(CONF_COOL_OUTPUT, CONF_HEAT_OUTPUT), +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) + await cg.register_component(var, config) + await climate.register_climate(var, config) - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) if CONF_COOL_OUTPUT in config: - out = yield cg.get_variable(config[CONF_COOL_OUTPUT]) + out = await cg.get_variable(config[CONF_COOL_OUTPUT]) cg.add(var.set_cool_output(out)) if CONF_HEAT_OUTPUT in config: - out = yield cg.get_variable(config[CONF_HEAT_OUTPUT]) + out = await cg.get_variable(config[CONF_HEAT_OUTPUT]) cg.add(var.set_heat_output(out)) params = config[CONF_CONTROL_PARAMETERS] cg.add(var.set_kp(params[CONF_KP])) @@ -64,16 +77,67 @@ def to_code(config): cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) -@automation.register_action('climate.pid.autotune', PIDAutotuneAction, automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(PIDClimate), - cv.Optional(CONF_NOISEBAND, default=0.25): cv.float_, - cv.Optional(CONF_POSITIVE_OUTPUT, default=1.0): cv.possibly_negative_percentage, - cv.Optional(CONF_NEGATIVE_OUTPUT, default=-1.0): cv.possibly_negative_percentage, -})) -def esp8266_set_frequency_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "climate.pid.reset_integral_term", + PIDResetIntegralTermAction, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PIDClimate), + } + ), +) +async def pid_reset_integral_term(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "climate.pid.autotune", + PIDAutotuneAction, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Optional(CONF_NOISEBAND, default=0.25): cv.float_, + cv.Optional( + CONF_POSITIVE_OUTPUT, default=1.0 + ): cv.possibly_negative_percentage, + cv.Optional( + CONF_NEGATIVE_OUTPUT, default=-1.0 + ): cv.possibly_negative_percentage, + } + ), +) +async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) cg.add(var.set_noiseband(config[CONF_NOISEBAND])) cg.add(var.set_positive_output(config[CONF_POSITIVE_OUTPUT])) cg.add(var.set_negative_output(config[CONF_NEGATIVE_OUTPUT])) - yield var + return var + + +@automation.register_action( + "climate.pid.set_control_parameters", + PIDSetControlParametersAction, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Required(CONF_KP): cv.templatable(cv.float_), + cv.Optional(CONF_KI, default=0.0): cv.templatable(cv.float_), + cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_), + } + ), +) +async def set_control_parameters(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + kp_template_ = await cg.templatable(config[CONF_KP], args, float) + cg.add(var.set_kp(kp_template_)) + + ki_template_ = await cg.templatable(config[CONF_KI], args, float) + cg.add(var.set_ki(ki_template_)) + + kd_template_ = await cg.templatable(config[CONF_KD], args, float) + cg.add(var.set_kd(kd_template_)) + return var diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index e8b006b8d7..fc012aaa39 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace pid { -static const char *TAG = "pid.autotune"; +static const char *const TAG = "pid.autotune"; /* * # PID Autotuner @@ -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; @@ -330,7 +330,7 @@ bool PIDAutotuner::OscillationAmplitudeDetector::has_enough_data() const { float PIDAutotuner::OscillationAmplitudeDetector::get_mean_oscillation_amplitude() const { float total_amplitudes = 0; size_t total_amplitudes_n = 0; - for (int i = 1; i < std::min(phase_mins.size(), phase_maxs.size()) - 1; i++) { + for (size_t i = 1; i < std::min(phase_mins.size(), phase_maxs.size()) - 1; i++) { total_amplitudes += std::abs(phase_maxs[i] - phase_mins[i + 1]); total_amplitudes_n++; } diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 0c777ffd8b..f5c7792782 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace pid { -static const char *TAG = "pid.climate"; +static const char *const TAG = "pid.climate"; void PIDClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { @@ -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); } @@ -148,5 +145,7 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { }); } +void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 8f379c47b4..ff301386b6 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -29,6 +29,9 @@ class PIDClimate : public climate::Climate, public Component { float get_output_value() const { return output_value_; } float get_error_value() const { return controller_.error; } + float get_kp() { return controller_.kp; } + float get_ki() { return controller_.ki; } + float get_kd() { return controller_.kd; } float get_proportional_term() const { return controller_.proportional_term; } float get_integral_term() const { return controller_.integral_term; } float get_derivative_term() const { return controller_.derivative_term; } @@ -39,6 +42,7 @@ class PIDClimate : public climate::Climate, public Component { default_target_temperature_ = default_target_temperature; } void start_autotune(std::unique_ptr &&autotune); + void reset_integral_term(); protected: /// Override control to change settings of the climate device. @@ -52,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_; @@ -71,6 +74,10 @@ template class PIDAutotuneAction : public Action { public: PIDAutotuneAction(PIDClimate *parent) : parent_(parent) {} + void set_noiseband(float noiseband) { noiseband_ = noiseband; } + void set_positive_output(float positive_output) { positive_output_ = positive_output; } + void set_negative_output(float negative_output) { negative_output_ = negative_output; } + void play(Ts... x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); @@ -79,10 +86,6 @@ template class PIDAutotuneAction : public Action { this->parent_->start_autotune(std::move(tuner)); } - void set_noiseband(float noiseband) { noiseband_ = noiseband; } - void set_positive_output(float positive_output) { positive_output_ = positive_output; } - void set_negative_output(float negative_output) { negative_output_ = negative_output; } - protected: float noiseband_; float positive_output_; @@ -90,5 +93,37 @@ template class PIDAutotuneAction : public Action { PIDClimate *parent_; }; +template class PIDResetIntegralTermAction : public Action { + public: + PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->reset_integral_term(); } + + protected: + PIDClimate *parent_; +}; + +template class PIDSetControlParametersAction : public Action { + public: + PIDSetControlParametersAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { + auto kp = this->kp_.value(x...); + auto ki = this->ki_.value(x...); + auto kd = this->kd_.value(x...); + + this->parent_->set_kp(kp); + this->parent_->set_ki(ki); + this->parent_->set_kd(kd); + } + + protected: + TEMPLATABLE_VALUE(float, kp) + TEMPLATABLE_VALUE(float, ki) + TEMPLATABLE_VALUE(float, kd) + + PIDClimate *parent_; +}; + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 7ec7724e15..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_; @@ -40,6 +40,8 @@ struct PIDController { return proportional_term + integral_term + derivative_term; } + void reset_accumulated_integral() { accumulated_integral_ = 0; } + /// Proportional gain K_p. float kp = 0; /// Integral gain K_i. diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index cfab23d204..d1007fcbc4 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -1,36 +1,55 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_ID, UNIT_PERCENT, ICON_GAUGE, CONF_TYPE +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + ICON_GAUGE, + CONF_TYPE, +) from ..climate import pid_ns, PIDClimate -PIDClimateSensor = pid_ns.class_('PIDClimateSensor', sensor.Sensor, cg.Component) -PIDClimateSensorType = pid_ns.enum('PIDClimateSensorType') +PIDClimateSensor = pid_ns.class_("PIDClimateSensor", sensor.Sensor, cg.Component) +PIDClimateSensorType = pid_ns.enum("PIDClimateSensorType") PID_CLIMATE_SENSOR_TYPES = { - 'RESULT': PIDClimateSensorType.PID_SENSOR_TYPE_RESULT, - 'ERROR': PIDClimateSensorType.PID_SENSOR_TYPE_ERROR, - 'PROPORTIONAL': PIDClimateSensorType.PID_SENSOR_TYPE_PROPORTIONAL, - 'INTEGRAL': PIDClimateSensorType.PID_SENSOR_TYPE_INTEGRAL, - 'DERIVATIVE': PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, - 'HEAT': PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, - 'COOL': PIDClimateSensorType.PID_SENSOR_TYPE_COOL, + "RESULT": PIDClimateSensorType.PID_SENSOR_TYPE_RESULT, + "ERROR": PIDClimateSensorType.PID_SENSOR_TYPE_ERROR, + "PROPORTIONAL": PIDClimateSensorType.PID_SENSOR_TYPE_PROPORTIONAL, + "INTEGRAL": PIDClimateSensorType.PID_SENSOR_TYPE_INTEGRAL, + "DERIVATIVE": PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, + "HEAT": PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, + "COOL": PIDClimateSensorType.PID_SENSOR_TYPE_COOL, + "KP": PIDClimateSensorType.PID_SENSOR_TYPE_KP, + "KI": PIDClimateSensorType.PID_SENSOR_TYPE_KI, + "KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD, } -CONF_CLIMATE_ID = 'climate_id' -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PERCENT, ICON_GAUGE, 1).extend({ - cv.GenerateID(): cv.declare_id(PIDClimateSensor), - cv.GenerateID(CONF_CLIMATE_ID): cv.use_id(PIDClimate), - - cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_TYPES, upper=True), -}).extend(cv.COMPONENT_SCHEMA) +CONF_CLIMATE_ID = "climate_id" +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_GAUGE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(PIDClimateSensor), + cv.GenerateID(CONF_CLIMATE_ID): cv.use_id(PIDClimate), + cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_TYPES, upper=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): - parent = yield cg.get_variable(config[CONF_CLIMATE_ID]) +async def to_code(config): + parent = await cg.get_variable(config[CONF_CLIMATE_ID]) var = cg.new_Pvariable(config[CONF_ID]) - yield sensor.register_sensor(var, config) - yield cg.register_component(var, config) + await sensor.register_sensor(var, config) + await cg.register_component(var, config) cg.add(var.set_parent(parent)) cg.add(var.set_type(config[CONF_TYPE])) diff --git a/esphome/components/pid/sensor/pid_climate_sensor.cpp b/esphome/components/pid/sensor/pid_climate_sensor.cpp index 6241a139f6..2a76c775d3 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.cpp +++ b/esphome/components/pid/sensor/pid_climate_sensor.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace pid { -static const char *TAG = "pid.sensor"; +static const char *const TAG = "pid.sensor"; void PIDClimateSensor::setup() { this->parent_->add_on_pid_computed_callback([this]() { this->update_from_parent_(); }); @@ -35,6 +35,18 @@ void PIDClimateSensor::update_from_parent_() { case PID_SENSOR_TYPE_COOL: value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f); break; + case PID_SENSOR_TYPE_KP: + value = this->parent_->get_kp(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KI: + value = this->parent_->get_ki(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KD: + value = this->parent_->get_kd(); + this->publish_state(value); + return; default: value = NAN; break; diff --git a/esphome/components/pid/sensor/pid_climate_sensor.h b/esphome/components/pid/sensor/pid_climate_sensor.h index 85759f1eaf..f3774610f8 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.h +++ b/esphome/components/pid/sensor/pid_climate_sensor.h @@ -14,6 +14,9 @@ enum PIDClimateSensorType { PID_SENSOR_TYPE_DERIVATIVE, PID_SENSOR_TYPE_HEAT, PID_SENSOR_TYPE_COOL, + PID_SENSOR_TYPE_KP, + PID_SENSOR_TYPE_KI, + PID_SENSOR_TYPE_KD, }; class PIDClimateSensor : public sensor::Sensor, public Component { 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..13a08bbd16 --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -0,0 +1,920 @@ +#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: + 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 (size_t 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 (size_t 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_ = parse_number(fc).value_or(0); + 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..a206e41988 --- /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_EMPTY + ), + 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_EMPTY + ), + 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 489442c637..5de94699f0 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -4,11 +4,41 @@ namespace esphome { namespace pmsx003 { -static const char *TAG = "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; } @@ -66,6 +96,7 @@ optional PMSX003Component::check_byte_() { length_matches = payload_length == 28 || payload_length == 20; break; case PMSX003_TYPE_5003T: + case PMSX003_TYPE_5003S: length_matches = payload_length == 28; break; case PMSX003_TYPE_5003ST: @@ -102,19 +133,73 @@ optional PMSX003Component::check_byte_() { void PMSX003Component::parse_data_() { switch (this->type_) { + case PMSX003_TYPE_5003ST: { + 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%%", temperature, humidity); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + // The rest of the PMS5003ST matches the PMS5003S, continue on + } + case PMSX003_TYPE_5003S: { + uint16_t formaldehyde = this->get_16_bit_uint_(28); + + ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde); + + if (this->formaldehyde_sensor_ != nullptr) + this->formaldehyde_sensor_->publish_state(formaldehyde); + // The rest of the PMS5003S 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 +216,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 +225,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..fd6364c70c 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -11,6 +11,7 @@ enum PMSX003Type { PMSX003_TYPE_X003 = 0, PMSX003_TYPE_5003T, PMSX003_TYPE_5003ST, + PMSX003_TYPE_5003S, }; class PMSX003Component : public uart::UARTDevice, public Component { @@ -21,9 +22,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 +51,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 4dbe500d3d..56a91d22fc 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -1,91 +1,239 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart -from esphome.const import CONF_FORMALDEHYDE, CONF_HUMIDITY, CONF_ID, CONF_PM_10_0, \ - CONF_PM_1_0, CONF_PM_2_5, CONF_TEMPERATURE, CONF_TYPE, ICON_CHEMICAL_WEAPON, \ - UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_CELSIUS, \ - UNIT_PERCENT +from esphome.const import ( + CONF_FORMALDEHYDE, + CONF_HUMIDITY, + CONF_ID, + 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, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_CELSIUS, + UNIT_COUNT_DECILITRE, + UNIT_PERCENT, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -pmsx003_ns = cg.esphome_ns.namespace('pmsx003') -PMSX003Component = pmsx003_ns.class_('PMSX003Component', uart.UARTDevice, cg.Component) -PMSX003Sensor = pmsx003_ns.class_('PMSX003Sensor', sensor.Sensor) +pmsx003_ns = cg.esphome_ns.namespace("pmsx003") +PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component) +PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) -TYPE_PMSX003 = 'PMSX003' -TYPE_PMS5003T = 'PMS5003T' -TYPE_PMS5003ST = 'PMS5003ST' +TYPE_PMSX003 = "PMSX003" +TYPE_PMS5003T = "PMS5003T" +TYPE_PMS5003ST = "PMS5003ST" +TYPE_PMS5003S = "PMS5003S" -PMSX003Type = pmsx003_ns.enum('PMSX003Type') +PMSX003Type = pmsx003_ns.enum("PMSX003Type") PMSX003_TYPES = { TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, + TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S, } SENSORS_TO_TYPE = { - CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003ST], - CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], + CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], + CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_FORMALDEHYDE: [TYPE_PMS5003ST], + CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S], } 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 -CONFIG_SCHEMA = cv.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(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - cv.Optional(CONF_PM_2_5): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - cv.Optional(CONF_PM_10_0): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - cv.Optional(CONF_TEMPERATURE): - sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): - sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), - cv.Optional(CONF_FORMALDEHYDE): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PMSX003Component), + cv.Required(CONF_TYPE): cv.enum(PMSX003_TYPES, upper=True), + cv.Optional(CONF_PM_1_0_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + 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_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_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, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) 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 = yield sensor.new_sensor(config[CONF_PM_1_0]) + sens = await sensor.new_sensor(config[CONF_PM_1_0]) cg.add(var.set_pm_1_0_sensor(sens)) if CONF_PM_2_5 in config: - sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + sens = await sensor.new_sensor(config[CONF_PM_2_5]) cg.add(var.set_pm_2_5_sensor(sens)) if CONF_PM_10_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + 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 = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) if CONF_FORMALDEHYDE in config: - sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE]) + sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE]) cg.add(var.set_formaldehyde_sensor(sens)) diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index c82d35b398..b902e8e3d0 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -1,31 +1,94 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.components import spi -from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID +from esphome.components import nfc +from esphome.const import CONF_ID, CONF_ON_TAG_REMOVED, CONF_ON_TAG, CONF_TRIGGER_ID -DEPENDENCIES = ['spi'] -AUTO_LOAD = ['binary_sensor'] +CODEOWNERS = ["@OttoWinter", "@jesserockz"] +AUTO_LOAD = ["binary_sensor", "nfc"] MULTI_CONF = True -pn532_ns = cg.esphome_ns.namespace('pn532') -PN532 = pn532_ns.class_('PN532', cg.PollingComponent, spi.SPIDevice) -PN532Trigger = pn532_ns.class_('PN532Trigger', automation.Trigger.template(cg.std_string)) +CONF_PN532_ID = "pn532_id" +CONF_ON_FINISHED_WRITE = "on_finished_write" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(PN532), - cv.Optional(CONF_ON_TAG): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532Trigger), - }), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +pn532_ns = cg.esphome_ns.namespace("pn532") +PN532 = pn532_ns.class_("PN532", cg.PollingComponent) + +PN532OnTagTrigger = pn532_ns.class_( + "PN532OnTagTrigger", automation.Trigger.template(cg.std_string, nfc.NfcTag) +) +PN532OnFinishedWriteTrigger = pn532_ns.class_( + "PN532OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN532IsWritingCondition = pn532_ns.class_( + "PN532IsWritingCondition", automation.Condition +) + +PN532_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN532), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN532OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), + } + ), + } +).extend(cv.polling_component_schema("1s")) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) +def CONFIG_SCHEMA(conf): + if conf: + raise cv.Invalid( + "This component has been moved in 1.16, please see the docs for updated " + "instructions. https://esphome.io/components/binary_sensor/pn532.html" + ) + + +async def setup_pn532(var, config): + await cg.register_component(var, config) for conf in config.get(CONF_ON_TAG, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_trigger(trigger)) - yield automation.build_automation(trigger, [(cg.std_string, 'x')], conf) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn532.is_writing", + PN532IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN532), + } + ), +) +async def pn532_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn532/binary_sensor.py b/esphome/components/pn532/binary_sensor.py index 1c5e220fa6..9a5816b322 100644 --- a/esphome/components/pn532/binary_sensor.py +++ b/esphome/components/pn532/binary_sensor.py @@ -3,42 +3,48 @@ import esphome.config_validation as cv from esphome.components import binary_sensor from esphome.const import CONF_UID, CONF_ID from esphome.core import HexInt -from . import pn532_ns, PN532 +from . import pn532_ns, PN532, CONF_PN532_ID -DEPENDENCIES = ['pn532'] - -CONF_PN532_ID = 'pn532_id' +DEPENDENCIES = ["pn532"] def validate_uid(value): value = cv.string_strict(value) - for x in value.split('-'): + for x in value.split("-"): if len(x) != 2: - raise cv.Invalid("Each part (separated by '-') of the UID must be two characters " - "long.") + raise cv.Invalid( + "Each part (separated by '-') of the UID must be two characters " + "long." + ) try: x = int(x, 16) - except ValueError: - raise cv.Invalid("Valid characters for parts of a UID are 0123456789ABCDEF.") + except ValueError as err: + raise cv.Invalid( + "Valid characters for parts of a UID are 0123456789ABCDEF." + ) from err if x < 0 or x > 255: - raise cv.Invalid("Valid values for UID parts (separated by '-') are 00 to FF") + raise cv.Invalid( + "Valid values for UID parts (separated by '-') are 00 to FF" + ) return value -PN532BinarySensor = pn532_ns.class_('PN532BinarySensor', binary_sensor.BinarySensor) +PN532BinarySensor = pn532_ns.class_("PN532BinarySensor", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(PN532BinarySensor), - cv.GenerateID(CONF_PN532_ID): cv.use_id(PN532), - cv.Required(CONF_UID): validate_uid, -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN532BinarySensor), + cv.GenerateID(CONF_PN532_ID): cv.use_id(PN532), + cv.Required(CONF_UID): validate_uid, + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) + await binary_sensor.register_binary_sensor(var, config) - hub = yield cg.get_variable(config[CONF_PN532_ID]) + hub = await cg.get_variable(config[CONF_PN532_ID]) cg.add(hub.register_tag(var)) - addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split('-')] + addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")] cg.add(var.set_uid(addr)) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 93000a7421..0c46ff8a57 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 @@ -9,95 +12,81 @@ namespace esphome { namespace pn532 { -static const char *TAG = "pn532"; - -void format_uid(char *buf, const uint8_t *uid, uint8_t uid_length) { - int offset = 0; - for (uint8_t i = 0; i < uid_length; i++) { - const char *format = "%02X"; - if (i + 1 < uid_length) - format = "%02X-"; - offset += sprintf(buf + offset, format, uid[i]); - } -} +static const char *const TAG = "pn532"; void PN532::setup() { ESP_LOGCONFIG(TAG, "Setting up PN532..."); - this->spi_setup(); - // Wake the chip up from power down - // 1. Enable the SS line for at least 2ms - // 2. Send a dummy command to get the protocol synced up - // (this may time out, but that's ok) - // 3. Send SAM config command with normal mode without waiting for ready bit (IRQ not initialized yet) - // 4. Probably optional, send SAM config again, this time checking ACK and return value - this->cs_->digital_write(false); - delay(10); + // Get version data + if (!this->write_command_({PN532_COMMAND_VERSION_DATA})) { + ESP_LOGE(TAG, "Error sending version command"); + this->mark_failed(); + return; + } - // send dummy firmware version command to get synced up - this->pn532_write_command_check_ack_({0x02}); // get firmware version command - // do not actually read any data, this should be OK according to datasheet + std::vector version_data; + if (!this->read_response(PN532_COMMAND_VERSION_DATA, version_data)) { + ESP_LOGE(TAG, "Error getting version"); + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Found chip PN5%02X", version_data[0]); + ESP_LOGD(TAG, "Firmware ver. %d.%d", version_data[1], version_data[2]); - this->pn532_write_command_({ - 0x14, // SAM config command - 0x01, // normal mode - 0x14, // zero timeout (not in virtual card mode) - 0x01, - }); + if (!this->write_command_({ + PN532_COMMAND_SAMCONFIGURATION, + 0x01, // normal mode + 0x14, // zero timeout (not in virtual card mode) + 0x01, + })) { + ESP_LOGE(TAG, "No wakeup ack"); + this->mark_failed(); + return; + } - // do not wait for ready bit, this is a dummy command - delay(2); - - // Try to read ACK, if it fails it might be because there's data from a previous power cycle left - this->read_ack_(); - // do not wait for ready bit for return data - delay(5); - - // read data packet for wakeup result - auto wakeup_result = this->pn532_read_data_(); - if (wakeup_result.size() != 1) { + std::vector wakeup_result; + if (!this->read_response(PN532_COMMAND_SAMCONFIGURATION, wakeup_result)) { this->error_code_ = WAKEUP_FAILED; this->mark_failed(); return; } // Set up SAM (secure access module) - uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); - bool ret = this->pn532_write_command_check_ack_({ - 0x14, // SAM config command - 0x01, // normal mode - sam_timeout, // timeout as multiple of 50ms (actually only for virtual card mode, but shouldn't matter) - 0x01, // Enable IRQ - }); - - if (!ret) { + uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); + if (!this->write_command_({ + PN532_COMMAND_SAMCONFIGURATION, + 0x01, // normal mode + sam_timeout, // timeout as multiple of 50ms (actually only for virtual card mode, but shouldn't matter) + 0x01, // Enable IRQ + })) { this->error_code_ = SAM_COMMAND_FAILED; this->mark_failed(); return; } - auto sam_result = this->pn532_read_data_(); - if (sam_result.size() != 1) { + std::vector sam_result; + if (!this->read_response(PN532_COMMAND_SAMCONFIGURATION, sam_result)) { ESP_LOGV(TAG, "Invalid SAM result: (%u)", sam_result.size()); // NOLINT - for (auto dat : sam_result) { + for (uint8_t dat : sam_result) { ESP_LOGV(TAG, " 0x%02X", dat); } this->error_code_ = SAM_COMMAND_FAILED; this->mark_failed(); return; } + + this->turn_off_rf_(); } void PN532::update() { for (auto *obj : this->binary_sensors_) obj->on_scan_end(); - bool success = this->pn532_write_command_check_ack_({ - 0x4A, // INLISTPASSIVETARGET - 0x01, // max 1 card - 0x00, // baud rate ISO14443A (106 kbit/s) - }); - if (!success) { + if (!this->write_command_({ + PN532_COMMAND_INLISTPASSIVETARGET, + 0x01, // max 1 card + 0x00, // baud rate ISO14443A (106 kbit/s) + })) { ESP_LOGW(TAG, "Requesting tag read failed!"); this->status_set_warning(); return; @@ -105,236 +94,255 @@ void PN532::update() { this->status_clear_warning(); this->requested_read_ = true; } + void PN532::loop() { - if (!this->requested_read_ || !this->is_ready_()) + if (!this->requested_read_) return; - auto read = this->pn532_read_data_(); + std::vector read; + bool success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + this->requested_read_ = false; - if (read.size() <= 2 || read[0] != 0x4B) { + if (!success) { // Something failed + if (!this->current_uid_.empty()) { + auto tag = make_unique(this->current_uid_); + for (auto *trigger : this->triggers_ontagremoved_) + trigger->process(tag); + } + this->current_uid_ = {}; + this->turn_off_rf_(); return; } - uint8_t num_targets = read[1]; - if (num_targets != 1) + uint8_t num_targets = read[0]; + if (num_targets != 1) { // no tags found or too many + if (!this->current_uid_.empty()) { + auto tag = make_unique(this->current_uid_); + for (auto *trigger : this->triggers_ontagremoved_) + trigger->process(tag); + } + this->current_uid_ = {}; + this->turn_off_rf_(); return; + } - // const uint8_t target_number = read[2]; - // const uint16_t sens_res = uint16_t(read[3] << 8) | read[4]; - // const uint8_t sel_res = read[5]; - const uint8_t nfcid_length = read[6]; - const uint8_t *nfcid = &read[7]; - if (read.size() < 7U + nfcid_length) { + uint8_t nfcid_length = read[5]; + std::vector nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length); + if (read.size() < 6U + nfcid_length) { // oops, pn532 returned invalid data return; } bool report = true; - // 1. Go through all triggers - for (auto *trigger : this->triggers_) - trigger->process(nfcid, nfcid_length); - - // 2. Find a binary sensor - for (auto *tag : this->binary_sensors_) { - if (tag->process(nfcid, nfcid_length)) { - // 2.1 if found, do not dump + for (auto *bin_sens : this->binary_sensors_) { + if (bin_sens->process(nfcid)) { report = false; } } - if (report) { - char buf[32]; - format_uid(buf, nfcid, nfcid_length); - ESP_LOGD(TAG, "Found new tag '%s'", buf); + if (nfcid.size() == this->current_uid_.size()) { + bool same_uid = false; + for (size_t i = 0; i < nfcid.size(); i++) + same_uid |= nfcid[i] == this->current_uid_[i]; + if (same_uid) + return; } + + this->current_uid_ = nfcid; + + if (next_task_ == READ) { + auto tag = this->read_tag_(nfcid); + for (auto *trigger : this->triggers_ontag_) + trigger->process(tag); + + if (report) { + ESP_LOGD(TAG, "Found new tag '%s'", nfc::format_uid(nfcid).c_str()); + if (tag->has_ndef_message()) { + const auto &message = tag->get_ndef_message(); + const auto &records = message->get_records(); + ESP_LOGD(TAG, " NDEF formatted records:"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } + } + } else if (next_task_ == CLEAN) { + ESP_LOGD(TAG, " Tag cleaning..."); + if (!this->clean_tag_(nfcid)) { + ESP_LOGE(TAG, " Tag was not fully cleaned successfully"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + } else if (next_task_ == FORMAT) { + ESP_LOGD(TAG, " Tag formatting..."); + if (!this->format_tag_(nfcid)) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + } else if (next_task_ == WRITE) { + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (!this->format_tag_(nfcid)) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (!this->write_tag_(nfcid, this->next_task_message_to_write_)) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + delete this->next_task_message_to_write_; + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + } + + this->read_mode(); + + this->turn_off_rf_(); } -float PN532::get_setup_priority() const { return setup_priority::DATA; } - -void PN532::pn532_write_command_(const std::vector &data) { - this->enable(); - delay(2); - // First byte, communication mode: Write data - this->write_byte(0x01); - +bool PN532::write_command_(const std::vector &data) { + std::vector write_data; // Preamble - this->write_byte(0x00); + write_data.push_back(0x00); // Start code - this->write_byte(0x00); - this->write_byte(0xFF); + write_data.push_back(0x00); + write_data.push_back(0xFF); // Length of message, TFI + data bytes const uint8_t real_length = data.size() + 1; // LEN - this->write_byte(real_length); + write_data.push_back(real_length); // LCS (Length checksum) - this->write_byte(~real_length + 1); + write_data.push_back(~real_length + 1); // TFI (Frame Identifier, 0xD4 means to PN532, 0xD5 means from PN532) - this->write_byte(0xD4); + write_data.push_back(0xD4); // calculate checksum, TFI is part of checksum uint8_t checksum = 0xD4; // DATA for (uint8_t dat : data) { - this->write_byte(dat); + write_data.push_back(dat); checksum += dat; } // DCS (Data checksum) - this->write_byte(~checksum + 1); + write_data.push_back(~checksum + 1); // Postamble - this->write_byte(0x00); + write_data.push_back(0x00); - this->disable(); + this->write_data(write_data); + + return this->read_ack_(); } -bool PN532::pn532_write_command_check_ack_(const std::vector &data) { - // 1. write command - this->pn532_write_command_(data); - - // 2. wait for readiness - if (!this->wait_ready_()) - return false; - - // 3. read ack - if (!this->read_ack_()) { - ESP_LOGV(TAG, "Invalid ACK frame received from PN532!"); - return false; - } - - return true; -} - -std::vector PN532::pn532_read_data_() { - this->enable(); - delay(2); - // Read data (transmission from the PN532 to the host) - this->write_byte(0x03); - - // sometimes preamble is not transmitted for whatever reason - // mostly happens during startup. - // just read the first two bytes and check if that is the case - uint8_t header[6]; - this->read_array(header, 2); - if (header[0] == 0x00 && header[1] == 0x00) { - // normal packet, preamble included - this->read_array(header + 2, 4); - } else if (header[0] == 0x00 && header[1] == 0xFF) { - // weird packet, preamble skipped; make it look like a normal packet - header[0] = 0x00; - header[1] = 0x00; - header[2] = 0xFF; - this->read_array(header + 3, 3); - } else { - // invalid packet - this->disable(); - ESP_LOGV(TAG, "read data invalid preamble!"); - return {}; - } - - bool valid_header = (header[0] == 0x00 && // preamble - header[1] == 0x00 && // start code - header[2] == 0xFF && static_cast(header[3] + header[4]) == 0 && // LCS, len + lcs = 0 - header[5] == 0xD5 // TFI - frame from PN532 to system controller - ); - if (!valid_header) { - this->disable(); - ESP_LOGV(TAG, "read data invalid header!"); - return {}; - } - - std::vector ret; - // full length of message, including TFI - const uint8_t full_len = header[3]; - // length of data, excluding TFI - uint8_t len = full_len - 1; - if (full_len == 0) - len = 0; - - ret.resize(len); - this->read_array(ret.data(), len); - - uint8_t checksum = 0xD5; - for (uint8_t dat : ret) - checksum += dat; - checksum = ~checksum + 1; - - uint8_t dcs = this->read_byte(); - if (dcs != checksum) { - this->disable(); - ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", dcs, checksum); - return {}; - } - - if (this->read_byte() != 0x00) { - this->disable(); - ESP_LOGV(TAG, "read data invalid postamble!"); - return {}; - } - this->disable(); - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - ESP_LOGVV(TAG, "PN532 Data Frame: (%u)", ret.size()); // NOLINT - for (uint8_t dat : ret) { - ESP_LOGVV(TAG, " 0x%02X", dat); - } -#endif - - return ret; -} -bool PN532::is_ready_() { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - // PN532 returns a single data byte, - // "After having sent a command, the host controller must wait for bit 0 of Status byte equals 1 - // before reading the data from the PN532." - bool ret = this->read_byte() == 0x01; - this->disable(); - - if (ret) { - ESP_LOGVV(TAG, "Chip is ready!"); - } - return ret; -} bool PN532::read_ack_() { - ESP_LOGVV(TAG, "Reading ACK..."); - this->enable(); - delay(2); - // "Read data (transmission from the PN532 to the host) " - this->write_byte(0x03); + ESP_LOGV(TAG, "Reading ACK..."); - uint8_t ack[6]; - memset(ack, 0, sizeof(ack)); + std::vector data; + if (!this->read_data(data, 6)) { + return false; + } - this->read_array(ack, 6); - this->disable(); - - bool matches = (ack[0] == 0x00 && // preamble - ack[1] == 0x00 && // start of packet - ack[2] == 0xFF && ack[3] == 0x00 && // ACK packet code - ack[4] == 0xFF && ack[5] == 0x00 // postamble - ); - ESP_LOGVV(TAG, "ACK valid: %s", YESNO(matches)); + bool matches = (data[1] == 0x00 && // preamble + data[2] == 0x00 && // start of packet + data[3] == 0xFF && data[4] == 0x00 && // ACK packet code + data[5] == 0xFF && data[6] == 0x00); // postamble + ESP_LOGV(TAG, "ACK valid: %s", YESNO(matches)); return matches; } -bool PN532::wait_ready_() { - uint32_t start_time = millis(); - while (!this->is_ready_()) { - if (millis() - start_time > 100) { - ESP_LOGE(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); - } - return true; + +void PN532::send_nack_() { + ESP_LOGV(TAG, "Sending NACK for retransmit"); + this->write_data({0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00}); + delay(10); } +void PN532::turn_off_rf_() { + ESP_LOGV(TAG, "Turning RF field OFF"); + this->write_command_({ + PN532_COMMAND_RFCONFIGURATION, + 0x01, // RF Field + 0x00, // Off + }); +} + +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) { + ESP_LOGD(TAG, "Mifare classic"); + return this->read_mifare_classic_tag_(uid); + } else if (type == nfc::TAG_TYPE_2) { + ESP_LOGD(TAG, "Mifare ultralight"); + return this->read_mifare_ultralight_tag_(uid); + } else if (type == nfc::TAG_TYPE_UNKNOWN) { + ESP_LOGV(TAG, "Cannot determine tag type"); + return make_unique(uid); + } else { + return make_unique(uid); + } +} + +void PN532::read_mode() { + this->next_task_ = READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} +void PN532::clean_mode() { + this->next_task_ = CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} +void PN532::format_mode() { + this->next_task_ = FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} +void PN532::write_mode(nfc::NdefMessage *message) { + this->next_task_ = WRITE; + this->next_task_message_to_write_ = message; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +bool PN532::clean_tag_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + return this->format_mifare_classic_mifare_(uid); + } else if (type == nfc::TAG_TYPE_2) { + return this->clean_mifare_ultralight_(); + } + ESP_LOGE(TAG, "Unsupported Tag for formatting"); + return false; +} + +bool PN532::format_tag_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + return this->format_mifare_classic_ndef_(uid); + } else if (type == nfc::TAG_TYPE_2) { + return this->clean_mifare_ultralight_(); + } + ESP_LOGE(TAG, "Unsupported Tag for formatting"); + return false; +} + +bool PN532::write_tag_(std::vector &uid, nfc::NdefMessage *message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + return this->write_mifare_classic_tag_(uid, message); + } else if (type == nfc::TAG_TYPE_2) { + return this->write_mifare_ultralight_tag_(uid, message); + } + ESP_LOGE(TAG, "Unsupported Tag for formatting"); + return false; +} + +float PN532::get_setup_priority() const { return setup_priority::DATA; } + void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { @@ -348,7 +356,6 @@ void PN532::dump_config() { break; } - LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); for (auto *child : this->binary_sensors_) { @@ -356,11 +363,11 @@ void PN532::dump_config() { } } -bool PN532BinarySensor::process(const uint8_t *data, uint8_t len) { - if (len != this->uid_.size()) +bool PN532BinarySensor::process(std::vector &data) { + if (data.size() != this->uid_.size()) return false; - for (uint8_t i = 0; i < len; i++) { + for (size_t i = 0; i < data.size(); i++) { if (data[i] != this->uid_[i]) return false; } @@ -369,10 +376,8 @@ bool PN532BinarySensor::process(const uint8_t *data, uint8_t len) { this->found_ = true; return true; } -void PN532Trigger::process(const uint8_t *uid, uint8_t uid_length) { - char buf[32]; - format_uid(buf, uid, uid_length); - this->trigger(std::string(buf)); +void PN532OnTagTrigger::process(const std::unique_ptr &tag) { + this->trigger(nfc::format_uid(tag->get_uid()), *tag); } } // namespace pn532 diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 49d5878265..692a5011e6 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -3,17 +3,22 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/spi/spi.h" +#include "esphome/components/nfc/nfc_tag.h" +#include "esphome/components/nfc/nfc.h" namespace esphome { namespace pn532 { -class PN532BinarySensor; -class PN532Trigger; +static const uint8_t PN532_COMMAND_VERSION_DATA = 0x02; +static const uint8_t PN532_COMMAND_SAMCONFIGURATION = 0x14; +static const uint8_t PN532_COMMAND_RFCONFIGURATION = 0x32; +static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; +static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; -class PN532 : public PollingComponent, - public spi::SPIDevice { +class PN532BinarySensor; +class PN532OnTagTrigger; + +class PN532 : public PollingComponent { public: void setup() override; @@ -25,51 +30,78 @@ class PN532 : public PollingComponent, void loop() override; void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); } - void register_trigger(PN532Trigger *trig) { this->triggers_.push_back(trig); } + void register_ontag_trigger(PN532OnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(PN532OnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(nfc::NdefMessage *message); protected: - /// Write the full command given in data to the PN532 - void pn532_write_command_(const std::vector &data); - bool pn532_write_command_check_ack_(const std::vector &data); - - /** Read a data frame from the PN532 and return the result as a vector. - * - * Note that is_ready needs to be checked first before requesting this method. - * - * On failure, an empty vector is returned. - */ - std::vector pn532_read_data_(); - - /** Checks if the PN532 has set its ready status flag. - * - * Procedure goes as follows: - * - Host sends command to PN532 "write data" - * - Wait for readiness (until PN532 has processed command) by polling "read status"/is_ready_ - * - Parse ACK/NACK frame with "read data" byte - * - * - If data required, wait until device reports readiness again - * - Then call "read data" and read certain number of bytes (length is given at offset 4 of frame) - */ - bool is_ready_(); - bool wait_ready_(); - + void turn_off_rf_(); + bool write_command_(const std::vector &data); bool read_ack_(); + void send_nack_(); + + virtual bool write_data(const std::vector &data) = 0; + virtual bool read_data(std::vector &data, uint8_t len) = 0; + virtual bool read_response(uint8_t command, std::vector &data) = 0; + + 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); + + 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); + bool format_mifare_classic_mifare_(std::vector &uid); + bool format_mifare_classic_ndef_(std::vector &uid); + bool write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message); + + std::unique_ptr read_mifare_ultralight_tag_(std::vector &uid); + bool read_mifare_ultralight_page_(uint8_t page_num, std::vector &data); + bool is_mifare_ultralight_formatted_(); + uint16_t read_mifare_ultralight_capacity_(); + bool find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index); + bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + bool write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message); + bool clean_mifare_ultralight_(); bool requested_read_{false}; std::vector binary_sensors_; - std::vector triggers_; + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; + std::vector current_uid_; + nfc::NdefMessage *next_task_message_to_write_; + enum NfcTask { + READ = 0, + CLEAN, + FORMAT, + WRITE, + } next_task_{READ}; enum PN532Error { NONE = 0, WAKEUP_FAILED, SAM_COMMAND_FAILED, } error_code_{NONE}; + CallbackManager on_finished_write_callback_; }; class PN532BinarySensor : public binary_sensor::BinarySensor { public: void set_uid(const std::vector &uid) { uid_ = uid; } - bool process(const uint8_t *data, uint8_t len); + bool process(std::vector &data); void on_scan_end() { if (!this->found_) { @@ -83,9 +115,21 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532Trigger : public Trigger { +class PN532OnTagTrigger : public Trigger { public: - void process(const uint8_t *uid, uint8_t uid_length); + void process(const std::unique_ptr &tag); +}; + +class PN532OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN532OnFinishedWriteTrigger(PN532 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN532IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } }; } // namespace pn532 diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp new file mode 100644 index 0000000000..81d135d8e6 --- /dev/null +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -0,0 +1,251 @@ +#include + +#include "pn532.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn532 { + +static const char *const TAG = "pn532.mifare_classic"; + +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; + + if (this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { + 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 make_unique(uid, nfc::ERROR); + } + } else { + ESP_LOGE(TAG, "Failed to read block %d", current_block); + return make_unique(uid, nfc::MIFARE_CLASSIC); + } + } else { + ESP_LOGV(TAG, "Tag is not NDEF formatted"); + return make_unique(uid, nfc::MIFARE_CLASSIC); + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (!this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { + ESP_LOGE(TAG, "Error, Block authentication failed for %d", current_block); + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data)) { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } else { + ESP_LOGE(TAG, "Error reading block %d", current_block); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + return make_unique(uid, nfc::MIFARE_CLASSIC, buffer); +} + +bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + if (!this->write_command_({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + block_num, + })) { + return false; + } + + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) { + return false; + } + data.erase(data.begin()); + + ESP_LOGVV(TAG, " Block %d: %s", block_num, nfc::format_bytes(data).c_str()); + return true; +} + +bool PN532::auth_mifare_classic_block_(std::vector &uid, uint8_t block_num, uint8_t key_num, + const uint8_t *key) { + std::vector data({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + key_num, // Mifare Key slot + block_num, // Block number + }); + data.insert(data.end(), key, key + 6); + data.insert(data.end(), uid.begin(), uid.end()); + if (!this->write_command_(data)) { + ESP_LOGE(TAG, "Authentication failed - Block %d", block_num); + return false; + } + + std::vector response; + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response[0] != 0x00) { + ESP_LOGE(TAG, "Authentication failed - Block 0x%02x", block_num); + return false; + } + + return true; +} + +bool PN532::format_mifare_classic_mifare_(std::vector &uid) { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + bool error = false; + + for (int block = 0; block < 64; block += 4) { + if (!this->auth_mifare_classic_block_(uid, block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { + continue; + } + if (block != 0) { + if (!this->write_mifare_classic_block_(block, blank_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block); + error = true; + } + } + if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block + 1); + error = true; + } + if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block + 2); + error = true; + } + if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block + 3); + error = true; + } + } + + return !error; +} + +bool PN532::format_mifare_classic_ndef_(std::vector &uid) { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!"); + return false; + } + if (!this->write_mifare_classic_block_(1, block_1_data)) + return false; + if (!this->write_mifare_classic_block_(2, block_2_data)) + return false; + if (!this->write_mifare_classic_block_(3, block_3_trailer)) + return false; + + ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); + + for (int block = 4; block < 64; block += 4) { + if (!this->auth_mifare_classic_block_(uid, block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { + return false; + } + if (block == 4) { + if (!this->write_mifare_classic_block_(block, empty_ndef_message)) + ESP_LOGE(TAG, "Unable to write block %d", block); + } else { + if (!this->write_mifare_classic_block_(block, blank_block)) + ESP_LOGE(TAG, "Unable to write block %d", block); + } + if (!this->write_mifare_classic_block_(block + 1, blank_block)) + ESP_LOGE(TAG, "Unable to write block %d", block + 1); + if (!this->write_mifare_classic_block_(block + 2, blank_block)) + ESP_LOGE(TAG, "Unable to write block %d", block + 2); + if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) + ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); + } + return true; +} + +bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + std::vector data({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_WRITE, + block_num, + }); + data.insert(data.end(), write_data.begin(), write_data.end()); + if (!this->write_command_(data)) { + ESP_LOGE(TAG, "Error writing block %d", block_num); + return false; + } + + std::vector response; + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response)) { + ESP_LOGE(TAG, "Error writing block %d", block_num); + return false; + } + + return true; +} + +bool PN532::write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (!this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { + return false; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (!this->write_mifare_classic_block_(current_block, data)) { + return false; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return true; +} + +} // namespace pn532 +} // namespace esphome diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp new file mode 100644 index 0000000000..1b91ae919e --- /dev/null +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -0,0 +1,181 @@ +#include + +#include "pn532.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn532 { + +static const char *const TAG = "pn532.mifare_ultralight"; + +std::unique_ptr PN532::read_mifare_ultralight_tag_(std::vector &uid) { + if (!this->is_mifare_ultralight_formatted_()) { + ESP_LOGD(TAG, "Not NDEF formatted"); + 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 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 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 make_unique(uid, nfc::NFC_FORUM_TYPE_2); + } + data.insert(data.end(), page_data.begin(), page_data.end()); + + if (data.size() >= (message_length + message_start_index)) + break; + } + + data.erase(data.begin(), data.begin() + message_start_index); + data.erase(data.begin() + message_length, data.end()); + + return make_unique(uid, nfc::NFC_FORUM_TYPE_2, data); +} + +bool PN532::read_mifare_ultralight_page_(uint8_t page_num, std::vector &data) { + if (!this->write_command_({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + page_num, + })) { + return false; + } + + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) { + return false; + } + data.erase(data.begin()); + // We only want 1 page of data but the PN532 returns 4 at once. + data.erase(data.begin() + 4, data.end()); + + ESP_LOGVV(TAG, "Pages %d-%d: %s", page_num, page_num + 4, nfc::format_bytes(data).c_str()); + + return true; +} + +bool PN532::is_mifare_ultralight_formatted_() { + std::vector data; + if (this->read_mifare_ultralight_page_(4, data)) { + return !(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[3] == 0xFF); + } + return true; +} + +uint16_t PN532::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_page_(3, data)) { + return data[2] * 8U; + } + return 0; +} + +bool PN532::find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index) { + std::vector data; + for (int page = 4; page < 6; page++) { + std::vector page_data; + if (!this->read_mifare_ultralight_page_(page, page_data)) { + return false; + } + data.insert(data.end(), page_data.begin(), page_data.end()); + } + if (data[0] == 0x03) { + message_length = data[1]; + message_start_index = 2; + return true; + } else if (data[5] == 0x03) { + message_length = data[6]; + message_start_index = 7; + return true; + } + return false; +} + +bool PN532::write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %d > %d", buffer_length, capacity); + return false; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (!this->write_mifare_ultralight_page_(current_page, data)) { + return false; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return true; +} + +bool PN532::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (!this->write_mifare_ultralight_page_(i, blank_data)) { + return false; + } + } + return true; +} + +bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector data({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_WRITE_ULTRALIGHT, + page_num, + }); + data.insert(data.end(), write_data.begin(), write_data.end()); + if (!this->write_command_(data)) { + ESP_LOGE(TAG, "Error writing page %d", page_num); + return false; + } + + std::vector response; + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response)) { + ESP_LOGE(TAG, "Error writing page %d", page_num); + return false; + } + + return true; +} + +} // namespace pn532 +} // namespace esphome diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py new file mode 100644 index 0000000000..36af2f8aa0 --- /dev/null +++ b/esphome/components/pn532_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn532 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn532"] +CODEOWNERS = ["@OttoWinter", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn532_i2c_ns = cg.esphome_ns.namespace("pn532_i2c") +PN532I2C = pn532_i2c_ns.class_("PN532I2C", pn532.PN532, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn532.PN532_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN532I2C), + } + ).extend(i2c.i2c_device_schema(0x24)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn532.setup_pn532(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp new file mode 100644 index 0000000000..e7c99e94b0 --- /dev/null +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -0,0 +1,132 @@ +#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 +// - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf + +namespace esphome { +namespace pn532_i2c { + +static const char *const TAG = "pn532_i2c"; + +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); + + std::vector ready; + ready.resize(1); + uint32_t start_time = millis(); + while (true) { + if (this->read_bytes_raw(ready.data(), 1)) { + if (ready[0] == 0x01) + break; + } + + if (millis() - start_time > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + return false; + } + } + + data.resize(len + 1); + this->read_bytes_raw(data.data(), len + 1); + return true; +} + +bool PN532I2C::read_response(uint8_t command, std::vector &data) { + ESP_LOGV(TAG, "Reading response"); + uint8_t len = this->read_response_length_(); + if (len == 0) { + return false; + } + + ESP_LOGV(TAG, "Reading response of length %d", len); + if (!this->read_data(data, 6 + len + 2)) { + ESP_LOGD(TAG, "No response data"); + return false; + } + + if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + // invalid packet + ESP_LOGV(TAG, "read data invalid preamble!"); + return false; + } + + bool valid_header = (static_cast(data[4] + data[5]) == 0 && // LCS, len + lcs = 0 + data[6] == 0xD5 && // TFI - frame from PN532 to system controller + data[7] == command + 1); // Correct command response + + if (!valid_header) { + ESP_LOGV(TAG, "read data invalid header!"); + return false; + } + + data.erase(data.begin(), data.begin() + 6); // Remove headers + + uint8_t checksum = 0; + for (int i = 0; i < len + 1; i++) { + uint8_t dat = data[i]; + checksum += dat; + } + checksum = ~checksum + 1; + + if (data[len + 1] != checksum) { + ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", data[len], checksum); + return false; + } + + if (data[len + 2] != 0x00) { + ESP_LOGV(TAG, "read data invalid postamble!"); + return false; + } + + data.erase(data.begin(), data.begin() + 2); // Remove TFI and command code + data.erase(data.end() - 2, data.end()); // Remove checksum and postamble + + return true; +} + +uint8_t PN532I2C::read_response_length_() { + std::vector data; + if (!this->read_data(data, 6)) { + return 0; + } + + if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + // invalid packet + ESP_LOGV(TAG, "read data invalid preamble!"); + return 0; + } + + bool valid_header = (static_cast(data[4] + data[5]) == 0 && // LCS, len + lcs = 0 + data[6] == 0xD5); // TFI - frame from PN532 to system controller + + if (!valid_header) { + ESP_LOGV(TAG, "read data invalid header!"); + return 0; + } + + this->send_nack_(); + + // full length of message, including TFI + uint8_t full_len = data[4]; + // length of data, excluding TFI + uint8_t len = full_len - 1; + if (full_len == 0) + len = 0; + return len; +} + +void PN532I2C::dump_config() { + PN532::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn532_i2c +} // namespace esphome diff --git a/esphome/components/pn532_i2c/pn532_i2c.h b/esphome/components/pn532_i2c/pn532_i2c.h new file mode 100644 index 0000000000..296d73e042 --- /dev/null +++ b/esphome/components/pn532_i2c/pn532_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn532/pn532.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pn532_i2c { + +class PN532I2C : public pn532::PN532, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + bool write_data(const std::vector &data) override; + bool read_data(std::vector &data, uint8_t len) override; + bool read_response(uint8_t command, std::vector &data) override; + uint8_t read_response_length_(); +}; + +} // namespace pn532_i2c +} // namespace esphome diff --git a/esphome/components/pn532_spi/__init__.py b/esphome/components/pn532_spi/__init__.py new file mode 100644 index 0000000000..8a8ab1b175 --- /dev/null +++ b/esphome/components/pn532_spi/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, pn532 +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) + +CONFIG_SCHEMA = cv.All( + pn532.PN532_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN532Spi), + } + ).extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn532.setup_pn532(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp new file mode 100644 index 0000000000..be58f265b9 --- /dev/null +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -0,0 +1,160 @@ +#include "pn532_spi.h" +#include "esphome/core/log.h" + +// Based on: +// - https://cdn-shop.adafruit.com/datasheets/PN532C106_Application+Note_v1.2.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf + +namespace esphome { +namespace pn532_spi { + +static const char *const TAG = "pn532_spi"; + +void PN532Spi::setup() { + ESP_LOGI(TAG, "PN532Spi setup started!"); + this->spi_setup(); + + this->cs_->digital_write(false); + delay(10); + ESP_LOGI(TAG, "SPI setup finished!"); + PN532::setup(); +} + +bool PN532Spi::write_data(const std::vector &data) { + this->enable(); + delay(2); + // First byte, communication mode: Write data + this->write_byte(0x01); + ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty(data).c_str()); + this->write_array(data.data(), data.size()); + this->disable(); + + return true; +} + +bool PN532Spi::read_data(std::vector &data, uint8_t len) { + ESP_LOGV(TAG, "Waiting for ready byte..."); + + uint32_t start_time = millis(); + while (true) { + this->enable(); + // First byte, communication mode: Read state + this->write_byte(0x02); + bool ready = this->read_byte() == 0x01; + this->disable(); + if (ready) + break; + ESP_LOGV(TAG, "Not ready yet..."); + + if (millis() - start_time > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + return false; + } + yield(); + } + + // Read data (transmission from the PN532 to the host) + this->enable(); + delay(2); + this->write_byte(0x03); + + ESP_LOGV(TAG, "Reading data..."); + + data.resize(len); + this->read_array(data.data(), len); + this->disable(); + data.insert(data.begin(), 0x01); + ESP_LOGV(TAG, "Read data: %s", format_hex_pretty(data).c_str()); + return true; +} + +bool PN532Spi::read_response(uint8_t command, std::vector &data) { + ESP_LOGV(TAG, "Reading response"); + + uint32_t start_time = millis(); + while (true) { + this->enable(); + // First byte, communication mode: Read state + this->write_byte(0x02); + bool ready = this->read_byte() == 0x01; + this->disable(); + if (ready) + break; + ESP_LOGV(TAG, "Not ready yet..."); + + if (millis() - start_time > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + return false; + } + yield(); + } + + this->enable(); + delay(2); + this->write_byte(0x03); + + std::vector header(7); + this->read_array(header.data(), 7); + + ESP_LOGV(TAG, "Header data: %s", format_hex_pretty(header).c_str()); + + if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { + // invalid packet + ESP_LOGV(TAG, "read data invalid preamble!"); + return false; + } + + bool valid_header = (static_cast(header[3] + header[4]) == 0 && // LCS, len + lcs = 0 + header[5] == 0xD5 && // TFI - frame from PN532 to system controller + header[6] == command + 1); // Correct command response + + if (!valid_header) { + ESP_LOGV(TAG, "read data invalid header!"); + return false; + } + + // full length of message, including command response + uint8_t full_len = header[3]; + // length of data, excluding command response + uint8_t len = full_len - 1; + if (full_len == 0) + len = 0; + + ESP_LOGV(TAG, "Reading response of length %d", len); + + data.resize(len + 1); + this->read_array(data.data(), len + 1); + this->disable(); + + ESP_LOGV(TAG, "Response data: %s", format_hex_pretty(data).c_str()); + + uint8_t checksum = header[5] + header[6]; // TFI + Command response code + for (int i = 0; i < len - 1; i++) { + uint8_t dat = data[i]; + checksum += dat; + } + checksum = ~checksum + 1; + + if (data[len - 1] != checksum) { + ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", data[len - 1], checksum); + return false; + } + + if (data[len] != 0x00) { + ESP_LOGV(TAG, "read data invalid postamble!"); + return false; + } + + data.erase(data.end() - 2, data.end()); // Remove checksum and postamble + + return true; +} + +void PN532Spi::dump_config() { + PN532::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +} // namespace pn532_spi +} // namespace esphome diff --git a/esphome/components/pn532_spi/pn532_spi.h b/esphome/components/pn532_spi/pn532_spi.h new file mode 100644 index 0000000000..d98bd447c8 --- /dev/null +++ b/esphome/components/pn532_spi/pn532_spi.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn532/pn532.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace pn532_spi { + +class PN532Spi : public pn532::PN532, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + bool write_data(const std::vector &data) override; + bool read_data(std::vector &data, uint8_t len) override; + bool read_response(uint8_t command, std::vector &data) override; +}; + +} // namespace pn532_spi +} // namespace esphome diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index 5646ffdc0b..f7dd8bca84 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -3,25 +3,32 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_ENABLE_TIME, CONF_ID, CONF_KEEP_ON_TIME, CONF_PIN -power_supply_ns = cg.esphome_ns.namespace('power_supply') -PowerSupply = power_supply_ns.class_('PowerSupply', cg.Component) +CODEOWNERS = ["@esphome/core"] +power_supply_ns = cg.esphome_ns.namespace("power_supply") +PowerSupply = power_supply_ns.class_("PowerSupply", cg.Component) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(PowerSupply), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_ENABLE_TIME, default='20ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_KEEP_ON_TIME, default='10s'): cv.positive_time_period_milliseconds, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PowerSupply), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_ENABLE_TIME, default="20ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_KEEP_ON_TIME, default="10s" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) cg.add(var.set_enable_time(config[CONF_ENABLE_TIME])) cg.add(var.set_keep_on_time(config[CONF_KEEP_ON_TIME])) - cg.add_define('USE_POWER_SUPPLY') + cg.add_define("USE_POWER_SUPPLY") diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index 1f8471ac9f..a492919202 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace power_supply { -static const char *TAG = "power_supply"; +static const char *const TAG = "power_supply"; void PowerSupply::setup() { ESP_LOGCONFIG(TAG, "Setting up Power Supply..."); @@ -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/__init__.py b/esphome/components/prometheus/__init__.py new file mode 100644 index 0000000000..45345f06e8 --- /dev/null +++ b/esphome/components/prometheus/__init__.py @@ -0,0 +1,29 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.components import web_server_base + +AUTO_LOAD = ["web_server_base"] + +prometheus_ns = cg.esphome_ns.namespace("prometheus") +PrometheusHandler = prometheus_ns.class_("PrometheusHandler", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PrometheusHandler), + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( + web_server_base.WebServerBase + ), + }, + cv.only_with_arduino, +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + + cg.add_define("USE_PROMETHEUS") + + var = cg.new_Pvariable(config[CONF_ID], paren) + await cg.register_component(var, config) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp new file mode 100644 index 0000000000..fa7b4fe132 --- /dev/null +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -0,0 +1,316 @@ +#ifdef USE_ARDUINO + +#include "prometheus_handler.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace prometheus { + +void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { + AsyncResponseStream *stream = req->beginResponseStream("text/plain"); + +#ifdef USE_SENSOR + this->sensor_type_(stream); + for (auto *obj : App.get_sensors()) + this->sensor_row_(stream, obj); +#endif + +#ifdef USE_BINARY_SENSOR + this->binary_sensor_type_(stream); + for (auto *obj : App.get_binary_sensors()) + this->binary_sensor_row_(stream, obj); +#endif + +#ifdef USE_FAN + this->fan_type_(stream); + for (auto *obj : App.get_fans()) + this->fan_row_(stream, obj); +#endif + +#ifdef USE_LIGHT + this->light_type_(stream); + for (auto *obj : App.get_lights()) + this->light_row_(stream, obj); +#endif + +#ifdef USE_COVER + this->cover_type_(stream); + for (auto *obj : App.get_covers()) + this->cover_row_(stream, obj); +#endif + +#ifdef USE_SWITCH + this->switch_type_(stream); + for (auto *obj : App.get_switches()) + this->switch_row_(stream, obj); +#endif + + req->send(stream); +} + +// Type-specific implementation +#ifdef USE_SENSOR +void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); +} +void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { + if (obj->is_internal()) + return; + 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()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",unit=\"")); + stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(F("\"} ")); + stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_BINARY_SENSOR +void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_binary_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); +} +void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { + if (obj->is_internal()) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_FAN +void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_fan_value GAUGE\n")); + stream->print(F("#TYPE esphome_fan_failed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_speed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); +} +void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::FanState *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_fan_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + // Speed if available + if (obj->get_traits().supports_speed()) { + stream->print(F("esphome_fan_speed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->speed); + stream->print('\n'); + } + // Oscillation if available + if (obj->get_traits().supports_oscillation()) { + stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->oscillating); + stream->print('\n'); + } +} +#endif + +#ifdef USE_LIGHT +void PrometheusHandler::light_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_light_state GAUGE\n")); + stream->print(F("#TYPE esphome_light_color GAUGE\n")); + stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); +} +void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { + if (obj->is_internal()) + return; + // State + stream->print(F("esphome_light_state{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->remote_values.is_on()); + stream->print(F("\n")); + // Brightness and RGBW + light::LightColorValues color = obj->current_values; + float brightness, r, g, b, w; + color.as_brightness(&brightness); + color.as_rgbw(&r, &g, &b, &w); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(F("\n")); + // Effect + std::string effect = obj->get_effect_name(); + if (effect == "None") { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"None\"} 0\n")); + } else { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"")); + stream->print(effect.c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_COVER +void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_cover_value GAUGE\n")); + stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); +} +void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { + if (obj->is_internal()) + return; + 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()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_cover_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->position); + stream->print('\n'); + if (obj->get_traits().get_supports_tilt()) { + stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->tilt); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_SWITCH +void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_switch_value GAUGE\n")); + stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); +} +void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_switch_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); +} +#endif + +} // namespace prometheus +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h new file mode 100644 index 0000000000..5076883ba6 --- /dev/null +++ b/esphome/components/prometheus/prometheus_handler.h @@ -0,0 +1,85 @@ +#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" + +namespace esphome { +namespace prometheus { + +class PrometheusHandler : public AsyncWebHandler, public Component { + public: + PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {} + + bool canHandle(AsyncWebServerRequest *request) override { + if (request->method() == HTTP_GET) { + if (request->url() == "/metrics") + return true; + } + + return false; + } + + void handleRequest(AsyncWebServerRequest *req) override; + + void setup() override { + this->base_->init(); + this->base_->add_handler(this); + } + float get_setup_priority() const override { + // After WiFi + return setup_priority::WIFI - 1.0f; + } + + protected: +#ifdef USE_SENSOR + /// Return the type for prometheus + void sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); +#endif + +#ifdef USE_BINARY_SENSOR + /// Return the type for prometheus + void binary_sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + /// Return the type for prometheus + void fan_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void fan_row_(AsyncResponseStream *stream, fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + /// Return the type for prometheus + void light_type_(AsyncResponseStream *stream); + /// Return the Light Values state as prometheus data point + void light_row_(AsyncResponseStream *stream, light::LightState *obj); +#endif + +#ifdef USE_COVER + /// Return the type for prometheus + void cover_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); +#endif + +#ifdef USE_SWITCH + /// Return the type for prometheus + void switch_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); +#endif + + web_server_base::WebServerBase *base_; +}; + +} // 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 c71e51eb32..d9f198f4fc 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -4,19 +4,19 @@ namespace esphome { namespace pulse_counter { -static const char *TAG = "pulse_counter"; +static const char *const TAG = "pulse_counter"; -const char *EDGE_MODE_TO_STRING[] = {"DISABLE", "INCREMENT", "DECREMENT"}; +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); @@ -154,15 +155,21 @@ void PulseCounterSensor::dump_config() { void PulseCounterSensor::update() { pulse_counter_t raw = this->storage_.read_raw_value(); - float value = (60000.0f * raw) / float(this->get_update_interval()); // per minute + uint32_t now = millis(); + if (this->last_time_ != 0) { + uint32_t interval = now - this->last_time_; + float value = (60000.0f * raw) / float(interval); // per minute + ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value); + this->publish_state(value); + } - ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value); - this->publish_state(value); + if (this->total_sensor_ != nullptr) { + current_total_ += raw; + ESP_LOGD(TAG, "'%s': Total : %i pulses", this->get_name().c_str(), current_total_); + this->total_sensor_->publish_state(current_total_); + } + this->last_time_ = now; } -#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 483036ac34..9ed2159ae3 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,10 +50,11 @@ 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; } + void set_total_sensor(sensor::Sensor *total_sensor) { total_sensor_ = total_sensor; } /// Unit of measurement is "pulses/min". void setup() override; @@ -62,13 +63,12 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { void dump_config() override; protected: - GPIOPin *pin_; + InternalGPIOPin *pin_; PulseCounterStorage storage_; + uint32_t last_time_{0}; + 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 61d3f3d5b5..c7b89d41b0 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -2,23 +2,36 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_COUNT_MODE, CONF_FALLING_EDGE, CONF_ID, CONF_INTERNAL_FILTER, \ - CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, \ - ICON_PULSE, UNIT_PULSES_PER_MINUTE +from esphome.const import ( + CONF_COUNT_MODE, + CONF_FALLING_EDGE, + CONF_ID, + CONF_INTERNAL_FILTER, + CONF_PIN, + CONF_RISING_EDGE, + CONF_NUMBER, + CONF_TOTAL, + ICON_PULSE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_PULSES_PER_MINUTE, + UNIT_PULSES, +) from esphome.core import CORE -pulse_counter_ns = cg.esphome_ns.namespace('pulse_counter') -PulseCounterCountMode = pulse_counter_ns.enum('PulseCounterCountMode') +pulse_counter_ns = cg.esphome_ns.namespace("pulse_counter") +PulseCounterCountMode = pulse_counter_ns.enum("PulseCounterCountMode") COUNT_MODES = { - 'DISABLE': PulseCounterCountMode.PULSE_COUNTER_DISABLE, - 'INCREMENT': PulseCounterCountMode.PULSE_COUNTER_INCREMENT, - 'DECREMENT': PulseCounterCountMode.PULSE_COUNTER_DECREMENT, + "DISABLE": PulseCounterCountMode.PULSE_COUNTER_DISABLE, + "INCREMENT": PulseCounterCountMode.PULSE_COUNTER_INCREMENT, + "DECREMENT": PulseCounterCountMode.PULSE_COUNTER_DECREMENT, } COUNT_MODE_SCHEMA = cv.enum(COUNT_MODES, upper=True) -PulseCounterSensor = pulse_counter_ns.class_('PulseCounterSensor', - sensor.Sensor, cg.PollingComponent) +PulseCounterSensor = pulse_counter_ns.class_( + "PulseCounterSensor", sensor.Sensor, cg.PollingComponent +) def validate_internal_filter(value): @@ -34,41 +47,74 @@ def validate_internal_filter(value): def validate_pulse_counter_pin(value): 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 pulse counters on ESP8266.") + raise cv.Invalid( + "Pins GPIO16 and GPIO17 cannot be used as pulse counters on ESP8266." + ) return value def validate_count_mode(value): rising_edge = value[CONF_RISING_EDGE] falling_edge = value[CONF_FALLING_EDGE] - if rising_edge == 'DISABLE' and falling_edge == 'DISABLE': - raise cv.Invalid("Can't set both count modes to DISABLE! This means no counting occurs at " - "all!") + if rising_edge == "DISABLE" and falling_edge == "DISABLE": + raise cv.Invalid( + "Can't set both count modes to DISABLE! This means no counting occurs at " + "all!" + ) return value -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).extend({ - cv.GenerateID(): cv.declare_id(PulseCounterSensor), - cv.Required(CONF_PIN): validate_pulse_counter_pin, - cv.Optional(CONF_COUNT_MODE, default={ - CONF_RISING_EDGE: 'INCREMENT', - CONF_FALLING_EDGE: 'DISABLE', - }): cv.All(cv.Schema({ - cv.Required(CONF_RISING_EDGE): COUNT_MODE_SCHEMA, - cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, - }), validate_count_mode), - cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(PulseCounterSensor), + cv.Required(CONF_PIN): validate_pulse_counter_pin, + cv.Optional( + CONF_COUNT_MODE, + default={ + CONF_RISING_EDGE: "INCREMENT", + CONF_FALLING_EDGE: "DISABLE", + }, + ): cv.All( + cv.Schema( + { + cv.Required(CONF_RISING_EDGE): COUNT_MODE_SCHEMA, + cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, + } + ), + validate_count_mode, + ), + cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, + cv.Optional(CONF_TOTAL): sensor.sensor_schema( + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) count = config[CONF_COUNT_MODE] cg.add(var.set_rising_edge_mode(count[CONF_RISING_EDGE])) cg.add(var.set_falling_edge_mode(count[CONF_FALLING_EDGE])) cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER])) + + if CONF_TOTAL in config: + sens = await sensor.new_sensor(config[CONF_TOTAL]) + cg.add(var.set_total_sensor(sens)) diff --git a/esphome/components/pulse_meter/__init__.py b/esphome/components/pulse_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pulse_meter/automation.h b/esphome/components/pulse_meter/automation.h new file mode 100644 index 0000000000..3112ded680 --- /dev/null +++ b/esphome/components/pulse_meter/automation.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/pulse_meter/pulse_meter_sensor.h" + +namespace esphome { + +namespace pulse_meter { + +template class SetTotalPulsesAction : public Action { + public: + SetTotalPulsesAction(PulseMeterSensor *pulse_meter) : pulse_meter_(pulse_meter) {} + + TEMPLATABLE_VALUE(uint32_t, total_pulses) + + void play(Ts... x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } + + protected: + PulseMeterSensor *pulse_meter_; +}; + +} // namespace pulse_meter +} // namespace esphome diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp new file mode 100644 index 0000000000..7d526b241b --- /dev/null +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -0,0 +1,85 @@ +#include "pulse_meter_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pulse_meter { + +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, gpio::INTERRUPT_ANY_EDGE); + + this->last_detected_edge_us_ = 0; + this->last_valid_edge_us_ = 0; +} + +void PulseMeterSensor::loop() { + const uint32_t now = micros(); + + // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until + // we get at least two valid pulses. + const uint32_t time_since_valid_edge_us = now - this->last_valid_edge_us_; + if ((this->last_valid_edge_us_ != 0) && (time_since_valid_edge_us > this->timeout_us_)) { + ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); + this->last_valid_edge_us_ = 0; + this->pulse_width_us_ = 0; + } + + // We quantize our pulse widths to 1 ms to avoid unnecessary jitter + const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; + if (this->pulse_width_dedupe_.next(pulse_width_ms)) { + if (pulse_width_ms == 0) { + // Treat 0 pulse width as 0 pulses/min (normally because we've not detected any pulses for a while) + this->publish_state(0); + } else { + // Calculate pulses/min from the pulse width in ms + this->publish_state((60.0f * 1000.0f) / pulse_width_ms); + } + } + + if (this->total_sensor_ != nullptr) { + const uint32_t total = this->total_pulses_; + if (this->total_dedupe_.next(total)) { + this->total_sensor_->publish_state(total); + } + } +} + +void PulseMeterSensor::set_total_pulses(uint32_t pulses) { this->total_pulses_ = pulses; } + +void PulseMeterSensor::dump_config() { + LOG_SENSOR("", "Pulse Meter", this); + LOG_PIN(" Pin: ", this->pin_); + ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->filter_us_); + ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %us", this->timeout_us_ / 1000000); +} + +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()) { + return; + } + + // Check to see if we should filter this edge out + if ((now - sensor->last_detected_edge_us_) >= sensor->filter_us_) { + // Don't measure the first valid pulse (we need at least two pulses to measure the width) + if (sensor->last_valid_edge_us_ != 0) { + sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_); + } + + sensor->total_pulses_++; + sensor->last_valid_edge_us_ = now; + } + + sensor->last_detected_edge_us_ = now; +} + +} // namespace pulse_meter +} // namespace esphome diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h new file mode 100644 index 0000000000..1cebc1748e --- /dev/null +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pulse_meter { + +class PulseMeterSensor : public sensor::Sensor, public Component { + public: + 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; } + + void set_total_pulses(uint32_t pulses); + + void setup() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + protected: + static void gpio_intr(PulseMeterSensor *sensor); + + InternalGPIOPin *pin_ = nullptr; + ISRInternalGPIOPin isr_pin_; + uint32_t filter_us_ = 0; + uint32_t timeout_us_ = 1000000UL * 60UL * 5UL; + sensor::Sensor *total_sensor_ = nullptr; + + Deduplicator pulse_width_dedupe_; + Deduplicator total_dedupe_; + + volatile uint32_t last_detected_edge_us_ = 0; + volatile uint32_t last_valid_edge_us_ = 0; + volatile uint32_t pulse_width_us_ = 0; + volatile uint32_t total_pulses_ = 0; +}; + +} // namespace pulse_meter +} // namespace esphome diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py new file mode 100644 index 0000000000..454cb3a69d --- /dev/null +++ b/esphome/components/pulse_meter/sensor.py @@ -0,0 +1,103 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation, pins +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_INTERNAL_FILTER, + CONF_PIN, + CONF_NUMBER, + CONF_TIMEOUT, + CONF_TOTAL, + CONF_VALUE, + ICON_PULSE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_PULSES, + UNIT_PULSES_PER_MINUTE, +) +from esphome.core import CORE + +CODEOWNERS = ["@stevebaxter"] + +pulse_meter_ns = cg.esphome_ns.namespace("pulse_meter") + +PulseMeterSensor = pulse_meter_ns.class_( + "PulseMeterSensor", sensor.Sensor, cg.Component +) + +SetTotalPulsesAction = pulse_meter_ns.class_("SetTotalPulsesAction", automation.Action) + + +def validate_internal_filter(value): + return cv.positive_time_period_microseconds(value) + + +def validate_timeout(value): + value = cv.positive_time_period_microseconds(value) + if value.total_minutes > 70: + raise cv.Invalid("Maximum timeout is 70 minutes") + return value + + +def validate_pulse_meter_pin(value): + 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 pulse counters on ESP8266." + ) + return value + + +CONFIG_SCHEMA = sensor.sensor_schema( + 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), + cv.Required(CONF_PIN): validate_pulse_meter_pin, + 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_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER])) + cg.add(var.set_timeout_us(config[CONF_TIMEOUT])) + + if CONF_TOTAL in config: + sens = await sensor.new_sensor(config[CONF_TOTAL]) + cg.add(var.set_total_sensor(sens)) + + +@automation.register_action( + "pulse_meter.set_total_pulses", + SetTotalPulsesAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(PulseMeterSensor), + cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t), + } + ), +) +async def set_total_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALUE], args, int) + cg.add(var.set_total_pulses(template_)) + return var diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index 4be536662b..8d66861049 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -4,10 +4,10 @@ namespace esphome { namespace pulse_width { -static const char *TAG = "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 8328da2ac0..b090647627 100644 --- a/esphome/components/pulse_width/sensor.py +++ b/esphome/components/pulse_width/sensor.py @@ -2,23 +2,41 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_ID, CONF_PIN, UNIT_SECOND, ICON_TIMER +from esphome.const import ( + CONF_ID, + CONF_PIN, + STATE_CLASS_MEASUREMENT, + UNIT_SECOND, + ICON_TIMER, +) -pulse_width_ns = cg.esphome_ns.namespace('pulse_width') +pulse_width_ns = cg.esphome_ns.namespace("pulse_width") -PulseWidthSensor = pulse_width_ns.class_('PulseWidthSensor', sensor.Sensor, cg.PollingComponent) +PulseWidthSensor = pulse_width_ns.class_( + "PulseWidthSensor", sensor.Sensor, cg.PollingComponent +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_SECOND, ICON_TIMER, 3).extend({ - cv.GenerateID(): cv.declare_id(PulseWidthSensor), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, - pins.validate_has_interrupt), -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + 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), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) 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..12090bddba --- /dev/null +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -0,0 +1,87 @@ +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, + ENTITY_CATEGORY_DIAGNOSTIC, + 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, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if 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 e2d832b019..e5418765bd 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -4,7 +4,15 @@ namespace esphome { namespace pzem004t { -static const char *TAG = "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(); @@ -59,11 +67,19 @@ void PZEM004T::loop() { if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(power); ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(READ_ENERGY); + break; + } + + case 0xA3: { // Energy Response + uint32_t energy = (uint32_t(resp[1]) << 16) | (uint32_t(resp[2]) << 8) | (uint32_t(resp[3])); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(energy); + ESP_LOGD(TAG, "Got Energy %u Wh", energy); this->write_state_(DONE); break; } - case 0xA3: // Energy Response case 0xA5: // Set Power Alarm Response case 0xB0: // Voltage Request case 0xB1: // Current Request diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index f0208d415a..f4f9f29b4d 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -12,6 +12,9 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + + void setup() override; void loop() override; @@ -23,12 +26,14 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; enum PZEM004TReadState { SET_ADDRESS = 0xB4, READ_VOLTAGE = 0xB0, READ_CURRENT = 0xB1, READ_POWER = 0xB2, + READ_ENERGY = 0xB3, DONE = 0x00, } read_state_{DONE}; diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index 6e3628c5ec..70dec82c3f 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -1,37 +1,82 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart -from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT +from esphome.const import ( + CONF_CURRENT, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + CONF_ENERGY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_WATT_HOURS, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -pzem004t_ns = cg.esphome_ns.namespace('pzem004t') -PZEM004T = pzem004t_ns.class_('PZEM004T', cg.PollingComponent, uart.UARTDevice) +pzem004t_ns = cg.esphome_ns.namespace("pzem004t") +PZEM004T = pzem004t_ns.class_("PZEM004T", cg.PollingComponent, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(PZEM004T), - - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), -}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PZEM004T), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) if CONF_VOLTAGE in config: conf = config[CONF_VOLTAGE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_voltage_sensor(sens)) if CONF_CURRENT in config: conf = config[CONF_CURRENT] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: conf = config[CONF_POWER] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index c79508d22f..b1a9607304 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace pzemac { -static const char *TAG = "pzemac"; +static const char *const TAG = "pzemac"; static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index e3d2b90742..b6697e3d19 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -1,53 +1,106 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, modbus -from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ - ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC, UNIT_HERTZ, \ - CONF_ENERGY, UNIT_WATT_HOURS, ICON_COUNTER +from esphome.const import ( + CONF_CURRENT, + CONF_ENERGY, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + CONF_FREQUENCY, + CONF_POWER_FACTOR, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_WATT_HOURS, +) -AUTO_LOAD = ['modbus'] +AUTO_LOAD = ["modbus"] -pzemac_ns = cg.esphome_ns.namespace('pzemac') -PZEMAC = pzemac_ns.class_('PZEMAC', cg.PollingComponent, modbus.ModbusDevice) +pzemac_ns = cg.esphome_ns.namespace("pzemac") +PZEMAC = pzemac_ns.class_("PZEMAC", cg.PollingComponent, modbus.ModbusDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(PZEMAC), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), - cv.Optional(CONF_ENERGY): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_COUNTER, 0), - cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), - cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), -}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PZEMAC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + 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_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(modbus.modbus_device_schema(0x01)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield modbus.register_modbus_device(var, config) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) if CONF_VOLTAGE in config: conf = config[CONF_VOLTAGE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_voltage_sensor(sens)) if CONF_CURRENT in config: conf = config[CONF_CURRENT] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: conf = config[CONF_POWER] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) if CONF_ENERGY in config: conf = config[CONF_ENERGY] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_energy_sensor(sens)) if CONF_FREQUENCY in config: conf = config[CONF_FREQUENCY] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_frequency_sensor(sens)) if CONF_POWER_FACTOR in config: conf = config[CONF_POWER_FACTOR] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_power_factor_sensor(sens)) diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 9bd58410c0..6a31a723a1 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace pzemdc { -static const char *TAG = "pzemdc"; +static const char *const TAG = "pzemdc"; static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 8c6fd08868..08ec688afb 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -1,36 +1,68 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, modbus -from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, ICON_POWER, ICON_CURRENT_AC +from esphome.const import ( + CONF_CURRENT, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) -AUTO_LOAD = ['modbus'] +AUTO_LOAD = ["modbus"] -pzemdc_ns = cg.esphome_ns.namespace('pzemdc') -PZEMDC = pzemdc_ns.class_('PZEMDC', cg.PollingComponent, modbus.ModbusDevice) +pzemdc_ns = cg.esphome_ns.namespace("pzemdc") +PZEMDC = pzemdc_ns.class_("PZEMDC", cg.PollingComponent, modbus.ModbusDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(PZEMDC), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), - cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), -}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PZEMDC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(modbus.modbus_device_schema(0x01)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield modbus.register_modbus_device(var, config) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) if CONF_VOLTAGE in config: conf = config[CONF_VOLTAGE] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_voltage_sensor(sens)) if CONF_CURRENT in config: conf = config[CONF_CURRENT] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_current_sensor(sens)) if CONF_POWER in config: conf = config[CONF_POWER] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index f809f9dfb3..f03b6af191 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -1,10 +1,12 @@ #include "qmc5883l.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include namespace esphome { namespace qmc5883l { -static const char *TAG = "qmc5883l"; +static const char *const TAG = "qmc5883l"; static const uint8_t QMC5883L_ADDRESS = 0x0D; static const uint8_t QMC5883L_REGISTER_DATA_X_LSB = 0x00; @@ -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 8a2952f54f..27d1df5b29 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,23 +1,34 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, - UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, - CONF_UPDATE_INTERVAL) +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_OVERSAMPLING, + CONF_RANGE, + ICON_MAGNET, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_MICROTESLA, + UNIT_DEGREES, + ICON_SCREEN_ROTATION, + CONF_UPDATE_INTERVAL, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -qmc5883l_ns = cg.esphome_ns.namespace('qmc5883l') +qmc5883l_ns = cg.esphome_ns.namespace("qmc5883l") -CONF_FIELD_STRENGTH_X = 'field_strength_x' -CONF_FIELD_STRENGTH_Y = 'field_strength_y' -CONF_FIELD_STRENGTH_Z = 'field_strength_z' -CONF_HEADING = 'heading' +CONF_FIELD_STRENGTH_X = "field_strength_x" +CONF_FIELD_STRENGTH_Y = "field_strength_y" +CONF_FIELD_STRENGTH_Z = "field_strength_z" +CONF_HEADING = "heading" QMC5883LComponent = qmc5883l_ns.class_( - 'QMC5883LComponent', cg.PollingComponent, i2c.I2CDevice) + "QMC5883LComponent", cg.PollingComponent, i2c.I2CDevice +) -QMC5883LDatarate = qmc5883l_ns.enum('QMC5883LDatarate') +QMC5883LDatarate = qmc5883l_ns.enum("QMC5883LDatarate") QMC5883LDatarates = { 10: QMC5883LDatarate.QMC5883L_DATARATE_10_HZ, 50: QMC5883LDatarate.QMC5883L_DATARATE_50_HZ, @@ -25,13 +36,13 @@ QMC5883LDatarates = { 200: QMC5883LDatarate.QMC5883L_DATARATE_200_HZ, } -QMC5883LRange = qmc5883l_ns.enum('QMC5883LRange') +QMC5883LRange = qmc5883l_ns.enum("QMC5883LRange") QMC5883L_RANGES = { 200: QMC5883LRange.QMC5883L_RANGE_200_UT, 800: QMC5883LRange.QMC5883L_RANGE_800_UT, } -QMC5883LOversampling = qmc5883l_ns.enum('QMC5883LOversampling') +QMC5883LOversampling = qmc5883l_ns.enum("QMC5883LOversampling") QMC5883LOversamplings = { 512: QMC5883LOversampling.QMC5883L_SAMPLING_512, 256: QMC5883LOversampling.QMC5883L_SAMPLING_256, @@ -51,53 +62,74 @@ def validate_enum(enum_values, units=None, int=True): value = cv.string(value) for unit in _units: if value.endswith(unit): - value = value[:-len(unit)] + value = value[: -len(unit)] break return enum_bound(value) + return validate_enum_bound -field_strength_schema = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1) -heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) +field_strength_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, +) +heading_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(QMC5883LComponent), - cv.Optional(CONF_ADDRESS): cv.i2c_address, - cv.Optional(CONF_RANGE, default='200µT'): validate_enum(QMC5883L_RANGES, units=["uT", "µT"]), - cv.Optional(CONF_OVERSAMPLING, default="512x"): validate_enum(QMC5883LOversamplings, units="x"), - cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, - cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, - cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, - cv.Optional(CONF_HEADING): heading_schema, -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x0D)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QMC5883LComponent), + cv.Optional(CONF_ADDRESS): cv.i2c_address, + cv.Optional(CONF_RANGE, default="200µT"): validate_enum( + QMC5883L_RANGES, units=["uT", "µT"] + ), + cv.Optional(CONF_OVERSAMPLING, default="512x"): validate_enum( + QMC5883LOversamplings, units="x" + ), + cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, + cv.Optional(CONF_HEADING): heading_schema, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x0D)) +) def auto_data_rate(config): interval_sec = config[CONF_UPDATE_INTERVAL].seconds - interval_hz = 1.0/interval_sec + interval_hz = 1.0 / interval_sec for datarate in sorted(QMC5883LDatarates.keys()): if float(datarate) >= interval_hz: return QMC5883LDatarates[datarate] return QMC5883LDatarates[200] -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) cg.add(var.set_datarate(auto_data_rate(config))) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: - sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) + sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) cg.add(var.set_x_sensor(sens)) if CONF_FIELD_STRENGTH_Y in config: - sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y]) + sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y]) cg.add(var.set_y_sensor(sens)) if CONF_FIELD_STRENGTH_Z in config: - sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z]) + sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z]) cg.add(var.set_z_sensor(sens)) if CONF_HEADING in config: - sens = yield sensor.new_sensor(config[CONF_HEADING]) + sens = await sensor.new_sensor(config[CONF_HEADING]) cg.add(var.set_heading_sensor(sens)) diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py new file mode 100644 index 0000000000..d64cf3c085 --- /dev/null +++ b/esphome/components/rc522/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation, pins +from esphome.components import i2c +from esphome.const import CONF_ON_TAG, CONF_TRIGGER_ID, CONF_RESET_PIN + +CODEOWNERS = ["@glmnet"] +AUTO_LOAD = ["binary_sensor"] + +CONF_RC522_ID = "rc522_id" + +rc522_ns = cg.esphome_ns.namespace("rc522") +RC522 = rc522_ns.class_("RC522", cg.PollingComponent, i2c.I2CDevice) +RC522Trigger = rc522_ns.class_( + "RC522Trigger", automation.Trigger.template(cg.std_string) +) + +RC522_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RC522), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RC522Trigger), + } + ), + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_rc522(var, config): + await cg.register_component(var, config) + + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_trigger(trigger)) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) diff --git a/esphome/components/rc522/binary_sensor.py b/esphome/components/rc522/binary_sensor.py new file mode 100644 index 0000000000..67d3068599 --- /dev/null +++ b/esphome/components/rc522/binary_sensor.py @@ -0,0 +1,50 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_UID, CONF_ID +from esphome.core import HexInt +from . import rc522_ns, RC522, CONF_RC522_ID + +DEPENDENCIES = ["rc522"] + + +def validate_uid(value): + value = cv.string_strict(value) + for x in value.split("-"): + if len(x) != 2: + raise cv.Invalid( + "Each part (separated by '-') of the UID must be two characters " + "long." + ) + try: + x = int(x, 16) + except ValueError as err: + raise cv.Invalid( + "Valid characters for parts of a UID are 0123456789ABCDEF." + ) from err + if x < 0 or x > 255: + raise cv.Invalid( + "Valid values for UID parts (separated by '-') are 00 to FF" + ) + return value + + +RC522BinarySensor = rc522_ns.class_("RC522BinarySensor", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RC522BinarySensor), + cv.GenerateID(CONF_RC522_ID): cv.use_id(RC522), + cv.Required(CONF_UID): validate_uid, + } +) + + +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_RC522_ID]) + cg.add(hub.register_tag(var)) + addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")] + cg.add(var.set_uid(addr)) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp new file mode 100644 index 0000000000..d203b3ce8f --- /dev/null +++ b/esphome/components/rc522/rc522.cpp @@ -0,0 +1,496 @@ +#include "rc522.h" +#include "esphome/core/log.h" + +// Based on: +// - https://github.com/miguelbalboa/rfid + +namespace esphome { +namespace rc522 { + +static const uint8_t WAIT_I_RQ = 0x30; // RxIRq and IdleIRq + +static const char *const TAG = "rc522"; + +static const uint8_t RESET_COUNT = 5; + +std::string format_buffer(uint8_t *b, uint8_t len) { + char buf[32]; + int offset = 0; + for (uint8_t i = 0; i < len; i++) { + const char *format = "%02X"; + if (i + 1 < len) + format = "%02X-"; + offset += sprintf(buf + offset, format, b[i]); + } + return std::string(buf); +} + +std::string format_uid(std::vector &uid) { + char buf[32]; + int offset = 0; + for (size_t i = 0; i < uid.size(); i++) { + const char *format = "%02X"; + if (i + 1 < uid.size()) + format = "%02X-"; + offset += sprintf(buf + offset, format, uid[i]); + } + return std::string(buf); +} + +void RC522::setup() { + state_ = STATE_SETUP; + // Pull device out of power down / reset state. + + // First set the resetPowerDownPin as digital input, to check the MFRC522 power down mode. + if (reset_pin_ != nullptr) { + reset_pin_->pin_mode(gpio::FLAG_INPUT); + + 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(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(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(); + return; + } + } + + // Setup a soft reset + reset_count_ = RESET_COUNT; + reset_timeout_ = millis(); +} + +void RC522::initialize_() { + // Per original code, wait 50 ms + if (millis() - reset_timeout_ < 50) + return; + + // Reset baud rates + ESP_LOGV(TAG, "Initialize"); + + pcd_write_register(TX_MODE_REG, 0x00); + pcd_write_register(RX_MODE_REG, 0x00); + // Reset ModWidthReg + pcd_write_register(MOD_WIDTH_REG, 0x26); + + // When communicating with a PICC we need a timeout if something goes wrong. + // f_timer = 13.56 MHz / (2*TPreScaler+1) where TPreScaler = [TPrescaler_Hi:TPrescaler_Lo]. + // TPrescaler_Hi are the four low bits in TModeReg. TPrescaler_Lo is TPrescalerReg. + pcd_write_register(T_MODE_REG, 0x80); // TAuto=1; timer starts automatically at the end of the transmission in all + // communication modes at all speeds + + // TPreScaler = TModeReg[3..0]:TPrescalerReg, ie 0x0A9 = 169 => f_timer=40kHz, ie a timer period of 25μs. + pcd_write_register(T_PRESCALER_REG, 0xA9); + pcd_write_register(T_RELOAD_REG_H, 0x03); // Reload timer with 0x3E8 = 1000, ie 25ms before timeout. + pcd_write_register(T_RELOAD_REG_L, 0xE8); + + // Default 0x00. Force a 100 % ASK modulation independent of the ModGsPReg register setting + pcd_write_register(TX_ASK_REG, 0x40); + pcd_write_register(MODE_REG, 0x3D); // Default 0x3F. Set the preset value for the CRC coprocessor for the CalcCRC + // command to 0x6363 (ISO 14443-3 part 6.2.4) + + state_ = STATE_INIT; +} + +void RC522::dump_config() { + ESP_LOGCONFIG(TAG, "RC522:"); + switch (this->error_code_) { + case NONE: + break; + case RESET_FAILED: + ESP_LOGE(TAG, "Reset command failed!"); + break; + } + + LOG_PIN(" RESET Pin: ", this->reset_pin_); + + LOG_UPDATE_INTERVAL(this); + + for (auto *child : this->binary_sensors_) { + LOG_BINARY_SENSOR(" ", "Tag", child); + } +} + +void RC522::update() { + if (state_ == STATE_INIT) { + pcd_antenna_on_(); + pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared. + buffer_[0] = PICC_CMD_REQA; + pcd_transceive_data_(1); + state_ = STATE_PICC_REQUEST_A; + } else { + ESP_LOGW(TAG, "Communication takes longer than update interval: %d", state_); + } +} + +void RC522::loop() { + // First check reset is needed + if (reset_count_ > 0) { + pcd_reset_(); + return; + } + if (state_ == STATE_SETUP) { + initialize_(); + return; + } + + StatusCode status = STATUS_ERROR; // For lint passing. TODO: refactor this + if (awaiting_comm_) { + if (state_ == STATE_SELECT_SERIAL_DONE) + status = await_crc_(); + else + status = await_transceive_(); + + if (status == STATUS_WAITING) { + return; + } + awaiting_comm_ = false; + ESP_LOGV(TAG, "finished communication status: %d, state: %d", status, state_); + } + + switch (state_) { + case STATE_PICC_REQUEST_A: { + if (status == STATUS_TIMEOUT) { // no tag present + for (auto *obj : this->binary_sensors_) + obj->on_scan_end(); // reset the binary sensors + ESP_LOGV(TAG, "CMD_REQA -> TIMEOUT (no tag present) %d", status); + state_ = STATE_DONE; + } else if (status != STATUS_OK) { + 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 unexpected back_length_ of %d", back_length_); + state_ = STATE_DONE; + } else { + state_ = STATE_READ_SERIAL; + } + if (state_ == STATE_DONE) { + // Don't wait another loop cycle + pcd_antenna_off_(); + } + break; + } + case STATE_READ_SERIAL: { + ESP_LOGV(TAG, "STATE_READ_SERIAL (%d)", status); + switch (uid_idx_) { + case 0: + buffer_[0] = PICC_CMD_SEL_CL1; + break; + case 3: + buffer_[0] = PICC_CMD_SEL_CL2; + break; + case 6: + buffer_[0] = PICC_CMD_SEL_CL3; + break; + default: + ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_); + state_ = STATE_DONE; + } + buffer_[1] = 32; + pcd_transceive_data_(2); + state_ = STATE_SELECT_SERIAL; + break; + } + case STATE_SELECT_SERIAL: { + buffer_[1] = 0x70; // select + // todo: set CRC + buffer_[6] = buffer_[2] ^ buffer_[3] ^ buffer_[4] ^ buffer_[5]; + pcd_calculate_crc_(buffer_, 7); + state_ = STATE_SELECT_SERIAL_DONE; + break; + } + case STATE_SELECT_SERIAL_DONE: { + send_len_ = 6; + pcd_transceive_data_(9); + state_ = STATE_READ_SERIAL_DONE; + break; + } + case STATE_READ_SERIAL_DONE: { + if (status != STATUS_OK || back_length_ != 3) { + if (status == STATUS_TIMEOUT) + ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status); + else + ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_, + format_buffer(buffer_, 9).c_str()); + + state_ = STATE_DONE; + uid_idx_ = 0; + + pcd_antenna_off_(); + return; + } + + // copy the uid + bool cascade = buffer_[2] == PICC_CMD_CT; // todo: should be determined based on select response (buffer[6]) + for (uint8_t i = 2 + cascade; i < 6; i++) + uid_buffer_[uid_idx_++] = buffer_[i]; + ESP_LOGVV(TAG, "copied uid to idx %d last byte is 0x%x, cascade is %d", uid_idx_, uid_buffer_[uid_idx_ - 1], + cascade); + + if (cascade) { // there is more bytes in the UID + state_ = STATE_READ_SERIAL; + return; + } + + std::vector rfid_uid(std::begin(uid_buffer_), std::begin(uid_buffer_) + uid_idx_); + uid_idx_ = 0; + // ESP_LOGD(TAG, "Processing '%s'", format_uid(rfid_uid).c_str()); + pcd_antenna_off_(); + state_ = STATE_INIT; // scan again on next update + bool report = true; + + for (auto *tag : this->binary_sensors_) { + if (tag->process(rfid_uid)) { + report = false; + } + } + + if (this->current_uid_ == rfid_uid) { + return; + } + + this->current_uid_ = rfid_uid; + + for (auto *trigger : this->triggers_) + trigger->process(rfid_uid); + + if (report) { + ESP_LOGD(TAG, "Found new tag '%s'", format_uid(rfid_uid).c_str()); + } + break; + } + case STATE_DONE: { + this->current_uid_ = {}; + state_ = STATE_INIT; + break; + } + default: + break; + } +} // namespace rc522 + +/** + * Performs a soft reset on the MFRC522 chip and waits for it to be ready again. + */ +void RC522::pcd_reset_() { + // The datasheet does not mention how long the SoftRest command takes to complete. + // But the MFRC522 might have been in soft power-down mode (triggered by bit 4 of CommandReg) + // 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. + + if (millis() - reset_timeout_ < 50) + return; + + if (reset_count_ == RESET_COUNT) { + ESP_LOGI(TAG, "Soft reset..."); + // Issue the SoftReset command. + pcd_write_register(COMMAND_REG, PCD_SOFT_RESET); + } + + // Expect the PowerDown bit in CommandReg to be cleared (max 3x50ms) + if ((pcd_read_register(COMMAND_REG) & (1 << 4)) == 0) { + reset_count_ = 0; + ESP_LOGI(TAG, "Device online."); + // Wait for initialize + reset_timeout_ = millis(); + return; + } + + if (--reset_count_ == 0) { + ESP_LOGE(TAG, "Unable to reset RC522."); + this->error_code_ = RESET_FAILED; + mark_failed(); + } +} + +/** + * Turns the antenna on by enabling pins TX1 and TX2. + * After a reset these pins are disabled. + */ +void RC522::pcd_antenna_on_() { + uint8_t value = pcd_read_register(TX_CONTROL_REG); + if ((value & 0x03) != 0x03) { + pcd_write_register(TX_CONTROL_REG, value | 0x03); + } +} + +/** + * Turns the antenna off by disabling pins TX1 and TX2. + */ +void RC522::pcd_antenna_off_() { + uint8_t value = pcd_read_register(TX_CONTROL_REG); + if ((value & 0x03) != 0x00) { + pcd_write_register(TX_CONTROL_REG, value & ~0x03); + } +} + +/** + * Sets the bits given in mask in register reg. + */ +void RC522::pcd_set_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to set. +) { + uint8_t tmp = pcd_read_register(reg); + pcd_write_register(reg, tmp | mask); // set bit mask +} + +/** + * Clears the bits given in mask from register reg. + */ +void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to clear. +) { + uint8_t tmp = pcd_read_register(reg); + pcd_write_register(reg, tmp & (~mask)); // clear bit mask +} + +/** + * Transfers data to the MFRC522 FIFO, executes a command, waits for completion and transfers data back from the FIFO. + * CRC validation can only be done if backData and backLen are specified. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ +void RC522::pcd_transceive_data_(uint8_t send_len) { + ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_buffer(buffer_, send_len).c_str()); + delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands + send_len_ = send_len; + // Prepare values for BitFramingReg + // For REQA and WUPA we need the short frame format - transmit only 7 bits of the last (and only) + // uint8_t. TxLastBits = BitFramingReg[2..0] + uint8_t bit_framing = (buffer_[0] == PICC_CMD_REQA) ? 7 : 0; + + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. + pcd_write_register(COM_IRQ_REG, 0x7F); // Clear all seven interrupt request bits + pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization + pcd_write_register(FIFO_DATA_REG, send_len_, buffer_); // Write sendData to the FIFO + pcd_write_register(BIT_FRAMING_REG, bit_framing); // Bit adjustments + pcd_write_register(COMMAND_REG, PCD_TRANSCEIVE); // Execute the command + pcd_set_register_bit_mask_(BIT_FRAMING_REG, 0x80); // StartSend=1, transmission of data starts + awaiting_comm_ = true; + awaiting_comm_time_ = millis(); +} + +RC522::StatusCode RC522::await_transceive_() { + if (millis() - awaiting_comm_time_ < 2) // wait at least 2 ms + return STATUS_WAITING; + uint8_t n = pcd_read_register( + COM_IRQ_REG); // ComIrqReg[7..0] bits are: Set1 TxIRq RxIRq IdleIRq HiAlertIRq LoAlertIRq ErrIRq TimerIRq + if (n & 0x01) { // Timer interrupt - nothing received in 25ms + back_length_ = 0; + error_counter_ = 0; // reset the error counter + return STATUS_TIMEOUT; + } + if (!(n & WAIT_I_RQ)) { // None of the interrupts that signal success has been set. + // Wait for the command to complete. + if (millis() - awaiting_comm_time_ < 40) + return STATUS_WAITING; + back_length_ = 0; + ESP_LOGW(TAG, "Communication with the MFRC522 might be down, reset in %d", + 10 - error_counter_); // todo: trigger reset? + if (error_counter_++ > 10) + setup(); + + return STATUS_TIMEOUT; + } + // Stop now if any errors except collisions were detected. + uint8_t error_reg_value = pcd_read_register( + ERROR_REG); // ErrorReg[7..0] bits are: WrErr TempErr reserved BufferOvfl CollErr CRCErr ParityErr ProtocolErr + if (error_reg_value & 0x13) { // BufferOvfl ParityErr ProtocolErr + return STATUS_ERROR; + } + error_counter_ = 0; // reset the error counter + + n = pcd_read_register(FIFO_LEVEL_REG); // Number of uint8_ts in the FIFO + if (n > sizeof(buffer_)) + return STATUS_NO_ROOM; + if (n > sizeof(buffer_) - send_len_) + send_len_ = sizeof(buffer_) - n; // simply overwrite the sent values + back_length_ = n; // Number of uint8_ts returned + pcd_read_register(FIFO_DATA_REG, n, buffer_ + send_len_, rx_align_); // Get received data from FIFO + uint8_t valid_bits_local = + pcd_read_register(CONTROL_REG) & 0x07; // RxLastBits[2:0] indicates the number of valid bits in the last + // received uint8_t. If this value is 000b, the whole uint8_t is valid. + + // Tell about collisions + if (error_reg_value & 0x08) { // CollErr + ESP_LOGW(TAG, "collision error, received %d bytes + %d bits (but anticollision not implemented)", + back_length_ - (valid_bits_local > 0), valid_bits_local); + return STATUS_COLLISION; + } + // Tell about collisions + if (valid_bits_local) { + ESP_LOGW(TAG, "only %d valid bits received, tag distance to high? Error code is 0x%x", valid_bits_local, + error_reg_value); // TODO: is this always due to collissions? + return STATUS_ERROR; + } + ESP_LOGV(TAG, "received %d bytes: %s", back_length_, format_buffer(buffer_ + send_len_, back_length_).c_str()); + + return STATUS_OK; +} + +/** + * Use the CRC coprocessor in the MFRC522 to calculate a CRC_A. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ + +void RC522::pcd_calculate_crc_(uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. + uint8_t length ///< In: The number of uint8_ts to transfer. +) { + ESP_LOGVV(TAG, "pcd_calculate_crc_(..., %d, ...)", length); + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. + pcd_write_register(DIV_IRQ_REG, 0x04); // Clear the CRCIRq interrupt request bit + pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization + pcd_write_register(FIFO_DATA_REG, length, data); // Write data to the FIFO + pcd_write_register(COMMAND_REG, PCD_CALC_CRC); // Start the calculation + + awaiting_comm_ = true; + awaiting_comm_time_ = millis(); +} + +RC522::StatusCode RC522::await_crc_() { + if (millis() - awaiting_comm_time_ < 2) // wait at least 2 ms + return STATUS_WAITING; + + // DivIrqReg[7..0] bits are: Set2 reserved reserved MfinActIRq reserved CRCIRq reserved reserved + uint8_t n = pcd_read_register(DIV_IRQ_REG); + if (n & 0x04) { // CRCIRq bit set - calculation done + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop calculating CRC for new content in the FIFO. + // Transfer the result from the registers to the result buffer + buffer_[7] = pcd_read_register(CRC_RESULT_REG_L); + buffer_[8] = pcd_read_register(CRC_RESULT_REG_H); + + ESP_LOGVV(TAG, "pcd_calculate_crc_() STATUS_OK"); + return STATUS_OK; + } + if (millis() - awaiting_comm_time_ < 89) + return STATUS_WAITING; + + ESP_LOGD(TAG, "pcd_calculate_crc_() TIMEOUT"); + // 89ms passed and nothing happened. Communication with the MFRC522 might be down. + return STATUS_TIMEOUT; +} + +bool RC522BinarySensor::process(std::vector &data) { + bool result = true; + if (data.size() != this->uid_.size()) + result = false; + else { + for (size_t i = 0; i < data.size(); i++) { + if (data[i] != this->uid_[i]) { + result = false; + break; + } + } + } + this->publish_state(result); + this->found_ = result; + return result; +} +void RC522Trigger::process(std::vector &data) { this->trigger(format_uid(data)); } + +} // namespace rc522 +} // namespace esphome diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h new file mode 100644 index 0000000000..d853d2f5ff --- /dev/null +++ b/esphome/components/rc522/rc522.h @@ -0,0 +1,276 @@ +#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" + +namespace esphome { +namespace rc522 { + +class RC522BinarySensor; +class RC522Trigger; +class RC522 : public PollingComponent { + public: + void setup() override; + + void dump_config() override; + + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void loop() override; + + void register_tag(RC522BinarySensor *tag) { this->binary_sensors_.push_back(tag); } + void register_trigger(RC522Trigger *trig) { this->triggers_.push_back(trig); } + + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + + protected: + // Return codes from the functions in this class. Remember to update GetStatusCodeName() if you add more. + // last value set to 0xff, then compiler uses less ram, it seems some optimisations are triggered + enum StatusCode : uint8_t { + STATUS_OK, // Success + STATUS_WAITING, // Waiting result from RC522 chip + STATUS_ERROR, // Error in communication + 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 ;-) + STATUS_INVALID, // Invalid argument. + STATUS_CRC_WRONG, // The CRC_A does not match + STATUS_MIFARE_NACK = 0xff // A MIFARE PICC responded with NAK. + }; + + enum State { + STATE_NONE = 0, + STATE_SETUP, + STATE_INIT, + STATE_PICC_REQUEST_A, + STATE_READ_SERIAL, + STATE_SELECT_SERIAL, + STATE_SELECT_SERIAL_DONE, + STATE_READ_SERIAL_DONE, + STATE_DONE, + } state_{STATE_NONE}; + + enum PcdRegister : uint8_t { + // Page 0: Command and status + // 0x00 // reserved for future use + COMMAND_REG = 0x01 << 1, // starts and stops command execution + COM_I_EN_REG = 0x02 << 1, // enable and disable interrupt request control bits + DIV_I_EN_REG = 0x03 << 1, // enable and disable interrupt request control bits + COM_IRQ_REG = 0x04 << 1, // interrupt request bits + DIV_IRQ_REG = 0x05 << 1, // interrupt request bits + ERROR_REG = 0x06 << 1, // error bits showing the error status of the last command executed + STATUS1_REG = 0x07 << 1, // communication status bits + STATUS2_REG = 0x08 << 1, // receiver and transmitter status bits + FIFO_DATA_REG = 0x09 << 1, // input and output of 64 uint8_t FIFO buffer + FIFO_LEVEL_REG = 0x0A << 1, // number of uint8_ts stored in the FIFO buffer + WATER_LEVEL_REG = 0x0B << 1, // level for FIFO underflow and overflow warning + CONTROL_REG = 0x0C << 1, // miscellaneous control registers + BIT_FRAMING_REG = 0x0D << 1, // adjustments for bit-oriented frames + COLL_REG = 0x0E << 1, // bit position of the first bit-collision detected on the RF interface + // 0x0F // reserved for future use + + // Page 1: Command + // 0x10 // reserved for future use + MODE_REG = 0x11 << 1, // defines general modes for transmitting and receiving + TX_MODE_REG = 0x12 << 1, // defines transmission data rate and framing + RX_MODE_REG = 0x13 << 1, // defines reception data rate and framing + TX_CONTROL_REG = 0x14 << 1, // controls the logical behavior of the antenna driver pins TX1 and TX2 + TX_ASK_REG = 0x15 << 1, // controls the setting of the transmission modulation + TX_SEL_REG = 0x16 << 1, // selects the internal sources for the antenna driver + RX_SEL_REG = 0x17 << 1, // selects internal receiver settings + RX_THRESHOLD_REG = 0x18 << 1, // selects thresholds for the bit decoder + DEMOD_REG = 0x19 << 1, // defines demodulator settings + // 0x1A // reserved for future use + // 0x1B // reserved for future use + MF_TX_REG = 0x1C << 1, // controls some MIFARE communication transmit parameters + MF_RX_REG = 0x1D << 1, // controls some MIFARE communication receive parameters + // 0x1E // reserved for future use + SERIAL_SPEED_REG = 0x1F << 1, // selects the speed of the serial UART interface + + // Page 2: Configuration + // 0x20 // reserved for future use + CRC_RESULT_REG_H = 0x21 << 1, // shows the MSB and LSB values of the CRC calculation + CRC_RESULT_REG_L = 0x22 << 1, + // 0x23 // reserved for future use + MOD_WIDTH_REG = 0x24 << 1, // controls the ModWidth setting? + // 0x25 // reserved for future use + RF_CFG_REG = 0x26 << 1, // configures the receiver gain + GS_N_REG = 0x27 << 1, // selects the conductance of the antenna driver pins TX1 and TX2 for modulation + CW_GS_P_REG = 0x28 << 1, // defines the conductance of the p-driver output during periods of no modulation + MOD_GS_P_REG = 0x29 << 1, // defines the conductance of the p-driver output during periods of modulation + T_MODE_REG = 0x2A << 1, // defines settings for the internal timer + T_PRESCALER_REG = 0x2B << 1, // the lower 8 bits of the TPrescaler value. The 4 high bits are in TModeReg. + T_RELOAD_REG_H = 0x2C << 1, // defines the 16-bit timer reload value + T_RELOAD_REG_L = 0x2D << 1, + T_COUNTER_VALUE_REG_H = 0x2E << 1, // shows the 16-bit timer value + T_COUNTER_VALUE_REG_L = 0x2F << 1, + + // Page 3: Test Registers + // 0x30 // reserved for future use + TEST_SEL1_REG = 0x31 << 1, // general test signal configuration + TEST_SEL2_REG = 0x32 << 1, // general test signal configuration + TEST_PIN_EN_REG = 0x33 << 1, // enables pin output driver on pins D1 to D7 + TEST_PIN_VALUE_REG = 0x34 << 1, // defines the values for D1 to D7 when it is used as an I/O bus + TEST_BUS_REG = 0x35 << 1, // shows the status of the internal test bus + AUTO_TEST_REG = 0x36 << 1, // controls the digital self-test + VERSION_REG = 0x37 << 1, // shows the software version + ANALOG_TEST_REG = 0x38 << 1, // controls the pins AUX1 and AUX2 + TEST_DA_C1_REG = 0x39 << 1, // defines the test value for TestDAC1 + TEST_DA_C2_REG = 0x3A << 1, // defines the test value for TestDAC2 + TEST_ADC_REG = 0x3B << 1 // shows the value of ADC I and Q channels + // 0x3C // reserved for production tests + // 0x3D // reserved for production tests + // 0x3E // reserved for production tests + // 0x3F // reserved for production tests + }; + + // MFRC522 commands. Described in chapter 10 of the datasheet. + enum PcdCommand : uint8_t { + PCD_IDLE = 0x00, // no action, cancels current command execution + PCD_MEM = 0x01, // stores 25 uint8_ts into the internal buffer + PCD_GENERATE_RANDOM_ID = 0x02, // generates a 10-uint8_t random ID number + PCD_CALC_CRC = 0x03, // activates the CRC coprocessor or performs a self-test + PCD_TRANSMIT = 0x04, // transmits data from the FIFO buffer + PCD_NO_CMD_CHANGE = 0x07, // no command change, can be used to modify the CommandReg register bits without + // affecting the command, for example, the PowerDown bit + PCD_RECEIVE = 0x08, // activates the receiver circuits + PCD_TRANSCEIVE = + 0x0C, // transmits data from FIFO buffer to antenna and automatically activates the receiver after transmission + PCD_MF_AUTHENT = 0x0E, // performs the MIFARE standard authentication as a reader + PCD_SOFT_RESET = 0x0F // resets the MFRC522 + }; + + // Commands sent to the PICC. + enum PiccCommand : uint8_t { + // The commands used by the PCD to manage communication with several PICCs (ISO 14443-3, Type A, section 6.4) + PICC_CMD_REQA = 0x26, // REQuest command, Type A. Invites PICCs in state IDLE to go to READY and prepare for + // anticollision or selection. 7 bit frame. + PICC_CMD_WUPA = 0x52, // Wake-UP command, Type A. Invites PICCs in state IDLE and HALT to go to READY(*) and + // prepare for anticollision or selection. 7 bit frame. + PICC_CMD_CT = 0x88, // Cascade Tag. Not really a command, but used during anti collision. + PICC_CMD_SEL_CL1 = 0x93, // Anti collision/Select, Cascade Level 1 + PICC_CMD_SEL_CL2 = 0x95, // Anti collision/Select, Cascade Level 2 + PICC_CMD_SEL_CL3 = 0x97, // Anti collision/Select, Cascade Level 3 + PICC_CMD_HLTA = 0x50, // HaLT command, Type A. Instructs an ACTIVE PICC to go to state HALT. + PICC_CMD_RATS = 0xE0, // Request command for Answer To Reset. + // The commands used for MIFARE Classic (from http://www.mouser.com/ds/2/302/MF1S503x-89574.pdf, Section 9) + // Use PCD_MFAuthent to authenticate access to a sector, then use these commands to read/write/modify the blocks on + // the sector. + // The read/write commands can also be used for MIFARE Ultralight. + PICC_CMD_MF_AUTH_KEY_A = 0x60, // Perform authentication with Key A + PICC_CMD_MF_AUTH_KEY_B = 0x61, // Perform authentication with Key B + PICC_CMD_MF_READ = + 0x30, // Reads one 16 uint8_t block from the authenticated sector of the PICC. Also used for MIFARE Ultralight. + PICC_CMD_MF_WRITE = 0xA0, // Writes one 16 uint8_t block to the authenticated sector of the PICC. Called + // "COMPATIBILITY WRITE" for MIFARE Ultralight. + PICC_CMD_MF_DECREMENT = + 0xC0, // Decrements the contents of a block and stores the result in the internal data register. + PICC_CMD_MF_INCREMENT = + 0xC1, // Increments the contents of a block and stores the result in the internal data register. + PICC_CMD_MF_RESTORE = 0xC2, // Reads the contents of a block into the internal data register. + PICC_CMD_MF_TRANSFER = 0xB0, // Writes the contents of the internal data register to a block. + // The commands used for MIFARE Ultralight (from http://www.nxp.com/documents/data_sheet/MF0ICU1.pdf, Section 8.6) + // The PICC_CMD_MF_READ and PICC_CMD_MF_WRITE can also be used for MIFARE Ultralight. + PICC_CMD_UL_WRITE = 0xA2 // Writes one 4 uint8_t page to the PICC. + }; + + void pcd_reset_(); + void initialize_(); + void pcd_antenna_on_(); + void pcd_antenna_off_(); + + virtual uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. + ) = 0; + + /** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + virtual void pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. + ) = 0; + virtual void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. + ) = 0; + + /** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + virtual void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. + ) = 0; + + void pcd_set_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to set. + ); + void pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to clear. + ); + + void pcd_transceive_data_(uint8_t send_len); + + void pcd_calculate_crc_(uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. + uint8_t length ///< In: The number of uint8_ts to transfer. + ); + + bool awaiting_comm_; + uint32_t awaiting_comm_time_; + StatusCode await_transceive_(); + StatusCode await_crc_(); + + uint8_t buffer_[9]; ///< buffer for communication, the first bits [0..back_idx-1] are for tx , + ///< [back_idx..back_idx+back_len] for rx + uint8_t send_len_; // index of first byte for RX + uint8_t back_length_; ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned. + uint8_t uid_buffer_[10]; // buffer to construct the uid (for 7 and 10 bit uids) + uint8_t uid_idx_ = 0; // number of read uid bytes e.g. index of the next available position in uid_buffer + uint8_t error_counter_ = 0; // to reset if unresponsive + uint8_t rx_align_; + uint8_t *valid_bits_; + + GPIOPin *reset_pin_{nullptr}; + uint8_t reset_count_{0}; + uint32_t reset_timeout_{0}; + std::vector binary_sensors_; + std::vector triggers_; + std::vector current_uid_; + + enum RC522Error { + NONE = 0, + RESET_FAILED, + } error_code_{NONE}; +}; + +class RC522BinarySensor : public binary_sensor::BinarySensor { + public: + void set_uid(const std::vector &uid) { uid_ = uid; } + + bool process(std::vector &data); + + void on_scan_end() { + if (!this->found_) { + this->publish_state(false); + } + this->found_ = false; + } + + protected: + std::vector uid_; + bool found_{false}; +}; + +class RC522Trigger : public Trigger { + public: + void process(std::vector &data); +}; + +} // namespace rc522 +} // namespace esphome diff --git a/esphome/components/rc522_i2c/__init__.py b/esphome/components/rc522_i2c/__init__.py new file mode 100644 index 0000000000..e42817352c --- /dev/null +++ b/esphome/components/rc522_i2c/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, rc522 +from esphome.const import CONF_ID + +CODEOWNERS = ["@glmnet"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["rc522"] +MULTI_CONF = True + +rc522_i2c_ns = cg.esphome_ns.namespace("rc522_i2c") +RC522I2C = rc522_i2c_ns.class_("RC522I2C", rc522.RC522, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + rc522.RC522_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RC522I2C), + } + ).extend(i2c.i2c_device_schema(0x2C)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await rc522.setup_rc522(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/rc522_i2c/rc522_i2c.cpp b/esphome/components/rc522_i2c/rc522_i2c.cpp new file mode 100644 index 0000000000..6a3d8d2486 --- /dev/null +++ b/esphome/components/rc522_i2c/rc522_i2c.cpp @@ -0,0 +1,70 @@ +#include "rc522_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rc522_i2c { + +static const char *const TAG = "rc522_i2c"; + +void RC522I2C::dump_config() { + RC522::dump_config(); + LOG_I2C_DEVICE(this); +} + +/** + * Reads a uint8_t from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +uint8_t RC522I2C::pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. +) { + uint8_t value; + if (!read_byte(reg >> 1, &value)) + return 0; + ESP_LOGVV(TAG, "read_register_(%x) -> %u", reg, value); + return value; +} + +/** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522I2C::pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. +) { + if (count == 0) { + return; + } + + uint8_t b = values[0]; + read_bytes(reg >> 1, values, count); + + if (rx_align) // Only update bit positions rxAlign..7 in values[0] + { + // Create bit mask for bit positions rxAlign..7 + uint8_t mask = 0xFF << rx_align; + // Apply mask to both current value of values[0] and the new data in values array. + values[0] = (b & ~mask) | (values[0] & mask); + } +} + +void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. +) { + this->write_byte(reg >> 1, value); +} + +/** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. +) { + write_bytes(reg >> 1, values, count); +} + +} // namespace rc522_i2c +} // namespace esphome diff --git a/esphome/components/rc522_i2c/rc522_i2c.h b/esphome/components/rc522_i2c/rc522_i2c.h new file mode 100644 index 0000000000..8d8b0a0716 --- /dev/null +++ b/esphome/components/rc522_i2c/rc522_i2c.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/rc522/rc522.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace rc522_i2c { + +class RC522I2C : public rc522::RC522, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. + ) override; + + /** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. + ) override; + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. + ) override; + + /** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. + ) override; +}; + +} // namespace rc522_i2c +} // namespace esphome diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py new file mode 100644 index 0000000000..77b0a99662 --- /dev/null +++ b/esphome/components/rc522_spi/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, rc522 +from esphome.const import CONF_ID + +CODEOWNERS = ["@glmnet"] +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["rc522"] +MULTI_CONF = True + +rc522_spi_ns = cg.esphome_ns.namespace("rc522_spi") +RC522Spi = rc522_spi_ns.class_("RC522Spi", rc522.RC522, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + rc522.RC522_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RC522Spi), + } + ).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) diff --git a/esphome/components/rc522_spi/binary_sensor.py b/esphome/components/rc522_spi/binary_sensor.py new file mode 100644 index 0000000000..8139f6d2ac --- /dev/null +++ b/esphome/components/rc522_spi/binary_sensor.py @@ -0,0 +1,9 @@ +import esphome.components.rc522.binary_sensor as rc522_binary_sensor + +DEPENDENCIES = ["rc522"] + +CONFIG_SCHEMA = rc522_binary_sensor.CONFIG_SCHEMA + + +async def to_code(config): + await rc522_binary_sensor.to_code(config) diff --git a/esphome/components/rc522_spi/rc522_spi.cpp b/esphome/components/rc522_spi/rc522_spi.cpp new file mode 100644 index 0000000000..fe1f6097e2 --- /dev/null +++ b/esphome/components/rc522_spi/rc522_spi.cpp @@ -0,0 +1,140 @@ +#include "rc522_spi.h" +#include "esphome/core/log.h" + +// Based on: +// - https://github.com/miguelbalboa/rfid + +namespace esphome { +namespace rc522_spi { + +static const char *const TAG = "rc522_spi"; + +void RC522Spi::setup() { + ESP_LOGI(TAG, "SPI Setup"); + this->spi_setup(); + + RC522::setup(); +} + +void RC522Spi::dump_config() { + RC522::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +/** + * Reads a uint8_t from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +uint8_t RC522Spi::pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. +) { + uint8_t value; + enable(); + transfer_byte(0x80 | reg); + value = read_byte(); + disable(); + ESP_LOGVV(TAG, "read_register_(%d) -> %d", reg, value); + return value; +} + +/** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. +) { +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + std::string buf; + buf = "Rx"; + char cstrb[20]; +#endif + if (count == 0) { + return; + } + + // Serial.print(F("Reading ")); Serial.print(count); Serial.println(F(" uint8_ts from register.")); + uint8_t address = 0x80 | reg; // MSB == 1 is for reading. LSB is not used in address. Datasheet section 8.1.2.3. + uint8_t index = 0; // Index in values array. + enable(); + count--; // One read is performed outside of the loop + + write_byte(address); // Tell MFRC522 which address we want to read + if (rx_align) { // Only update bit positions rxAlign..7 in values[0] + // Create bit mask for bit positions rxAlign..7 + uint8_t mask = 0xFF << rx_align; + // Read value and tell that we want to read the same address again. + uint8_t value = transfer_byte(address); + // Apply mask to both current value of values[0] and the new data in value. + values[0] = (values[0] & ~mask) | (value & mask); + index++; + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + sprintf(cstrb, " %x", values[0]); + buf.append(cstrb); +#endif + } + while (index < count) { + values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again. + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + sprintf(cstrb, " %x", values[index]); + buf.append(cstrb); +#endif + + index++; + } + values[index] = transfer_byte(0); // Read the final uint8_t. Send 0 to stop reading. + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + buf = buf + " "; + sprintf(cstrb, "%x", values[index]); + buf.append(cstrb); + + ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str()); +#endif + disable(); +} + +void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. +) { + enable(); + // MSB == 0 is for writing. LSB is not used in address. Datasheet section 8.1.2.3. + transfer_byte(reg); + transfer_byte(value); + disable(); +} + +/** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. +) { +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + std::string buf; + buf = "Tx"; + char cstrb[20]; +#endif + + enable(); + transfer_byte(reg); + + for (uint8_t index = 0; index < count; index++) { + transfer_byte(values[index]); + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + sprintf(cstrb, " %x", values[index]); + buf.append(cstrb); +#endif + } + disable(); + ESP_LOGVV(TAG, "write_register_(%d, %d) -> %s", reg, count, buf.c_str()); +} + +} // namespace rc522_spi +} // namespace esphome diff --git a/esphome/components/rc522_spi/rc522_spi.h b/esphome/components/rc522_spi/rc522_spi.h new file mode 100644 index 0000000000..58edbbed4f --- /dev/null +++ b/esphome/components/rc522_spi/rc522_spi.h @@ -0,0 +1,55 @@ +/** + * Library based on https://github.com/miguelbalboa/rfid + * and adapted to ESPHome by @glmnet + * + * original authors Dr.Leong, Miguel Balboa, Søren Thing Andersen, Tom Clement, many more! See GitLog. + * + * + */ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/rc522/rc522.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace rc522_spi { + +class RC522Spi : public rc522::RC522, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. + ) override; + + /** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. + ) override; + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. + ) override; + + /** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. + ) override; +}; + +} // namespace rc522_spi +} // namespace esphome diff --git a/esphome/components/rdm6300/__init__.py b/esphome/components/rdm6300/__init__.py index ee5077c315..37ebcb49a9 100644 --- a/esphome/components/rdm6300/__init__.py +++ b/esphome/components/rdm6300/__init__.py @@ -4,27 +4,37 @@ from esphome import automation from esphome.components import uart from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID -DEPENDENCIES = ['uart'] -AUTO_LOAD = ['binary_sensor'] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor"] -rdm6300_ns = cg.esphome_ns.namespace('rdm6300') -RDM6300Component = rdm6300_ns.class_('RDM6300Component', cg.Component, uart.UARTDevice) -RDM6300Trigger = rdm6300_ns.class_('RDM6300Trigger', automation.Trigger.template(cg.uint32)) +rdm6300_ns = cg.esphome_ns.namespace("rdm6300") +RDM6300Component = rdm6300_ns.class_("RDM6300Component", cg.Component, uart.UARTDevice) +RDM6300Trigger = rdm6300_ns.class_( + "RDM6300Trigger", automation.Trigger.template(cg.uint32) +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(RDM6300Component), - cv.Optional(CONF_ON_TAG): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RDM6300Trigger), - }), -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RDM6300Component), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RDM6300Trigger), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) for conf in config.get(CONF_ON_TAG, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) cg.add(var.register_trigger(trigger)) - yield automation.build_automation(trigger, [(cg.uint32, 'x')], conf) + await automation.build_automation(trigger, [(cg.uint32, "x")], conf) diff --git a/esphome/components/rdm6300/binary_sensor.py b/esphome/components/rdm6300/binary_sensor.py index 81b24bed0e..c99a2bfc06 100644 --- a/esphome/components/rdm6300/binary_sensor.py +++ b/esphome/components/rdm6300/binary_sensor.py @@ -4,22 +4,26 @@ from esphome.components import binary_sensor, rdm6300 from esphome.const import CONF_UID, CONF_ID from . import rdm6300_ns -DEPENDENCIES = ['rdm6300'] +DEPENDENCIES = ["rdm6300"] -CONF_RDM6300_ID = 'rdm6300_id' -RDM6300BinarySensor = rdm6300_ns.class_('RDM6300BinarySensor', binary_sensor.BinarySensor) +CONF_RDM6300_ID = "rdm6300_id" +RDM6300BinarySensor = rdm6300_ns.class_( + "RDM6300BinarySensor", binary_sensor.BinarySensor +) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(RDM6300BinarySensor), - cv.GenerateID(CONF_RDM6300_ID): cv.use_id(rdm6300.RDM6300Component), - cv.Required(CONF_UID): cv.uint32_t, -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RDM6300BinarySensor), + cv.GenerateID(CONF_RDM6300_ID): cv.use_id(rdm6300.RDM6300Component), + cv.Required(CONF_UID): cv.uint32_t, + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) + await binary_sensor.register_binary_sensor(var, config) - hub = yield cg.get_variable(config[CONF_RDM6300_ID]) + hub = await cg.get_variable(config[CONF_RDM6300_ID]) cg.add(hub.register_card(var)) cg.add(var.set_id(config[CONF_UID])) diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index 6c6f0c0311..434b9f5720 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace rdm6300 { -static const char *TAG = "rdm6300"; +static const char *const TAG = "rdm6300"; static const uint8_t RDM6300_START_BYTE = 0x02; static const uint8_t RDM6300_END_BYTE = 0x03; @@ -46,8 +46,7 @@ void rdm6300::RDM6300Component::loop() { } else { // Valid data this->status_clear_warning(); - const uint32_t result = (uint32_t(this->buffer_[1]) << 24) | (uint32_t(this->buffer_[2]) << 16) | - (uint32_t(this->buffer_[3]) << 8) | this->buffer_[4]; + const uint32_t result = encode_uint32(this->buffer_[1], this->buffer_[2], this->buffer_[3], this->buffer_[4]); bool report = result != last_id_; for (auto *card : this->cards_) { if (card->process(result)) { diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 05a3e7e1aa..914ce42efe 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -2,29 +2,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor -from esphome.const import CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, \ - CONF_COMMAND, CONF_CODE, CONF_PULSE_LENGTH, CONF_SYNC, CONF_ZERO, CONF_ONE, CONF_INVERTED, \ - CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, CONF_REPEAT, \ - CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2 +from esphome.const import ( + CONF_DATA, + CONF_TRIGGER_ID, + CONF_NBITS, + CONF_ADDRESS, + CONF_COMMAND, + CONF_CODE, + CONF_PULSE_LENGTH, + CONF_SYNC, + CONF_ZERO, + CONF_ONE, + CONF_INVERTED, + CONF_PROTOCOL, + CONF_GROUP, + CONF_DEVICE, + CONF_STATE, + CONF_CHANNEL, + CONF_FAMILY, + CONF_REPEAT, + CONF_WAIT_TIME, + CONF_TIMES, + CONF_TYPE_ID, + CONF_CARRIER_FREQUENCY, + CONF_RC_CODE_1, + CONF_RC_CODE_2, +) from esphome.core import coroutine +from esphome.jsonschema import jschema_extractor from esphome.util import Registry, SimpleRegistry -AUTO_LOAD = ['binary_sensor'] +AUTO_LOAD = ["binary_sensor"] -CONF_RECEIVER_ID = 'receiver_id' -CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_RECEIVER_ID = "receiver_id" +CONF_TRANSMITTER_ID = "transmitter_id" -ns = remote_base_ns = cg.esphome_ns.namespace('remote_base') -RemoteProtocol = ns.class_('RemoteProtocol') -RemoteReceiverListener = ns.class_('RemoteReceiverListener') -RemoteReceiverBinarySensorBase = ns.class_('RemoteReceiverBinarySensorBase', - binary_sensor.BinarySensor, cg.Component) -RemoteReceiverTrigger = ns.class_('RemoteReceiverTrigger', automation.Trigger, - RemoteReceiverListener) -RemoteTransmitterDumper = ns.class_('RemoteTransmitterDumper') -RemoteTransmitterActionBase = ns.class_('RemoteTransmitterActionBase', automation.Action) -RemoteReceiverBase = ns.class_('RemoteReceiverBase') -RemoteTransmitterBase = ns.class_('RemoteTransmitterBase') +ns = remote_base_ns = cg.esphome_ns.namespace("remote_base") +RemoteProtocol = ns.class_("RemoteProtocol") +RemoteReceiverListener = ns.class_("RemoteReceiverListener") +RemoteReceiverBinarySensorBase = ns.class_( + "RemoteReceiverBinarySensorBase", binary_sensor.BinarySensor, cg.Component +) +RemoteReceiverTrigger = ns.class_( + "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener +) +RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") +RemoteTransmitterActionBase = ns.class_( + "RemoteTransmitterActionBase", automation.Action +) +RemoteReceiverBase = ns.class_("RemoteReceiverBase") +RemoteTransmitterBase = ns.class_("RemoteTransmitterBase") def templatize(value): @@ -36,9 +63,8 @@ def templatize(value): return cv.Schema(ret) -@coroutine -def register_listener(var, config): - receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) +async def register_listener(var, config): + receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) cg.add(receiver.register_listener(var)) @@ -47,20 +73,21 @@ def register_binary_sensor(name, type, schema): def register_trigger(name, type, data_type): - validator = automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), - }) - registerer = TRIGGER_REGISTRY.register(f'on_{name}', validator) + validator = automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), + cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + } + ) + registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator) def decorator(func): - @coroutine - def new_func(config): + async def new_func(config): var = cg.new_Pvariable(config[CONF_TRIGGER_ID]) - yield register_listener(var, config) - yield coroutine(func)(var, config) - yield automation.build_automation(var, [(data_type, 'x')], config) - yield var + await register_listener(var, config) + await coroutine(func)(var, config) + await automation.build_automation(var, [(data_type, "x")], config) + return var return registerer(new_func) @@ -71,11 +98,10 @@ def register_dumper(name, type): registerer = DUMPER_REGISTRY.register(name, type, {}) def decorator(func): - @coroutine - def new_func(config, dumper_id): + async def new_func(config, dumper_id): var = cg.new_Pvariable(dumper_id) - yield coroutine(func)(var, config) - yield var + await coroutine(func)(var, config) + return var return registerer(new_func) @@ -84,36 +110,44 @@ def register_dumper(name, type): def validate_repeat(value): if isinstance(value, dict): - return cv.Schema({ - cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='25ms'): - cv.templatable(cv.positive_time_period_microseconds), - })(value) + return cv.Schema( + { + cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), + cv.Optional(CONF_WAIT_TIME, default="25ms"): cv.templatable( + cv.positive_time_period_microseconds + ), + } + )(value) return validate_repeat({CONF_TIMES: value}) -def register_action(name, type_, schema): - validator = templatize(schema).extend({ +BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema( + { cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), cv.Optional(CONF_REPEAT): validate_repeat, - }) - registerer = automation.register_action(f'remote_transmitter.transmit_{name}', - type_, validator) + } +) + + +def register_action(name, type_, schema): + validator = templatize(schema).extend(BASE_REMOTE_TRANSMITTER_SCHEMA) + registerer = automation.register_action( + f"remote_transmitter.transmit_{name}", type_, validator + ) def decorator(func): - @coroutine - def new_func(config, action_id, template_arg, args): - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + async def new_func(config, action_id, template_arg, args): + transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) var = cg.new_Pvariable(action_id, template_arg) cg.add(var.set_parent(transmitter)) if CONF_REPEAT in config: conf = config[CONF_REPEAT] - template_ = yield cg.templatable(conf[CONF_TIMES], args, cg.uint32) + template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32) cg.add(var.set_send_times(template_)) - template_ = yield cg.templatable(conf[CONF_WAIT_TIME], args, cg.uint32) + template_ = await cg.templatable(conf[CONF_WAIT_TIME], args, cg.uint32) cg.add(var.set_send_wait(template_)) - yield coroutine(func)(var, config, args) - yield var + await coroutine(func)(var, config, args) + return var return registerer(new_func) @@ -121,241 +155,373 @@ def register_action(name, type_, schema): def declare_protocol(name): - data = ns.struct(f'{name}Data') - binary_sensor_ = ns.class_(f'{name}BinarySensor', RemoteReceiverBinarySensorBase) - trigger = ns.class_(f'{name}Trigger', RemoteReceiverTrigger) - action = ns.class_(f'{name}Action', RemoteTransmitterActionBase) - dumper = ns.class_(f'{name}Dumper', RemoteTransmitterDumper) + data = ns.struct(f"{name}Data") + binary_sensor_ = ns.class_(f"{name}BinarySensor", RemoteReceiverBinarySensorBase) + trigger = ns.class_(f"{name}Trigger", RemoteReceiverTrigger) + action = ns.class_(f"{name}Action", RemoteTransmitterActionBase) + dumper = ns.class_(f"{name}Dumper", RemoteTransmitterDumper) return data, binary_sensor_, trigger, action, dumper -BINARY_SENSOR_REGISTRY = Registry(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), -})) -validate_binary_sensor = cv.validate_registry_entry('remote receiver', BINARY_SENSOR_REGISTRY) +BINARY_SENSOR_REGISTRY = Registry( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + } + ) +) +validate_binary_sensor = cv.validate_registry_entry( + "remote receiver", BINARY_SENSOR_REGISTRY +) TRIGGER_REGISTRY = SimpleRegistry() -DUMPER_REGISTRY = Registry({ - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), -}) +DUMPER_REGISTRY = Registry( + { + cv.Optional(CONF_RECEIVER_ID): cv.invalid( + "This has been removed in ESPHome 1.20.0 and the dumper attaches directly to the parent receiver." + ), + } +) def validate_dumpers(value): - if isinstance(value, str) and value.lower() == 'all': + if isinstance(value, str) and value.lower() == "all": return validate_dumpers(list(DUMPER_REGISTRY.keys())) - return cv.validate_registry('dumper', DUMPER_REGISTRY)(value) + return cv.validate_registry("dumper", DUMPER_REGISTRY)(value) def validate_triggers(base_schema): assert isinstance(base_schema, cv.Schema) + @jschema_extractor("triggers") def validator(config): added_keys = {} for key, (_, valid) in TRIGGER_REGISTRY.items(): added_keys[cv.Optional(key)] = valid new_schema = base_schema.extend(added_keys) + # pylint: disable=comparison-with-callable + if config == jschema_extractor: + return new_schema return new_schema(config) return validator -@coroutine -def build_binary_sensor(full_config): - registry_entry, config = cg.extract_registry_entry_config(BINARY_SENSOR_REGISTRY, full_config) +async def build_binary_sensor(full_config): + registry_entry, config = cg.extract_registry_entry_config( + BINARY_SENSOR_REGISTRY, full_config + ) type_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun var = cg.new_Pvariable(type_id) - yield cg.register_component(var, full_config) - yield register_listener(var, full_config) - yield builder(var, config) - yield var + await cg.register_component(var, full_config) + await register_listener(var, full_config) + await builder(var, config) + return var -@coroutine -def build_triggers(full_config): +async def build_triggers(full_config): for key in TRIGGER_REGISTRY: for config in full_config.get(key, []): func = TRIGGER_REGISTRY[key][0] - yield func(config) + await func(config) -@coroutine -def build_dumpers(config): +async def build_dumpers(config): dumpers = [] for conf in config: - dumper = yield cg.build_registry_entry(DUMPER_REGISTRY, conf) - receiver = yield cg.get_variable(conf[CONF_RECEIVER_ID]) - cg.add(receiver.register_dumper(dumper)) + dumper = await cg.build_registry_entry(DUMPER_REGISTRY, conf) dumpers.append(dumper) - yield dumpers + 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') +JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) -@register_binary_sensor('jvc', JVCBinarySensor, JVC_SCHEMA) +@register_binary_sensor("jvc", JVCBinarySensor, JVC_SCHEMA) def jvc_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - JVCData, - ('data', config[CONF_DATA]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + JVCData, + ("data", config[CONF_DATA]), + ) + ) + ) -@register_trigger('jvc', JVCTrigger, JVCData) +@register_trigger("jvc", JVCTrigger, JVCData) def jvc_trigger(var, config): pass -@register_dumper('jvc', JVCDumper) +@register_dumper("jvc", JVCDumper) def jvc_dumper(var, config): pass -@register_action('jvc', JVCAction, JVC_SCHEMA) -def jvc_action(var, config, args): - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) +@register_action("jvc", JVCAction, JVC_SCHEMA) +async def jvc_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) # LG -LGData, LGBinarySensor, LGTrigger, LGAction, LGDumper = declare_protocol('LG') -LG_SCHEMA = cv.Schema({ - cv.Required(CONF_DATA): cv.hex_uint32_t, - cv.Optional(CONF_NBITS, default=28): cv.one_of(28, 32, int=True), -}) +LGData, LGBinarySensor, LGTrigger, LGAction, LGDumper = declare_protocol("LG") +LG_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.hex_uint32_t, + cv.Optional(CONF_NBITS, default=28): cv.one_of(28, 32, int=True), + } +) -@register_binary_sensor('lg', LGBinarySensor, LG_SCHEMA) +@register_binary_sensor("lg", LGBinarySensor, LG_SCHEMA) def lg_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - LGData, - ('data', config[CONF_DATA]), - ('nbits', config[CONF_NBITS]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + LGData, + ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), + ) + ) + ) -@register_trigger('lg', LGTrigger, LGData) +@register_trigger("lg", LGTrigger, LGData) def lg_trigger(var, config): pass -@register_dumper('lg', LGDumper) +@register_dumper("lg", LGDumper) def lg_dumper(var, config): pass -@register_action('lg', LGAction, LG_SCHEMA) -def lg_action(var, config, args): - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) +@register_action("lg", LGAction, LG_SCHEMA) +async def lg_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint8) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) # NEC -NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol('NEC') -NEC_SCHEMA = cv.Schema({ - cv.Required(CONF_ADDRESS): cv.hex_uint16_t, - cv.Required(CONF_COMMAND): cv.hex_uint16_t, -}) +NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC") +NEC_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + cv.Required(CONF_COMMAND): cv.hex_uint16_t, + } +) -@register_binary_sensor('nec', NECBinarySensor, NEC_SCHEMA) +@register_binary_sensor("nec", NECBinarySensor, NEC_SCHEMA) def nec_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - NECData, - ('address', config[CONF_ADDRESS]), - ('command', config[CONF_COMMAND]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + NECData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) -@register_trigger('nec', NECTrigger, NECData) +@register_trigger("nec", NECTrigger, NECData) def nec_trigger(var, config): pass -@register_dumper('nec', NECDumper) +@register_dumper("nec", NECDumper) def nec_dumper(var, config): pass -@register_action('nec', NECAction, NEC_SCHEMA) -def nec_action(var, config, args): - template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint16) +@register_action("nec", NECAction, NEC_SCHEMA) +async def nec_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) cg.add(var.set_address(template_)) - template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint16) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16) cg.add(var.set_command(template_)) # Pioneer -(PioneerData, PioneerBinarySensor, PioneerTrigger, PioneerAction, - PioneerDumper) = declare_protocol('Pioneer') -PIONEER_SCHEMA = cv.Schema({ - cv.Required(CONF_RC_CODE_1): cv.hex_uint16_t, - cv.Optional(CONF_RC_CODE_2, default=0): cv.hex_uint16_t, -}) +( + PioneerData, + PioneerBinarySensor, + PioneerTrigger, + PioneerAction, + PioneerDumper, +) = declare_protocol("Pioneer") +PIONEER_SCHEMA = cv.Schema( + { + cv.Required(CONF_RC_CODE_1): cv.hex_uint16_t, + cv.Optional(CONF_RC_CODE_2, default=0): cv.hex_uint16_t, + } +) -@register_binary_sensor('pioneer', PioneerBinarySensor, PIONEER_SCHEMA) +@register_binary_sensor("pioneer", PioneerBinarySensor, PIONEER_SCHEMA) def pioneer_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - PioneerData, - ('rc_code_1', config[CONF_RC_CODE_1]), - ('rc_code_2', config[CONF_RC_CODE_2]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + PioneerData, + ("rc_code_1", config[CONF_RC_CODE_1]), + ("rc_code_2", config[CONF_RC_CODE_2]), + ) + ) + ) -@register_trigger('pioneer', PioneerTrigger, PioneerData) +@register_trigger("pioneer", PioneerTrigger, PioneerData) def pioneer_trigger(var, config): pass -@register_dumper('pioneer', PioneerDumper) +@register_dumper("pioneer", PioneerDumper) def pioneer_dumper(var, config): pass -@register_action('pioneer', PioneerAction, PIONEER_SCHEMA) -def pioneer_action(var, config, args): - template_ = yield cg.templatable(config[CONF_RC_CODE_1], args, cg.uint16) +@register_action("pioneer", PioneerAction, PIONEER_SCHEMA) +async def pioneer_action(var, config, args): + template_ = await cg.templatable(config[CONF_RC_CODE_1], args, cg.uint16) cg.add(var.set_rc_code_1(template_)) - template_ = yield cg.templatable(config[CONF_RC_CODE_2], args, cg.uint16) + template_ = await cg.templatable(config[CONF_RC_CODE_2], args, cg.uint16) cg.add(var.set_rc_code_2(template_)) +# Pronto +( + ProntoData, + ProntoBinarySensor, + ProntoTrigger, + ProntoAction, + ProntoDumper, +) = declare_protocol("Pronto") +PRONTO_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.string, + } +) + + +@register_binary_sensor("pronto", ProntoBinarySensor, PRONTO_SCHEMA) +def pronto_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + ProntoData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("pronto", ProntoTrigger, ProntoData) +def pronto_trigger(var, config): + pass + + +@register_dumper("pronto", ProntoDumper) +def pronto_dumper(var, config): + pass + + +@register_action("pronto", ProntoAction, PRONTO_SCHEMA) +async def pronto_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.std_string) + cg.add(var.set_data(template_)) + + # Sony -SonyData, SonyBinarySensor, SonyTrigger, SonyAction, SonyDumper = declare_protocol('Sony') -SONY_SCHEMA = cv.Schema({ - cv.Required(CONF_DATA): cv.hex_uint32_t, - cv.Optional(CONF_NBITS, default=12): cv.one_of(12, 15, 20, int=True), -}) +SonyData, SonyBinarySensor, SonyTrigger, SonyAction, SonyDumper = declare_protocol( + "Sony" +) +SONY_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.hex_uint32_t, + cv.Optional(CONF_NBITS, default=12): cv.one_of(12, 15, 20, int=True), + } +) -@register_binary_sensor('sony', SonyBinarySensor, SONY_SCHEMA) +@register_binary_sensor("sony", SonyBinarySensor, SONY_SCHEMA) def sony_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - SonyData, - ('data', config[CONF_DATA]), - ('nbits', config[CONF_NBITS]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + SonyData, + ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), + ) + ) + ) -@register_trigger('sony', SonyTrigger, SonyData) +@register_trigger("sony", SonyTrigger, SonyData) def sony_trigger(var, config): pass -@register_dumper('sony', SonyDumper) +@register_dumper("sony", SonyDumper) def sony_dumper(var, config): pass -@register_action('sony', SonyAction, SONY_SCHEMA) -def sony_action(var, config, args): - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint16) +@register_action("sony", SonyAction, SONY_SCHEMA) +async def sony_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16) cg.add(var.set_data(template_)) - template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint32) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) cg.add(var.set_nbits(template_)) @@ -367,22 +533,29 @@ def validate_raw_alternating(value): this_negative = val < 0 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), [i]) + raise cv.Invalid( + f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", + [i], + ) last_negative = this_negative return value -RawData, RawBinarySensor, RawTrigger, RawAction, RawDumper = declare_protocol('Raw') -CONF_CODE_STORAGE_ID = 'code_storage_id' -RAW_SCHEMA = cv.Schema({ - cv.Required(CONF_CODE): cv.All([cv.Any(cv.int_, cv.time_period_microseconds)], - cv.Length(min=1), validate_raw_alternating), - cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.int32), -}) +RawData, RawBinarySensor, RawTrigger, RawAction, RawDumper = declare_protocol("Raw") +CONF_CODE_STORAGE_ID = "code_storage_id" +RAW_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.int_, cv.time_period_microseconds)], + cv.Length(min=1), + validate_raw_alternating, + ), + cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.int32), + } +) -@register_binary_sensor('raw', RawBinarySensor, RAW_SCHEMA) +@register_binary_sensor("raw", RawBinarySensor, RAW_SCHEMA) def raw_binary_sensor(var, config): code_ = config[CONF_CODE] arr = cg.progmem_array(config[CONF_CODE_STORAGE_ID], code_) @@ -390,64 +563,78 @@ def raw_binary_sensor(var, config): cg.add(var.set_len(len(code_))) -@register_trigger('raw', RawTrigger, cg.std_vector.template(cg.int32)) +@register_trigger("raw", RawTrigger, cg.std_vector.template(cg.int32)) def raw_trigger(var, config): pass -@register_dumper('raw', RawDumper) +@register_dumper("raw", RawDumper) def raw_dumper(var, config): pass -@register_action('raw', RawAction, RAW_SCHEMA.extend({ - cv.Optional(CONF_CARRIER_FREQUENCY, default='0Hz'): cv.All(cv.frequency, cv.int_), -})) -def raw_action(var, config, args): +@register_action( + "raw", + RawAction, + RAW_SCHEMA.extend( + { + cv.Optional(CONF_CARRIER_FREQUENCY, default="0Hz"): cv.All( + cv.frequency, cv.int_ + ), + } + ), +) +async def raw_action(var, config, args): code_ = config[CONF_CODE] if cg.is_template(code_): - template_ = yield cg.templatable(code_, args, cg.std_vector.template(cg.int32)) + template_ = await cg.templatable(code_, args, cg.std_vector.template(cg.int32)) cg.add(var.set_code_template(template_)) else: code_ = config[CONF_CODE] arr = cg.progmem_array(config[CONF_CODE_STORAGE_ID], code_) cg.add(var.set_code_static(arr, len(code_))) - templ = yield cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32) + templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32) cg.add(var.set_carrier_frequency(templ)) # RC5 -RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol('RC5') -RC5_SCHEMA = cv.Schema({ - cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), - cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x3F)), -}) +RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol("RC5") +RC5_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), + } +) -@register_binary_sensor('rc5', RC5BinarySensor, RC5_SCHEMA) +@register_binary_sensor("rc5", RC5BinarySensor, RC5_SCHEMA) def rc5_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - RC5Data, - ('address', config[CONF_ADDRESS]), - ('command', config[CONF_COMMAND]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + RC5Data, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) -@register_trigger('rc5', RC5Trigger, RC5Data) +@register_trigger("rc5", RC5Trigger, RC5Data) def rc5_trigger(var, config): pass -@register_dumper('rc5', RC5Dumper) +@register_dumper("rc5", RC5Dumper) def rc5_dumper(var, config): pass -@register_action('rc5', RC5Action, RC5_SCHEMA) -def rc5_action(var, config, args): - template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint8) +@register_action("rc5", RC5Action, RC5_SCHEMA) +async def rc5_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) cg.add(var.set_address(template_)) - template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint8) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) cg.add(var.set_command(template_)) @@ -456,13 +643,15 @@ RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2)) RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( cv.int_range(min=1, max=8), - cv.Schema({ - cv.Required(CONF_PULSE_LENGTH): cv.uint32_t, - cv.Optional(CONF_SYNC, default=[1, 31]): RC_SWITCH_TIMING_SCHEMA, - cv.Optional(CONF_ZERO, default=[1, 3]): RC_SWITCH_TIMING_SCHEMA, - cv.Optional(CONF_ONE, default=[3, 1]): RC_SWITCH_TIMING_SCHEMA, - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }) + cv.Schema( + { + cv.Required(CONF_PULSE_LENGTH): cv.uint32_t, + cv.Optional(CONF_SYNC, default=[1, 31]): RC_SWITCH_TIMING_SCHEMA, + cv.Optional(CONF_ZERO, default=[1, 3]): RC_SWITCH_TIMING_SCHEMA, + cv.Optional(CONF_ONE, default=[3, 1]): RC_SWITCH_TIMING_SCHEMA, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } + ), ) @@ -470,12 +659,14 @@ def validate_rc_switch_code(value): if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch codes must be in quotes ('')") 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)) + if c not in ("0", "1"): + raise cv.Invalid( + 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))) + raise cv.Invalid( + 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") return value @@ -485,13 +676,14 @@ def validate_rc_switch_raw_code(value): if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") for c in value: - if c not in ('0', '1', 'x'): + 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))) + raise cv.Invalid( + 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") return value @@ -501,222 +693,471 @@ def build_rc_switch_protocol(config): if isinstance(config, int): return rc_switch_protocols[config] pl = config[CONF_PULSE_LENGTH] - return RCSwitchBase(config[CONF_SYNC][0] * pl, config[CONF_SYNC][1] * pl, - config[CONF_ZERO][0] * pl, config[CONF_ZERO][1] * pl, - config[CONF_ONE][0] * pl, config[CONF_ONE][1] * pl, - config[CONF_INVERTED]) + return RCSwitchBase( + config[CONF_SYNC][0] * pl, + config[CONF_SYNC][1] * pl, + config[CONF_ZERO][0] * pl, + config[CONF_ZERO][1] * pl, + config[CONF_ONE][0] * pl, + config[CONF_ONE][1] * pl, + config[CONF_INVERTED], + ) -RC_SWITCH_RAW_SCHEMA = cv.Schema({ - cv.Required(CONF_CODE): validate_rc_switch_raw_code, - cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, -}) -RC_SWITCH_TYPE_A_SCHEMA = cv.Schema({ - cv.Required(CONF_GROUP): cv.All(validate_rc_switch_code, cv.Length(min=5, max=5)), - cv.Required(CONF_DEVICE): cv.All(validate_rc_switch_code, cv.Length(min=5, max=5)), - cv.Required(CONF_STATE): cv.boolean, - cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, -}) -RC_SWITCH_TYPE_B_SCHEMA = cv.Schema({ - cv.Required(CONF_ADDRESS): cv.int_range(min=1, max=4), - cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=4), - cv.Required(CONF_STATE): cv.boolean, - cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, -}) -RC_SWITCH_TYPE_C_SCHEMA = cv.Schema({ - cv.Required(CONF_FAMILY): cv.one_of('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', - 'l', 'm', 'n', 'o', 'p', lower=True), - cv.Required(CONF_GROUP): cv.int_range(min=1, max=4), - cv.Required(CONF_DEVICE): cv.int_range(min=1, max=4), - cv.Required(CONF_STATE): cv.boolean, - cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, -}) -RC_SWITCH_TYPE_D_SCHEMA = cv.Schema({ - cv.Required(CONF_GROUP): cv.one_of('a', 'b', 'c', 'd', lower=True), - cv.Required(CONF_DEVICE): cv.int_range(min=1, max=3), - cv.Required(CONF_STATE): cv.boolean, - cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, -}) -RC_SWITCH_TRANSMITTER = cv.Schema({ - cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema({ - cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='0us'): - cv.templatable(cv.positive_time_period_microseconds), - }), -}) +RC_SWITCH_RAW_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): validate_rc_switch_raw_code, + cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, + } +) +RC_SWITCH_TYPE_A_SCHEMA = cv.Schema( + { + cv.Required(CONF_GROUP): cv.All( + validate_rc_switch_code, cv.Length(min=5, max=5) + ), + cv.Required(CONF_DEVICE): cv.All( + validate_rc_switch_code, cv.Length(min=5, max=5) + ), + cv.Required(CONF_STATE): cv.boolean, + cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, + } +) +RC_SWITCH_TYPE_B_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.int_range(min=1, max=4), + cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=4), + cv.Required(CONF_STATE): cv.boolean, + cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, + } +) +RC_SWITCH_TYPE_C_SCHEMA = cv.Schema( + { + cv.Required(CONF_FAMILY): cv.one_of( + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + lower=True, + ), + cv.Required(CONF_GROUP): cv.int_range(min=1, max=4), + cv.Required(CONF_DEVICE): cv.int_range(min=1, max=4), + cv.Required(CONF_STATE): cv.boolean, + cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, + } +) +RC_SWITCH_TYPE_D_SCHEMA = cv.Schema( + { + cv.Required(CONF_GROUP): cv.one_of("a", "b", "c", "d", lower=True), + cv.Required(CONF_DEVICE): cv.int_range(min=1, max=3), + cv.Required(CONF_STATE): cv.boolean, + cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, + } +) +RC_SWITCH_TRANSMITTER = cv.Schema( + { + cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema( + { + cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), + cv.Optional(CONF_WAIT_TIME, default="0us"): cv.templatable( + cv.positive_time_period_microseconds + ), + } + ), + } +) -rc_switch_protocols = ns.rc_switch_protocols -RCSwitchData = ns.struct('RCSwitchData') -RCSwitchBase = ns.class_('RCSwitchBase') -RCSwitchTrigger = ns.class_('RCSwitchTrigger', RemoteReceiverTrigger) -RCSwitchDumper = ns.class_('RCSwitchDumper', RemoteTransmitterDumper) -RCSwitchRawAction = ns.class_('RCSwitchRawAction', RemoteTransmitterActionBase) -RCSwitchTypeAAction = ns.class_('RCSwitchTypeAAction', RemoteTransmitterActionBase) -RCSwitchTypeBAction = ns.class_('RCSwitchTypeBAction', RemoteTransmitterActionBase) -RCSwitchTypeCAction = ns.class_('RCSwitchTypeCAction', RemoteTransmitterActionBase) -RCSwitchTypeDAction = ns.class_('RCSwitchTypeDAction', RemoteTransmitterActionBase) -RCSwitchRawReceiver = ns.class_('RCSwitchRawReceiver', RemoteReceiverBinarySensorBase) +rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS +RCSwitchData = ns.struct("RCSwitchData") +RCSwitchBase = ns.class_("RCSwitchBase") +RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger) +RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteTransmitterDumper) +RCSwitchRawAction = ns.class_("RCSwitchRawAction", RemoteTransmitterActionBase) +RCSwitchTypeAAction = ns.class_("RCSwitchTypeAAction", RemoteTransmitterActionBase) +RCSwitchTypeBAction = ns.class_("RCSwitchTypeBAction", RemoteTransmitterActionBase) +RCSwitchTypeCAction = ns.class_("RCSwitchTypeCAction", RemoteTransmitterActionBase) +RCSwitchTypeDAction = ns.class_("RCSwitchTypeDAction", RemoteTransmitterActionBase) +RCSwitchRawReceiver = ns.class_("RCSwitchRawReceiver", RemoteReceiverBinarySensorBase) -@register_binary_sensor('rc_switch_raw', RCSwitchRawReceiver, RC_SWITCH_RAW_SCHEMA) +@register_binary_sensor("rc_switch_raw", RCSwitchRawReceiver, RC_SWITCH_RAW_SCHEMA) def rc_switch_raw_binary_sensor(var, config): cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL]))) cg.add(var.set_code(config[CONF_CODE])) -@register_action('rc_switch_raw', RCSwitchRawAction, - RC_SWITCH_RAW_SCHEMA.extend(RC_SWITCH_TRANSMITTER)) -def rc_switch_raw_action(var, config, args): - proto = yield cg.templatable(config[CONF_PROTOCOL], args, RCSwitchBase, - to_exp=build_rc_switch_protocol) +@register_action( + "rc_switch_raw", + RCSwitchRawAction, + RC_SWITCH_RAW_SCHEMA.extend(RC_SWITCH_TRANSMITTER), +) +async def rc_switch_raw_action(var, config, args): + proto = await cg.templatable( + config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol + ) cg.add(var.set_protocol(proto)) - cg.add(var.set_code((yield cg.templatable(config[CONF_CODE], args, cg.std_string)))) + cg.add(var.set_code((await cg.templatable(config[CONF_CODE], args, cg.std_string)))) -@register_binary_sensor('rc_switch_type_a', RCSwitchRawReceiver, RC_SWITCH_TYPE_A_SCHEMA) +@register_binary_sensor( + "rc_switch_type_a", RCSwitchRawReceiver, RC_SWITCH_TYPE_A_SCHEMA +) def rc_switch_type_a_binary_sensor(var, config): cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL]))) cg.add(var.set_type_a(config[CONF_GROUP], config[CONF_DEVICE], config[CONF_STATE])) -@register_action('rc_switch_type_a', RCSwitchTypeAAction, - RC_SWITCH_TYPE_A_SCHEMA.extend(RC_SWITCH_TRANSMITTER)) -def rc_switch_type_a_action(var, config, args): - proto = yield cg.templatable(config[CONF_PROTOCOL], args, RCSwitchBase, - to_exp=build_rc_switch_protocol) +@register_action( + "rc_switch_type_a", + RCSwitchTypeAAction, + RC_SWITCH_TYPE_A_SCHEMA.extend(RC_SWITCH_TRANSMITTER), +) +async def rc_switch_type_a_action(var, config, args): + proto = await cg.templatable( + config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol + ) cg.add(var.set_protocol(proto)) - cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.std_string)))) - cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.std_string)))) - cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool)))) + cg.add( + var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.std_string))) + ) + cg.add( + var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.std_string))) + ) + cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) -@register_binary_sensor('rc_switch_type_b', RCSwitchRawReceiver, RC_SWITCH_TYPE_B_SCHEMA) +@register_binary_sensor( + "rc_switch_type_b", RCSwitchRawReceiver, RC_SWITCH_TYPE_B_SCHEMA +) def rc_switch_type_b_binary_sensor(var, config): cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL]))) - cg.add(var.set_type_b(config[CONF_ADDRESS], config[CONF_CHANNEL], config[CONF_STATE])) + cg.add( + var.set_type_b(config[CONF_ADDRESS], config[CONF_CHANNEL], config[CONF_STATE]) + ) -@register_action('rc_switch_type_b', RCSwitchTypeBAction, - RC_SWITCH_TYPE_B_SCHEMA.extend(RC_SWITCH_TRANSMITTER)) -def rc_switch_type_b_action(var, config, args): - proto = yield cg.templatable(config[CONF_PROTOCOL], args, RCSwitchBase, - to_exp=build_rc_switch_protocol) +@register_action( + "rc_switch_type_b", + RCSwitchTypeBAction, + RC_SWITCH_TYPE_B_SCHEMA.extend(RC_SWITCH_TRANSMITTER), +) +async def rc_switch_type_b_action(var, config, args): + proto = await cg.templatable( + config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol + ) cg.add(var.set_protocol(proto)) - cg.add(var.set_address((yield cg.templatable(config[CONF_ADDRESS], args, cg.uint8)))) - cg.add(var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8)))) - cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool)))) + cg.add( + var.set_address((await cg.templatable(config[CONF_ADDRESS], args, cg.uint8))) + ) + cg.add( + var.set_channel((await cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + ) + cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) -@register_binary_sensor('rc_switch_type_c', RCSwitchRawReceiver, RC_SWITCH_TYPE_C_SCHEMA) +@register_binary_sensor( + "rc_switch_type_c", RCSwitchRawReceiver, RC_SWITCH_TYPE_C_SCHEMA +) def rc_switch_type_c_binary_sensor(var, config): cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL]))) - cg.add(var.set_type_c(config[CONF_FAMILY], config[CONF_GROUP], config[CONF_DEVICE], - config[CONF_STATE])) + cg.add( + var.set_type_c( + config[CONF_FAMILY], + config[CONF_GROUP], + config[CONF_DEVICE], + config[CONF_STATE], + ) + ) -@register_action('rc_switch_type_c', RCSwitchTypeCAction, - RC_SWITCH_TYPE_C_SCHEMA.extend(RC_SWITCH_TRANSMITTER)) -def rc_switch_type_c_action(var, config, args): - proto = yield cg.templatable(config[CONF_PROTOCOL], args, RCSwitchBase, - to_exp=build_rc_switch_protocol) +@register_action( + "rc_switch_type_c", + RCSwitchTypeCAction, + RC_SWITCH_TYPE_C_SCHEMA.extend(RC_SWITCH_TRANSMITTER), +) +async def rc_switch_type_c_action(var, config, args): + proto = await cg.templatable( + config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol + ) cg.add(var.set_protocol(proto)) - cg.add(var.set_family((yield cg.templatable(config[CONF_FAMILY], args, cg.std_string)))) - cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8)))) - cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint8)))) - cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool)))) + cg.add( + var.set_family((await cg.templatable(config[CONF_FAMILY], args, cg.std_string))) + ) + cg.add(var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.uint8)))) + cg.add(var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.uint8)))) + cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) -@register_binary_sensor('rc_switch_type_d', RCSwitchRawReceiver, - RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER)) +@register_binary_sensor( + "rc_switch_type_d", + RCSwitchRawReceiver, + RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER), +) def rc_switch_type_d_binary_sensor(var, config): cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL]))) cg.add(var.set_type_d(config[CONF_GROUP], config[CONF_DEVICE], config[CONF_STATE])) -@register_action('rc_switch_type_d', RCSwitchTypeDAction, - RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER)) -def rc_switch_type_d_action(var, config, args): - proto = yield cg.templatable(config[CONF_PROTOCOL], args, RCSwitchBase, - to_exp=build_rc_switch_protocol) +@register_action( + "rc_switch_type_d", + RCSwitchTypeDAction, + RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER), +) +async def rc_switch_type_d_action(var, config, args): + proto = await cg.templatable( + config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol + ) cg.add(var.set_protocol(proto)) - cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.std_string)))) - cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint8)))) - cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool)))) + cg.add( + var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.std_string))) + ) + cg.add(var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.uint8)))) + cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) -@register_trigger('rc_switch', RCSwitchTrigger, RCSwitchData) +@register_trigger("rc_switch", RCSwitchTrigger, RCSwitchData) def rc_switch_trigger(var, config): pass -@register_dumper('rc_switch', RCSwitchDumper) +@register_dumper("rc_switch", RCSwitchDumper) def rc_switch_dumper(var, config): pass # Samsung -(SamsungData, SamsungBinarySensor, SamsungTrigger, SamsungAction, - SamsungDumper) = declare_protocol('Samsung') -SAMSUNG_SCHEMA = cv.Schema({ - cv.Required(CONF_DATA): cv.hex_uint32_t, -}) +( + SamsungData, + SamsungBinarySensor, + SamsungTrigger, + SamsungAction, + SamsungDumper, +) = declare_protocol("Samsung") +SAMSUNG_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.hex_uint64_t, + cv.Optional(CONF_NBITS, default=32): cv.int_range(32, 64), + } +) -@register_binary_sensor('samsung', SamsungBinarySensor, SAMSUNG_SCHEMA) +@register_binary_sensor("samsung", SamsungBinarySensor, SAMSUNG_SCHEMA) def samsung_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - SamsungData, - ('data', config[CONF_DATA]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + SamsungData, + ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), + ) + ) + ) -@register_trigger('samsung', SamsungTrigger, SamsungData) +@register_trigger("samsung", SamsungTrigger, SamsungData) def samsung_trigger(var, config): pass -@register_dumper('samsung', SamsungDumper) +@register_dumper("samsung", SamsungDumper) def samsung_dumper(var, config): pass -@register_action('samsung', SamsungAction, SAMSUNG_SCHEMA) -def samsung_action(var, config, args): - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) +@register_action("samsung", SamsungAction, SAMSUNG_SCHEMA) +async def samsung_action(var, config, args): + 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 +( + Samsung36Data, + Samsung36BinarySensor, + Samsung36Trigger, + Samsung36Action, + Samsung36Dumper, +) = declare_protocol("Samsung36") +SAMSUNG36_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + cv.Required(CONF_COMMAND): cv.hex_uint32_t, + } +) + + +@register_binary_sensor("samsung36", Samsung36BinarySensor, SAMSUNG36_SCHEMA) +def samsung36_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + Samsung36Data, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("samsung36", Samsung36Trigger, Samsung36Data) +def samsung36_trigger(var, config): + pass + + +@register_dumper("samsung36", Samsung36Dumper) +def samsung36_dumper(var, config): + pass + + +@register_action("samsung36", Samsung36Action, SAMSUNG36_SCHEMA) +async def samsung36_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32) + 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, PanasonicBinarySensor, PanasonicTrigger, PanasonicAction, - PanasonicDumper) = declare_protocol('Panasonic') -PANASONIC_SCHEMA = cv.Schema({ - cv.Required(CONF_ADDRESS): cv.hex_uint16_t, - cv.Required(CONF_COMMAND): cv.hex_uint32_t, -}) +( + PanasonicData, + PanasonicBinarySensor, + PanasonicTrigger, + PanasonicAction, + PanasonicDumper, +) = declare_protocol("Panasonic") +PANASONIC_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + cv.Required(CONF_COMMAND): cv.hex_uint32_t, + } +) -@register_binary_sensor('panasonic', PanasonicBinarySensor, PANASONIC_SCHEMA) +@register_binary_sensor("panasonic", PanasonicBinarySensor, PANASONIC_SCHEMA) def panasonic_binary_sensor(var, config): - cg.add(var.set_data(cg.StructInitializer( - PanasonicData, - ('address', config[CONF_ADDRESS]), - ('command', config[CONF_COMMAND]), - ))) + cg.add( + var.set_data( + cg.StructInitializer( + PanasonicData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) -@register_trigger('panasonic', PanasonicTrigger, PanasonicData) +@register_trigger("panasonic", PanasonicTrigger, PanasonicData) def panasonic_trigger(var, config): pass -@register_dumper('panasonic', PanasonicDumper) +@register_dumper("panasonic", PanasonicDumper) def panasonic_dumper(var, config): pass -@register_action('panasonic', PanasonicAction, PANASONIC_SCHEMA) -def panasonic_action(var, config, args): - template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint16) +@register_action("panasonic", PanasonicAction, PANASONIC_SCHEMA) +async def panasonic_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) cg.add(var.set_address(template_)) - template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint32) + 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/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index 94d15c2304..f43a28bdc5 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.jvc"; +static const char *const TAG = "remote.jvc"; static const uint8_t NBITS = 16; static const uint32_t HEADER_HIGH_US = 8400; diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index 8a216f5348..fc40a6a874 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -23,6 +23,7 @@ DECLARE_REMOTE_PROTOCOL(JVC) template class JVCAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { JVCData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index c01348ef94..a3e7f9828b 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.lg"; +static const char *const TAG = "remote.lg"; static const uint32_t HEADER_HIGH_US = 8000; static const uint32_t HEADER_LOW_US = 4000; diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index b810115f58..6267560443 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -26,6 +26,7 @@ template class LGAction : public RemoteTransmitterActionBasedata_.value(x...); 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..61e511601b --- /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 format_hex_pretty(this->data_, sizeof(this->data_)); } + // 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/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index e352d051c0..47b4d676dd 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.nec"; +static const char *const TAG = "remote.nec"; static const uint32_t HEADER_HIGH_US = 9000; static const uint32_t HEADER_LOW_US = 4500; @@ -17,14 +17,14 @@ void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { dst->set_carrier_frequency(38000); dst->item(HEADER_HIGH_US, HEADER_LOW_US); - for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + for (uint16_t mask = 1; mask; mask <<= 1) { if (data.address & mask) dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); else dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } - for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + for (uint16_t mask = 1; mask; mask <<= 1) { if (data.command & mask) dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); else @@ -41,7 +41,7 @@ optional NECProtocol::decode(RemoteReceiveData src) { if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; - for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + for (uint16_t mask = 1; mask; 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)) { @@ -51,7 +51,7 @@ optional NECProtocol::decode(RemoteReceiveData src) { } } - for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + for (uint16_t mask = 1; mask; 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)) { diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index c794991eab..593a3efe17 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -25,6 +25,7 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index 4a19f8c1fc..fd4f7c4bf7 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.panasonic"; +static const char *const TAG = "remote.panasonic"; static const uint32_t HEADER_HIGH_US = 3502; static const uint32_t HEADER_LOW_US = 1750; diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index b13bd3e92d..eae97a8a14 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -26,6 +26,7 @@ template class PanasonicAction : public RemoteTransmitterActionB public: TEMPLATABLE_VALUE(uint16_t, address) TEMPLATABLE_VALUE(uint32_t, command) + void encode(RemoteTransmitData *dst, Ts... x) override { PanasonicData data{}; data.address = this->address_.value(x...); diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index 49a27e08e7..74a3998f11 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.pioneer"; +static const char *const TAG = "remote.pioneer"; static const uint32_t HEADER_HIGH_US = 9000; static const uint32_t HEADER_LOW_US = 4500; diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index f93e51a033..4cac4f9f32 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -25,6 +25,7 @@ template class PioneerAction : public RemoteTransmitterActionBas public: TEMPLATABLE_VALUE(uint16_t, rc_code_1) TEMPLATABLE_VALUE(uint16_t, rc_code_2) + void encode(RemoteTransmitData *dst, Ts... x) override { PioneerData data{}; data.rc_code_1 = this->rc_code_1_.value(x...); diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp new file mode 100644 index 0000000000..4f6ace720c --- /dev/null +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -0,0 +1,135 @@ +/* + * @file irPronto.cpp + * @brief In this file, the functions IRrecv::compensateAndPrintPronto and IRsend::sendPronto are defined. + * + * See http://www.harctoolbox.org/Glossary.html#ProntoSemantics + * Pronto database http://www.remotecentral.com/search.htm + * + * This file is part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote. + * + ************************************************************************************ + * MIT License + * + * Copyright (c) 2020 Bengt Martensson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + ************************************************************************************ + */ + +#include "pronto_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.pronto"; + +// DO NOT EXPORT from this file +static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; +static const uint16_t LEARNED_TOKEN = 0x0000U; +static const uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; +static const uint16_t BITS_IN_HEXADECIMAL = 4U; +static const uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; +static const uint16_t NUMBERS_IN_PREAMBLE = 4U; +static const uint16_t HEX_MASK = 0xFU; +static const uint32_t REFERENCE_FREQUENCY = 4145146UL; +static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; +static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; +static const uint16_t PRONTO_DEFAULT_GAP = 45000; + +static uint16_t to_frequency_k_hz(uint16_t code) { + if (code == 0) + return 0; + + return ((REFERENCE_FREQUENCY / code) + 500) / 1000; +} + +/* + * Parse the string given as Pronto Hex, and send it a number of times given as argument. + */ +void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vector &data) { + if (data.size() < 4) + return; + + uint16_t timebase = (MICROSECONDS_IN_SECONDS * data[1] + REFERENCE_FREQUENCY / 2) / REFERENCE_FREQUENCY; + uint16_t khz; + switch (data[0]) { + case LEARNED_TOKEN: // normal, "learned" + khz = to_frequency_k_hz(data[1]); + break; + case LEARNED_NON_MODULATED_TOKEN: // non-demodulated, "learned" + khz = 0U; + break; + default: + return; // There are other types, but they are not handled yet. + } + ESP_LOGD(TAG, "Send Pronto: frequency=%dkHz", khz); + dst->set_carrier_frequency(khz * 1000); + + uint16_t intros = 2 * data[2]; + uint16_t repeats = 2 * data[3]; + ESP_LOGD(TAG, "Send Pronto: intros=%d", intros); + ESP_LOGD(TAG, "Send Pronto: repeats=%d", repeats); + if (NUMBERS_IN_PREAMBLE + intros + repeats != data.size()) { // inconsistent sizes + return; + } + + /* + * Generate a new microseconds timing array for sendRaw. + * If recorded by IRremote, intro contains the whole IR data and repeat is empty + */ + dst->reserve(intros + repeats); + + for (uint16_t i = 0; i < intros + repeats; i += 2) { + uint32_t duration0 = ((uint32_t) data[i + 0 + NUMBERS_IN_PREAMBLE]) * timebase; + duration0 = duration0 < MICROSECONDS_T_MAX ? duration0 : MICROSECONDS_T_MAX; + + uint32_t duration1 = ((uint32_t) data[i + 1 + NUMBERS_IN_PREAMBLE]) * timebase; + duration1 = duration1 < MICROSECONDS_T_MAX ? duration1 : MICROSECONDS_T_MAX; + + dst->item(duration0, duration1); + } +} + +void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) { + size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1; + std::vector data; + const char *p = str.c_str(); + char *endptr[1]; + + for (size_t i = 0; i < len; i++) { + uint16_t x = strtol(p, endptr, 16); + if (x == 0 && i >= NUMBERS_IN_PREAMBLE) { + // Alignment error?, bail immediately (often right result). + break; + } + data.push_back(x); // If input is conforming, there can be no overflow! + p = *endptr; + } + send_pronto_(dst, data); +} + +void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); } + +optional ProntoProtocol::decode(RemoteReceiveData src) { return {}; } + +void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h new file mode 100644 index 0000000000..e96511383f --- /dev/null +++ b/esphome/components/remote_base/pronto_protocol.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct ProntoData { + std::string data; + + bool operator==(const ProntoData &rhs) const { return data == rhs.data; } +}; + +class ProntoProtocol : public RemoteProtocol { + private: + void send_pronto_(RemoteTransmitData *dst, const std::vector &data); + void send_pronto_(RemoteTransmitData *dst, const std::string &str); + + public: + void encode(RemoteTransmitData *dst, const ProntoData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const ProntoData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Pronto) + +template class ProntoAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(std::string, data) + + void encode(RemoteTransmitData *dst, Ts... x) override { + ProntoData data{}; + data.data = this->data_.value(x...); + ProntoProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/raw_protocol.cpp b/esphome/components/remote_base/raw_protocol.cpp index 62d578caf9..3446dcdbb8 100644 --- a/esphome/components/remote_base/raw_protocol.cpp +++ b/esphome/components/remote_base/raw_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.raw"; +static const char *const TAG = "remote.raw"; bool RawDumper::dump(RemoteReceiveData src) { char buffer[256]; diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 35bff588e9..47a85cda57 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -4,20 +4,26 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.rc5"; +static const char *const TAG = "remote.rc5"; 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; - out_data |= 0b11 << 12; - out_data |= TOGGLE << 11; + uint8_t command = data.command; + if (data.command >= 64) { + out_data |= 0b10 << 12; + command = command - 64; + } else { + out_data |= 0b11 << 12; + } + out_data |= toggle << 11; out_data |= data.address << 6; - out_data |= data.command; + out_data |= command; for (uint64_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { if (out_data & mask) { @@ -28,29 +34,51 @@ 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, }; - src.expect_space(BIT_TIME_US); - if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US) || !src.expect_mark(BIT_TIME_US)) - return {}; + uint8_t field_bit; - uint64_t out_data = 0; - for (int bit = NBITS - 3; bit >= 0; bit--) { - if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { - out_data |= 1 << bit; - } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { + field_bit = 1; + } else if (src.expect_space(2 * BIT_TIME_US)) { + field_bit = 0; + } else { + return {}; + } + + if (!(((src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US)) || + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) && + (((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) || + ((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)))))) { + return {}; + } + + 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))) { out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; } else { return {}; } } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } - out.command = out_data & 0x3F; + 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/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index 2e1da74d9f..589c8d42de 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -26,6 +26,7 @@ template class RC5Action : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 91b22500e6..1dc094d552 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -4,17 +4,17 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.rc_switch"; +static const char *const TAG = "remote.rc_switch"; -RCSwitchBase rc_switch_protocols[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), - RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false), - RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false), - RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false), - RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false), - RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false), - RCSwitchBase(10350, 450, 450, 900, 900, 450, true), - RCSwitchBase(300, 9300, 150, 900, 900, 150, false), - RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)}; +const RCSwitchBase RC_SWITCH_PROTOCOLS[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), + RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false), + RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false), + RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false), + RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false), + RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false), + RCSwitchBase(10350, 450, 450, 900, 900, 450, true), + RCSwitchBase(300, 9300, 150, 900, 900, 150, false), + RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)}; RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, uint32_t one_low, bool inverted) @@ -55,13 +55,13 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { } void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { dst->set_carrier_frequency(0); + this->sync(dst); for (int16_t i = len - 1; i >= 0; i--) { if (code & ((uint64_t) 1 << i)) this->one(dst); else this->zero(dst); } - this->sync(dst); } bool RCSwitchBase::expect_one(RemoteReceiveData &src) const { @@ -101,10 +101,13 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const { if (!src.peek_space(this->sync_low_, 1)) return false; } else { - if (!src.peek_space(this->sync_high_)) - return false; - if (!src.peek_mark(this->sync_low_, 1)) + // We cant peek a space at the beginning because signals starts with a low to high transition. + // this long space at the beginning is the separation between the transmissions itself, so it is actually + // added at the end kind of artificially (by the value given to "idle:" option by the user in the yaml) + if (!src.peek_mark(this->sync_low_)) return false; + src.advance(1); + return true; } src.advance(2); return true; @@ -132,7 +135,7 @@ optional RCSwitchBase::decode(RemoteReceiveData &src) const { uint8_t out_nbits; for (uint8_t i = 1; i <= 8; i++) { src.reset(); - RCSwitchBase *protocol = &rc_switch_protocols[i]; + const RCSwitchBase *protocol = &RC_SWITCH_PROTOCOLS[i]; if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) { out.protocol = i; return out; @@ -246,7 +249,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { src.reset(); uint64_t out_data; uint8_t out_nbits; - RCSwitchBase *protocol = &rc_switch_protocols[i]; + const RCSwitchBase *protocol = &RC_SWITCH_PROTOCOLS[i]; if (protocol->decode(src, &out_data, &out_nbits) && out_nbits >= 3) { char buffer[65]; for (uint8_t j = 0; j < out_nbits; j++) diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 8362899cec..fc465dbd5d 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -60,7 +60,7 @@ class RCSwitchBase { bool inverted_{}; }; -extern RCSwitchBase rc_switch_protocols[9]; +extern const RCSwitchBase RC_SWITCH_PROTOCOLS[9]; uint64_t decode_binary_string(const std::string &data); diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 04407ad3b1..97ee027b84 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -4,9 +4,9 @@ namespace esphome { namespace remote_base { -static const char *TAG = "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_; @@ -33,7 +33,7 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { uint32_t buffer_offset = 0; buffer_offset += sprintf(buffer, "Sending times=%u wait=%ums: ", send_times, send_wait); - for (int32_t i = 0; i < vec.size(); i++) { + for (size_t i = 0; i < vec.size(); i++) { const int32_t value = vec[i]; const uint32_t remaining_length = sizeof(buffer) - buffer_offset; int written; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 250b59e55e..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 @@ -33,7 +33,7 @@ class RemoteTransmitData { const std::vector &get_data() const { return this->data_; } - void set_data(std::vector data) { + void set_data(const std::vector &data) { this->data_.clear(); this->data_.reserve(data.size()); for (auto dat : data) @@ -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()) { @@ -323,6 +323,9 @@ template class RemoteTransmitterActionBase : public Actionparent_ = parent; } + TEMPLATABLE_VALUE(uint32_t, send_times); + TEMPLATABLE_VALUE(uint32_t, send_wait); + void play(Ts... x) override { auto call = this->parent_->transmit(); this->encode(call.get_data(), x...); @@ -331,12 +334,9 @@ template class RemoteTransmitterActionBase : public Actionset_carrier_frequency(38000); + dst->reserve(NBITS); + + // send header + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + + // send first 16 bits + for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + if (data.address & mask) { + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + } + + // send middle header + dst->item(MIDDLE_HIGH_US, MIDDLE_LOW_US); + + // send last 20 bits + for (uint32_t mask = 1UL << 19; mask != 0; 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); + } + } + + // footer + dst->item(FOOTER_HIGH_US, FOOTER_LOW_US); +} + +optional Samsung36Protocol::decode(RemoteReceiveData src) { + Samsung36Data out{ + .address = 0, + .command = 0, + }; + + // check if header matches + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + // check if we have enough bits + if (src.size() != NBITS) + return {}; + + // get the first 16 bits + for (uint8_t i = 0; i < 16; i++) { + out.address <<= 1UL; + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.address |= 1UL; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + out.address |= 0UL; + } else { + return {}; + } + } + + // check if the middle mark matches + if (!src.expect_item(MIDDLE_HIGH_US, MIDDLE_LOW_US)) { + return {}; + } + + // get the last 20 bits + for (uint8_t i = 0; i < 20; i++) { + out.command <<= 1UL; + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.command |= 1UL; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + out.command |= 0UL; + } else { + return {}; + } + } + + return out; +} +void Samsung36Protocol::dump(const Samsung36Data &data) { + ESP_LOGD(TAG, "Received Samsung36: address=0x%04X, command=0x%08X", data.address, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/samsung36_protocol.h b/esphome/components/remote_base/samsung36_protocol.h new file mode 100644 index 0000000000..4ba6226edd --- /dev/null +++ b/esphome/components/remote_base/samsung36_protocol.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct Samsung36Data { + uint16_t address; + uint32_t command; + + bool operator==(const Samsung36Data &rhs) const { return address == rhs.address && command == rhs.command; } +}; + +class Samsung36Protocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const Samsung36Data &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const Samsung36Data &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Samsung36) + +template class Samsung36Action : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, address) + TEMPLATABLE_VALUE(uint32_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + Samsung36Data data{}; + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + Samsung36Protocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 25f68ceb97..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 *TAG = "remote.samsung"; +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 50ff02c1aa..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,10 +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/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 97318f6608..3bb643266a 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace remote_base { -static const char *TAG = "remote.sony"; +static const char *const TAG = "remote.sony"; static const uint32_t HEADER_HIGH_US = 2400; static const uint32_t HEADER_LOW_US = 600; diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index 9f0bcdf82f..aecc8ab91c 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -26,6 +26,7 @@ template class SonyAction : public RemoteTransmitterActionBasedata_.value(x...); 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 28570a7c62..253204bd1a 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -2,40 +2,62 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import remote_base -from esphome.const import CONF_BUFFER_SIZE, CONF_DUMP, CONF_FILTER, CONF_ID, CONF_IDLE, \ - CONF_PIN, CONF_TOLERANCE, CONF_MEMORY_BLOCKS +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_DUMP, + CONF_FILTER, + CONF_ID, + CONF_IDLE, + CONF_PIN, + CONF_TOLERANCE, + CONF_MEMORY_BLOCKS, +) from esphome.core import CORE -AUTO_LOAD = ['remote_base'] -remote_receiver_ns = cg.esphome_ns.namespace('remote_receiver') -RemoteReceiverComponent = remote_receiver_ns.class_('RemoteReceiverComponent', - remote_base.RemoteReceiverBase, - cg.Component) +AUTO_LOAD = ["remote_base"] +remote_receiver_ns = cg.esphome_ns.namespace("remote_receiver") +RemoteReceiverComponent = remote_receiver_ns.class_( + "RemoteReceiverComponent", remote_base.RemoteReceiverBase, cg.Component +) MULTI_CONF = True -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.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers, - cv.Optional(CONF_TOLERANCE, default=25): cv.All(cv.percentage_int, cv.Range(min=0)), - cv.SplitDefault(CONF_BUFFER_SIZE, esp32='10000b', esp8266='1000b'): cv.validate_bytes, - cv.Optional(CONF_FILTER, default='50us'): cv.positive_time_period_microseconds, - cv.Optional(CONF_IDLE, default='10ms'): cv.positive_time_period_microseconds, - cv.Optional(CONF_MEMORY_BLOCKS, default=3): cv.Range(min=1, max=8), -}).extend(cv.COMPONENT_SCHEMA)) +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), + cv.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers, + cv.Optional(CONF_TOLERANCE, default=25): cv.All( + cv.percentage_int, cv.Range(min=0) + ), + cv.SplitDefault( + CONF_BUFFER_SIZE, esp32="10000b", esp8266="1000b" + ): cv.validate_bytes, + cv.Optional( + CONF_FILTER, default="50us" + ): cv.positive_time_period_microseconds, + cv.Optional( + CONF_IDLE, default="10ms" + ): cv.positive_time_period_microseconds, + cv.Optional(CONF_MEMORY_BLOCKS, default=3): cv.Range(min=1, max=8), + } + ).extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) +async def to_code(config): + pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) else: var = cg.new_Pvariable(config[CONF_ID], pin) - yield remote_base.build_dumpers(config[CONF_DUMP]) - yield remote_base.build_triggers(config) - yield cg.register_component(var, config) + 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) cg.add(var.set_tolerance(config[CONF_TOLERANCE])) cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) diff --git a/esphome/components/remote_receiver/binary_sensor.py b/esphome/components/remote_receiver/binary_sensor.py index 7be64e5a54..218b40d6cc 100644 --- a/esphome/components/remote_receiver/binary_sensor.py +++ b/esphome/components/remote_receiver/binary_sensor.py @@ -1,13 +1,10 @@ -import esphome.codegen as cg from esphome.components import binary_sensor, remote_base -from esphome.const import CONF_NAME -DEPENDENCIES = ['remote_receiver'] +DEPENDENCIES = ["remote_receiver"] CONFIG_SCHEMA = remote_base.validate_binary_sensor -def to_code(config): - var = yield remote_base.build_binary_sensor(config) - cg.add(var.set_name(config[CONF_NAME])) - yield binary_sensor.register_binary_sensor(var, config) +async def to_code(config): + var = await remote_base.build_binary_sensor(config) + 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 89de4da92a..5a7fb3c985 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -1,16 +1,17 @@ #include "remote_receiver.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include namespace esphome { namespace remote_receiver { -static const char *TAG = "remote_receiver.esp32"; +static const char *const TAG = "remote_receiver.esp32"; void RemoteReceiverComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Remote Receiver..."); + this->pin_->setup(); rmt_config_t rmt{}; this->config_rmt(rmt); rmt.gpio_num = gpio_num_t(this->pin_->get_pin()); @@ -19,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) { @@ -77,6 +78,7 @@ void RemoteReceiverComponent::loop() { if (this->temp_.empty()) return; + this->temp_.push_back(-this->idle_us_); this->call_listeners_dumpers_(); } } @@ -85,24 +87,25 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { uint32_t prev_length = 0; this->temp_.clear(); int32_t multiplier = this->pin_->is_inverted() ? -1 : 1; + size_t item_count = len / sizeof(rmt_item32_t); ESP_LOGVV(TAG, "START:"); - for (size_t i = 0; i < len; i++) { + for (size_t i = 0; i < item_count; i++) { if (item[i].level0) { - ESP_LOGVV(TAG, "%u A: ON %uus (%u ticks)", i, this->to_microseconds(item[i].duration0), item[i].duration0); + ESP_LOGVV(TAG, "%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"); - this->temp_.reserve(len / 4); - for (size_t i = 0; i < len; i++) { + this->temp_.reserve(item_count * 2); // each RMT item has 2 pulses + for (size_t i = 0; i < item_count; i++) { if (item[i].duration0 == 0u) { // Do nothing } else if (bool(item[i].level0) == prev_level) { @@ -110,19 +113,15 @@ 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_) { - break; - } - if (item[i].duration1 == 0u) { // Do nothing } else if (bool(item[i].level1) == prev_level) { @@ -130,24 +129,20 @@ 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_) { - 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 cafbc34d69..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 *TAG = "remote_receiver.esp8266"; +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/__init__.py b/esphome/components/remote_transmitter/__init__.py index 5e217de608..e09e4c7f55 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -4,23 +4,27 @@ from esphome import pins from esphome.components import remote_base from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN -AUTO_LOAD = ['remote_base'] -remote_transmitter_ns = cg.esphome_ns.namespace('remote_transmitter') -RemoteTransmitterComponent = remote_transmitter_ns.class_('RemoteTransmitterComponent', - remote_base.RemoteTransmitterBase, - cg.Component) +AUTO_LOAD = ["remote_base"] +remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter") +RemoteTransmitterComponent = remote_transmitter_ns.class_( + "RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component +) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All(cv.percentage_int, cv.Range(min=1, max=100)), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) + ), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) +async def to_code(config): + pin = await cg.gpio_pin_expression(config[CONF_PIN]) var = cg.new_Pvariable(config[CONF_ID], pin) - yield cg.register_component(var, config) + await cg.register_component(var, config) cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT])) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 877b190a1d..425418ff39 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace remote_transmitter { -static const char *TAG = "remote_transmitter"; +static const char *const TAG = "remote_transmitter"; } // namespace remote_transmitter } // namespace esphome diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 000fbabfee..a4235e875f 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,21 +26,25 @@ 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); void space_(uint32_t usec); + + void await_target_time_(); + uint32_t target_time_; #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 d5e4a7b1b8..368b21f892 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 *TAG = "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); } @@ -112,7 +113,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->rmt_temp_.push_back(rmt_item); } - for (uint16_t i = 0; i < send_times; i++) { + for (uint32_t i = 0; i < send_times; i++) { esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); if (error != ESP_OK) { ESP_LOGW(TAG, "rmt_write_items failed: %s", esp_err_to_name(error)); @@ -120,10 +121,8 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } else { this->status_clear_warning(); } - if (i + 1 < send_times) { - delay(send_wait / 1000UL); - delayMicroseconds(send_wait % 1000UL); - } + if (i + 1 < send_times) + delayMicroseconds(send_wait); } } diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index e8906e87aa..39752cac5b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -2,12 +2,12 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 namespace esphome { namespace remote_transmitter { -static const char *TAG = "remote_transmitter"; +static const char *const TAG = "remote_transmitter"; void RemoteTransmitterComponent::setup() { this->pin_->setup(); @@ -33,57 +33,64 @@ void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequen *off_time_period = period - *on_time_period; } +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) + this->target_time_ = current_time; + else if (this->target_time_ > current_time) + delayMicroseconds(this->target_time_ - current_time); +} + void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - if (this->carrier_duty_percent_ == 100 || (on_time == 0 && off_time == 0)) { - this->pin_->digital_write(true); - delay_microseconds_accurate(usec); - this->pin_->digital_write(false); - return; - } - - const uint32_t start_time = micros(); - uint32_t current_time = start_time; - - while (current_time - start_time < usec) { - const uint32_t elapsed = current_time - start_time; - this->pin_->digital_write(true); - - delay_microseconds_accurate(std::min(on_time, usec - elapsed)); - this->pin_->digital_write(false); - if (elapsed + on_time >= usec) - return; - - delay_microseconds_accurate(std::min(usec - elapsed - on_time, off_time)); - - current_time = micros(); + this->await_target_time_(); + this->pin_->digital_write(true); + + const uint32_t target = this->target_time_ + usec; + if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { + while (true) { // Modulate with carrier frequency + this->target_time_ += on_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(false); + + this->target_time_ += off_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(true); + } } + this->target_time_ = target; } + void RemoteTransmitterComponent::space_(uint32_t usec) { + this->await_target_time_(); this->pin_->digital_write(false); - delay_microseconds_accurate(usec); + this->target_time_ += usec; } + void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { ESP_LOGD(TAG, "Sending remote code..."); uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); + this->target_time_ = 0; for (uint32_t i = 0; i < send_times; i++) { - { - InterruptLock lock; - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); } + App.feed_wdt(); } + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); - if (i + 1 < send_times) { - delay_microseconds_accurate(send_wait); - } + if (i + 1 < send_times) + this->target_time_ += send_wait; } } diff --git a/esphome/components/remote_transmitter/switch.py b/esphome/components/remote_transmitter/switch.py deleted file mode 100644 index 5e0be04d7a..0000000000 --- a/esphome/components/remote_transmitter/switch.py +++ /dev/null @@ -1,30 +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 7c48bbbc08..4d3dfa5928 100644 --- a/esphome/components/resistance/resistance_sensor.cpp +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace resistance { -static const char *TAG = "resistance"; +static const char *const TAG = "resistance"; void ResistanceSensor::dump_config() { LOG_SENSOR("", "Resistance Sensor", this); @@ -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 fb245bcdf0..329192e902 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -1,36 +1,53 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_SENSOR, UNIT_OHM, ICON_FLASH, CONF_ID +from esphome.const import ( + CONF_SENSOR, + STATE_CLASS_MEASUREMENT, + UNIT_OHM, + ICON_FLASH, + CONF_ID, +) -resistance_ns = cg.esphome_ns.namespace('resistance') -ResistanceSensor = resistance_ns.class_('ResistanceSensor', cg.Component, sensor.Sensor) +resistance_ns = cg.esphome_ns.namespace("resistance") +ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor) -CONF_REFERENCE_VOLTAGE = 'reference_voltage' -CONF_CONFIGURATION = 'configuration' -CONF_RESISTOR = 'resistor' +CONF_REFERENCE_VOLTAGE = "reference_voltage" +CONF_CONFIGURATION = "configuration" +CONF_RESISTOR = "resistor" -ResistanceConfiguration = resistance_ns.enum('ResistanceConfiguration') +ResistanceConfiguration = resistance_ns.enum("ResistanceConfiguration") CONFIGURATIONS = { - 'DOWNSTREAM': ResistanceConfiguration.DOWNSTREAM, - 'UPSTREAM': ResistanceConfiguration.UPSTREAM, + "DOWNSTREAM": ResistanceConfiguration.DOWNSTREAM, + "UPSTREAM": ResistanceConfiguration.UPSTREAM, } -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_OHM, ICON_FLASH, 1).extend({ - cv.GenerateID(): cv.declare_id(ResistanceSensor), - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_CONFIGURATION): cv.enum(CONFIGURATIONS, upper=True), - cv.Required(CONF_RESISTOR): cv.resistance, - cv.Optional(CONF_REFERENCE_VOLTAGE, default='3.3V'): cv.voltage, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(ResistanceSensor), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_CONFIGURATION): cv.enum(CONFIGURATIONS, upper=True), + cv.Required(CONF_RESISTOR): cv.resistance, + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - sens = yield cg.get_variable(config[CONF_SENSOR]) + sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) cg.add(var.set_configuration(config[CONF_CONFIGURATION])) cg.add(var.set_resistor(config[CONF_RESISTOR])) diff --git a/esphome/components/restart/__init__.py b/esphome/components/restart/__init__.py index e69de29bb2..f70ffa9520 100644 --- a/esphome/components/restart/__init__.py +++ b/esphome/components/restart/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/restart/button/__init__.py b/esphome/components/restart/button/__init__.py new file mode 100644 index 0000000000..1a0e9cdc3d --- /dev/null +++ b/esphome/components/restart/button/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, +) + +restart_ns = cg.esphome_ns.namespace("restart") +RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component) + +CONFIG_SCHEMA = ( + button.button_schema( + device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG + ) + .extend({cv.GenerateID(): cv.declare_id(RestartButton)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) diff --git a/esphome/components/restart/button/restart_button.cpp b/esphome/components/restart/button/restart_button.cpp new file mode 100644 index 0000000000..d8ff061355 --- /dev/null +++ b/esphome/components/restart/button/restart_button.cpp @@ -0,0 +1,20 @@ +#include "restart_button.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace restart { + +static const char *const TAG = "restart.button"; + +void RestartButton::press_action() { + ESP_LOGI(TAG, "Restarting device..."); + // Let MQTT settle a bit + delay(100); // NOLINT + App.safe_reboot(); +} +void RestartButton::dump_config() { LOG_BUTTON("", "Restart Button", this); } + +} // namespace restart +} // namespace esphome diff --git a/esphome/components/restart/button/restart_button.h b/esphome/components/restart/button/restart_button.h new file mode 100644 index 0000000000..db18f1dadc --- /dev/null +++ b/esphome/components/restart/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace restart { + +class RestartButton : public button::Button, public Component { + public: + void dump_config() override; + + protected: + void press_action() override; +}; + +} // namespace restart +} // namespace esphome diff --git a/esphome/components/restart/switch.py b/esphome/components/restart/switch.py deleted file mode 100644 index 9517302d33..0000000000 --- a/esphome/components/restart/switch.py +++ /dev/null @@ -1,20 +0,0 @@ -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_RESTART - -restart_ns = cg.esphome_ns.namespace('restart') -RestartSwitch = restart_ns.class_('RestartSwitch', switch.Switch, cg.Component) - -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(RestartSwitch), - cv.Optional(CONF_INVERTED): cv.invalid("Restart switches do not support inverted mode!"), - - cv.Optional(CONF_ICON, default=ICON_RESTART): switch.icon, -}).extend(cv.COMPONENT_SCHEMA) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield switch.register_switch(var, config) diff --git a/esphome/components/restart/switch/__init__.py b/esphome/components/restart/switch/__init__.py new file mode 100644 index 0000000000..de30392b45 --- /dev/null +++ b/esphome/components/restart/switch/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART, +) + +restart_ns = cg.esphome_ns.namespace("restart") +RestartSwitch = restart_ns.class_("RestartSwitch", switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RestartSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Restart switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_RESTART): switch.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + } +).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) diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/switch/restart_switch.cpp similarity index 87% rename from esphome/components/restart/restart_switch.cpp rename to esphome/components/restart/switch/restart_switch.cpp index f66ebc616e..3076fde99e 100644 --- a/esphome/components/restart/restart_switch.cpp +++ b/esphome/components/restart/switch/restart_switch.cpp @@ -1,11 +1,12 @@ #include "restart_switch.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" namespace esphome { namespace restart { -static const char *TAG = "restart"; +static const char *const TAG = "restart"; void RestartSwitch::write_state(bool state) { // Acknowledge diff --git a/esphome/components/restart/restart_switch.h b/esphome/components/restart/switch/restart_switch.h similarity index 100% rename from esphome/components/restart/restart_switch.h rename to esphome/components/restart/switch/restart_switch.h diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 1fd4fbc7bd..228e7d882b 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -1,75 +1,242 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_CODE, CONF_LOW, CONF_SYNC, CONF_HIGH from esphome.components import uart +from esphome.const import ( + CONF_CODE, + CONF_HIGH, + CONF_ID, + CONF_LENGTH, + CONF_LOW, + CONF_PROTOCOL, + CONF_RAW, + CONF_SYNC, + CONF_TRIGGER_ID, + CONF_DURATION, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@jesserockz"] -rf_bridge_ns = cg.esphome_ns.namespace('rf_bridge') -RFBridgeComponent = rf_bridge_ns.class_('RFBridgeComponent', cg.Component, uart.UARTDevice) +rf_bridge_ns = cg.esphome_ns.namespace("rf_bridge") +RFBridgeComponent = rf_bridge_ns.class_( + "RFBridgeComponent", cg.Component, uart.UARTDevice +) -RFBridgeData = rf_bridge_ns.struct('RFBridgeData') +RFBridgeData = rf_bridge_ns.struct("RFBridgeData") +RFBridgeAdvancedData = rf_bridge_ns.struct("RFBridgeAdvancedData") -RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_('RFBridgeReceivedCodeTrigger', - automation.Trigger.template(RFBridgeData)) +RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_( + "RFBridgeReceivedCodeTrigger", automation.Trigger.template(RFBridgeData) +) +RFBridgeReceivedAdvancedCodeTrigger = rf_bridge_ns.class_( + "RFBridgeReceivedAdvancedCodeTrigger", + automation.Trigger.template(RFBridgeAdvancedData), +) -RFBridgeSendCodeAction = rf_bridge_ns.class_('RFBridgeSendCodeAction', automation.Action) -RFBridgeLearnAction = rf_bridge_ns.class_('RFBridgeLearnAction', automation.Action) +RFBridgeSendCodeAction = rf_bridge_ns.class_( + "RFBridgeSendCodeAction", automation.Action +) +RFBridgeSendAdvancedCodeAction = rf_bridge_ns.class_( + "RFBridgeSendAdvancedCodeAction", automation.Action +) + +RFBridgeLearnAction = rf_bridge_ns.class_("RFBridgeLearnAction", automation.Action) + +RFBridgeStartAdvancedSniffingAction = rf_bridge_ns.class_( + "RFBridgeStartAdvancedSniffingAction", automation.Action +) +RFBridgeStopAdvancedSniffingAction = rf_bridge_ns.class_( + "RFBridgeStopAdvancedSniffingAction", automation.Action +) + +RFBridgeStartBucketSniffingAction = rf_bridge_ns.class_( + "RFBridgeStartBucketSniffingAction", automation.Action +) + +RFBridgeBeepAction = rf_bridge_ns.class_("RFBridgeBeepAction", automation.Action) + +RFBridgeSendRawAction = rf_bridge_ns.class_("RFBridgeSendRawAction", automation.Action) + +CONF_ON_CODE_RECEIVED = "on_code_received" +CONF_ON_ADVANCED_CODE_RECEIVED = "on_advanced_code_received" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RFBridgeComponent), + cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + RFBridgeReceivedCodeTrigger + ), + } + ), + cv.Optional(CONF_ON_ADVANCED_CODE_RECEIVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + RFBridgeReceivedAdvancedCodeTrigger + ), + } + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) -CONF_ON_CODE_RECEIVED = 'on_code_received' - -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(RFBridgeComponent), - cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RFBridgeReceivedCodeTrigger), - }), -}).extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) - - -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_CODE_RECEIVED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [(RFBridgeData, 'data')], conf) + await automation.build_automation(trigger, [(RFBridgeData, "data")], conf) + + for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(RFBridgeAdvancedData, "data")], conf + ) -RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(RFBridgeComponent), - cv.Required(CONF_SYNC): cv.templatable(cv.hex_uint16_t), - cv.Required(CONF_LOW): cv.templatable(cv.hex_uint16_t), - cv.Required(CONF_HIGH): cv.templatable(cv.hex_uint16_t), - cv.Required(CONF_CODE): cv.templatable(cv.hex_uint32_t) -}) +RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_SYNC): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_LOW): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_HIGH): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_CODE): cv.templatable(cv.hex_uint32_t), + } +) -@automation.register_action('rf_bridge.send_code', RFBridgeSendCodeAction, - RFBRIDGE_SEND_CODE_SCHEMA) -def rf_bridge_send_code_to_code(config, action_id, template_args, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "rf_bridge.send_code", RFBridgeSendCodeAction, RFBRIDGE_SEND_CODE_SCHEMA +) +async def rf_bridge_send_code_to_code(config, action_id, template_args, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) - template_ = yield cg.templatable(config[CONF_SYNC], args, cg.uint16) + template_ = await cg.templatable(config[CONF_SYNC], args, cg.uint16) cg.add(var.set_sync(template_)) - template_ = yield cg.templatable(config[CONF_LOW], args, cg.uint16) + template_ = await cg.templatable(config[CONF_LOW], args, cg.uint16) cg.add(var.set_low(template_)) - template_ = yield cg.templatable(config[CONF_HIGH], args, cg.uint16) + template_ = await cg.templatable(config[CONF_HIGH], args, cg.uint16) cg.add(var.set_high(template_)) - template_ = yield cg.templatable(config[CONF_CODE], args, cg.uint32) + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32) cg.add(var.set_code(template_)) - yield var + return var -RFBRIDGE_LEARN_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(RFBridgeComponent) -}) +RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)}) -@automation.register_action('rf_bridge.learn', RFBridgeLearnAction, RFBRIDGE_LEARN_SCHEMA) -def rf_bridge_learnx_to_code(config, action_id, template_args, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA) +async def rf_bridge_learnx_to_code(config, action_id, template_args, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) - yield var + return var + + +@automation.register_action( + "rf_bridge.start_advanced_sniffing", + RFBridgeStartAdvancedSniffingAction, + RFBRIDGE_ID_SCHEMA, +) +async def rf_bridge_start_advanced_sniffing_to_code( + config, action_id, template_args, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + return var + + +@automation.register_action( + "rf_bridge.stop_advanced_sniffing", + RFBridgeStopAdvancedSniffingAction, + RFBRIDGE_ID_SCHEMA, +) +async def rf_bridge_stop_advanced_sniffing_to_code( + config, action_id, template_args, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + return var + + +@automation.register_action( + "rf_bridge.start_bucket_sniffing", + RFBridgeStartBucketSniffingAction, + RFBRIDGE_ID_SCHEMA, +) +async def rf_bridge_start_bucket_sniffing_to_code( + config, action_id, template_args, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + return var + + +RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_LENGTH): cv.templatable(cv.hex_uint8_t), + cv.Required(CONF_PROTOCOL): cv.templatable(cv.hex_uint8_t), + cv.Required(CONF_CODE): cv.templatable(cv.string), + } +) + + +@automation.register_action( + "rf_bridge.send_advanced_code", + RFBridgeSendAdvancedCodeAction, + RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA, +) +async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint16) + cg.add(var.set_length(template_)) + template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint16) + cg.add(var.set_protocol(template_)) + template_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(template_)) + return var + + +RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_RAW): cv.templatable(cv.string), + } +) + + +@automation.register_action( + "rf_bridge.send_raw", RFBridgeSendRawAction, RFBRIDGE_SEND_RAW_SCHEMA +) +async def rf_bridge_send_raw_to_code(config, action_id, template_args, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = await cg.templatable(config[CONF_RAW], args, cg.std_string) + cg.add(var.set_raw(template_)) + return var + + +RFBRIDGE_BEEP_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_DURATION): cv.templatable(cv.uint16_t), + } +) + + +@automation.register_action("rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA) +async def rf_bridge_beep_to_code(config, action_id, template_args, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint16) + cg.add(var.set_duration(template_)) + return var diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index f1537cdc87..d8c8047496 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace rf_bridge { -static const char *TAG = "rf_bridge"; +static const char *const TAG = "rf_bridge"; void RFBridgeComponent::ack_() { ESP_LOGV(TAG, "Sending ACK"); @@ -15,59 +15,129 @@ void RFBridgeComponent::ack_() { this->flush(); } -void RFBridgeComponent::decode_() { - uint8_t action = uartbuf_[0]; - RFBridgeData data{}; +bool RFBridgeComponent::parse_bridge_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_LOGVV(TAG, "Processing byte: 0x%02X", byte); + + // Byte 0: Start + if (at == 0) + return byte == RF_CODE_START; + + // Byte 1: Action + if (at == 1) + return byte >= RF_CODE_ACK && byte <= RF_CODE_RFIN_BUCKET; + uint8_t action = raw[1]; switch (action) { case RF_CODE_ACK: ESP_LOGD(TAG, "Action OK"); break; case RF_CODE_LEARN_KO: - this->ack_(); - ESP_LOGD(TAG, "Learn timeout"); + ESP_LOGD(TAG, "Learning timeout"); break; case RF_CODE_LEARN_OK: - ESP_LOGD(TAG, "Learn started"); - case RF_CODE_RFIN: - this->ack_(); + case RF_CODE_RFIN: { + if (byte != RF_CODE_STOP || at < RF_MESSAGE_SIZE + 2) + return true; - data.sync = (uartbuf_[1] << 8) | uartbuf_[2]; - data.low = (uartbuf_[3] << 8) | uartbuf_[4]; - data.high = (uartbuf_[5] << 8) | uartbuf_[6]; - data.code = (uartbuf_[7] << 16) | (uartbuf_[8] << 8) | uartbuf_[9]; + RFBridgeData data; + data.sync = (raw[2] << 8) | raw[3]; + data.low = (raw[4] << 8) | raw[5]; + data.high = (raw[6] << 8) | raw[7]; + data.code = (raw[8] << 16) | (raw[9] << 8) | raw[10]; - ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, + if (action == RF_CODE_LEARN_OK) + ESP_LOGD(TAG, "Learning success"); + + ESP_LOGI(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, data.code); - this->callback_.call(data); + this->data_callback_.call(data); break; + } + case RF_CODE_LEARN_OK_NEW: + case RF_CODE_ADVANCED_RFIN: { + if (byte != RF_CODE_STOP) { + return at < (raw[2] + 3); + } + + RFBridgeAdvancedData data{}; + + data.length = raw[2]; + data.protocol = raw[3]; + char next_byte[3]; + for (uint8_t i = 0; i < data.length - 1; i++) { + sprintf(next_byte, "%02X", raw[4 + i]); + data.code += next_byte; + } + + ESP_LOGI(TAG, "Received RFBridge Advanced Code: length=0x%02X protocol=0x%02X code=0x%s", data.length, + data.protocol, data.code.c_str()); + this->advanced_data_callback_.call(data); + break; + } + case RF_CODE_RFIN_BUCKET: { + if (byte != RF_CODE_STOP) { + return true; + } + + uint8_t buckets = raw[2] << 1; + std::string str; + char next_byte[3]; + + for (uint32_t i = 0; i <= at; i++) { + sprintf(next_byte, "%02X", raw[i]); + str += next_byte; + if ((i > 3) && buckets) { + buckets--; + } + if ((i < 3) || (buckets % 2) || (i == at - 1)) { + str += " "; + } + } + ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str()); + break; + } default: - ESP_LOGD(TAG, "Unknown action: 0x%02X", action); + ESP_LOGW(TAG, "Unknown action: 0x%02X", action); break; } - this->last_ = millis(); + + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + + if (byte == RF_CODE_STOP && action != RF_CODE_ACK) + this->ack_(); + + // return false to reset buffer + return false; +} + +void RFBridgeComponent::write_byte_str_(const std::string &codes) { + uint8_t code; + int size = codes.length(); + for (int i = 0; i < size; i += 2) { + code = strtol(codes.substr(i, 2).c_str(), nullptr, 16); + this->write(code); + } } void RFBridgeComponent::loop() { - bool receiving = false; - if (this->last_ != 0 && millis() - this->last_ > RF_DEBOUNCE) { - this->last_ = 0; + const uint32_t now = millis(); + if (now - this->last_bridge_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_bridge_byte_ = now; } while (this->available()) { - uint8_t c = this->read(); - if (receiving) { - if (c == RF_CODE_STOP && (this->uartpos_ == 1 || this->uartpos_ == RF_MESSAGE_SIZE + 1)) { - this->decode_(); - receiving = false; - } else if (this->uartpos_ <= RF_MESSAGE_SIZE) { - this->uartbuf_[uartpos_++] = c; - } else { - receiving = false; - } - } else if (c == RF_CODE_START) { - this->uartpos_ = 0; - receiving = true; + uint8_t byte; + this->read_byte(&byte); + if (this->parse_bridge_byte_(byte)) { + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + this->last_bridge_byte_ = now; + } else { + this->rx_buffer_.clear(); } } } @@ -77,11 +147,29 @@ void RFBridgeComponent::send_code(RFBridgeData data) { data.code); this->write(RF_CODE_START); this->write(RF_CODE_RFOUT); - this->write(data.sync); - this->write(data.low); - this->write(data.high); - this->write(data.code); + this->write((data.sync >> 8) & 0xFF); + this->write(data.sync & 0xFF); + this->write((data.low >> 8) & 0xFF); + this->write(data.low & 0xFF); + this->write((data.high >> 8) & 0xFF); + this->write(data.high & 0xFF); + this->write((data.code >> 16) & 0xFF); + this->write((data.code >> 8) & 0xFF); + this->write(data.code & 0xFF); this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::send_advanced_code(const RFBridgeAdvancedData &data) { + ESP_LOGD(TAG, "Sending advanced code: length=0x%02X protocol=0x%02X code=0x%s", data.length, data.protocol, + data.code.c_str()); + this->write(RF_CODE_START); + this->write(RF_CODE_RFOUT_NEW); + this->write(data.length & 0xFF); + this->write(data.protocol & 0xFF); + this->write_byte_str_(data.code); + this->write(RF_CODE_STOP); + this->flush(); } void RFBridgeComponent::learn() { @@ -89,6 +177,7 @@ void RFBridgeComponent::learn() { this->write(RF_CODE_START); this->write(RF_CODE_LEARN); this->write(RF_CODE_STOP); + this->flush(); } void RFBridgeComponent::dump_config() { @@ -96,5 +185,47 @@ void RFBridgeComponent::dump_config() { this->check_uart_settings(19200); } +void RFBridgeComponent::start_advanced_sniffing() { + ESP_LOGI(TAG, "Advanced Sniffing on"); + this->write(RF_CODE_START); + this->write(RF_CODE_SNIFFING_ON); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::stop_advanced_sniffing() { + ESP_LOGI(TAG, "Advanced Sniffing off"); + this->write(RF_CODE_START); + this->write(RF_CODE_SNIFFING_OFF); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::start_bucket_sniffing() { + ESP_LOGI(TAG, "Raw Bucket Sniffing on"); + this->write(RF_CODE_START); + this->write(RF_CODE_RFIN_BUCKET); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::send_raw(const std::string &raw_code) { + ESP_LOGD(TAG, "Sending Raw Code: %s", raw_code.c_str()); + + this->write_byte_str_(raw_code); + this->flush(); +} + +void RFBridgeComponent::beep(uint16_t ms) { + ESP_LOGD(TAG, "Beeping for %hu ms", ms); + + this->write(RF_CODE_START); + this->write(RF_CODE_BEEP); + this->write((ms >> 8) & 0xFF); + this->write(ms & 0xFF); + this->write(RF_CODE_STOP); + this->flush(); +} + } // namespace rf_bridge } // namespace esphome diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 86713b8a5c..9156d995bc 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" @@ -15,6 +17,7 @@ static const uint8_t RF_CODE_LEARN_KO = 0xA2; static const uint8_t RF_CODE_LEARN_OK = 0xA3; static const uint8_t RF_CODE_RFIN = 0xA4; static const uint8_t RF_CODE_RFOUT = 0xA5; +static const uint8_t RF_CODE_ADVANCED_RFIN = 0xA6; static const uint8_t RF_CODE_SNIFFING_ON = 0xA6; static const uint8_t RF_CODE_SNIFFING_OFF = 0xA7; static const uint8_t RF_CODE_RFOUT_NEW = 0xA8; @@ -22,6 +25,8 @@ static const uint8_t RF_CODE_LEARN_NEW = 0xA9; static const uint8_t RF_CODE_LEARN_KO_NEW = 0xAA; static const uint8_t RF_CODE_LEARN_OK_NEW = 0xAB; static const uint8_t RF_CODE_RFOUT_BUCKET = 0xB0; +static const uint8_t RF_CODE_RFIN_BUCKET = 0xB1; +static const uint8_t RF_CODE_BEEP = 0xC0; static const uint8_t RF_CODE_STOP = 0x55; static const uint8_t RF_DEBOUNCE = 200; @@ -32,25 +37,42 @@ struct RFBridgeData { uint32_t code; }; +struct RFBridgeAdvancedData { + uint8_t length; + uint8_t protocol; + std::string code; +}; + class RFBridgeComponent : public uart::UARTDevice, public Component { public: void loop() override; void dump_config() override; void add_on_code_received_callback(std::function callback) { - this->callback_.add(std::move(callback)); + this->data_callback_.add(std::move(callback)); + } + void add_on_advanced_code_received_callback(std::function callback) { + this->advanced_data_callback_.add(std::move(callback)); } void send_code(RFBridgeData data); + void send_advanced_code(const RFBridgeAdvancedData &data); void learn(); + void start_advanced_sniffing(); + void stop_advanced_sniffing(); + void start_bucket_sniffing(); + void send_raw(const std::string &code); + void beep(uint16_t ms); protected: void ack_(); void decode_(); + bool parse_bridge_byte_(uint8_t byte); + void write_byte_str_(const std::string &codes); - unsigned long last_ = 0; - unsigned char uartbuf_[RF_MESSAGE_SIZE + 3] = {0}; - unsigned char uartpos_ = 0; + std::vector rx_buffer_; + uint32_t last_bridge_byte_{0}; - CallbackManager callback_; + CallbackManager data_callback_; + CallbackManager advanced_data_callback_; }; class RFBridgeReceivedCodeTrigger : public Trigger { @@ -60,6 +82,13 @@ class RFBridgeReceivedCodeTrigger : public Trigger { } }; +class RFBridgeReceivedAdvancedCodeTrigger : public Trigger { + public: + explicit RFBridgeReceivedAdvancedCodeTrigger(RFBridgeComponent *parent) { + parent->add_on_advanced_code_received_callback([this](const RFBridgeAdvancedData &data) { this->trigger(data); }); + } +}; + template class RFBridgeSendCodeAction : public Action { public: RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} @@ -81,6 +110,25 @@ template class RFBridgeSendCodeAction : public Action { RFBridgeComponent *parent_; }; +template class RFBridgeSendAdvancedCodeAction : public Action { + public: + RFBridgeSendAdvancedCodeAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint8_t, length) + TEMPLATABLE_VALUE(uint8_t, protocol) + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) { + RFBridgeAdvancedData data{}; + data.length = this->length_.value(x...); + data.protocol = this->protocol_.value(x...); + data.code = this->code_.value(x...); + this->parent_->send_advanced_code(data); + } + + protected: + RFBridgeComponent *parent_; +}; + template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} @@ -91,5 +139,57 @@ template class RFBridgeLearnAction : public Action { RFBridgeComponent *parent_; }; +template class RFBridgeStartAdvancedSniffingAction : public Action { + public: + RFBridgeStartAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->start_advanced_sniffing(); } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeStopAdvancedSniffingAction : public Action { + public: + RFBridgeStopAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->stop_advanced_sniffing(); } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeStartBucketSniffingAction : public Action { + public: + RFBridgeStartBucketSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->start_bucket_sniffing(); } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeSendRawAction : public Action { + public: + RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, raw) + + void play(Ts... x) { this->parent_->send_raw(this->raw_.value(x...)); } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeBeepAction : public Action { + public: + RFBridgeBeepAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint16_t, duration) + + void play(Ts... x) { this->parent_->beep(this->duration_.value(x...)); } + + protected: + RFBridgeComponent *parent_; +}; + } // namespace rf_bridge } // namespace esphome diff --git a/esphome/components/rgb/light.py b/esphome/components/rgb/light.py index 6bece17664..3d07855b8e 100644 --- a/esphome/components/rgb/light.py +++ b/esphome/components/rgb/light.py @@ -3,24 +3,26 @@ 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 -rgb_ns = cg.esphome_ns.namespace('rgb') -RGBLightOutput = rgb_ns.class_('RGBLightOutput', light.LightOutput) +rgb_ns = cg.esphome_ns.namespace("rgb") +RGBLightOutput = rgb_ns.class_("RGBLightOutput", light.LightOutput) -CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBLightOutput), - 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), -}) +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBLightOutput), + 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), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield light.register_light(var, config) + await light.register_light(var, config) - red = yield cg.get_variable(config[CONF_RED]) + red = await cg.get_variable(config[CONF_RED]) cg.add(var.set_red(red)) - green = yield cg.get_variable(config[CONF_GREEN]) + green = await cg.get_variable(config[CONF_GREEN]) cg.add(var.set_green(green)) - blue = yield cg.get_variable(config[CONF_BLUE]) + blue = await cg.get_variable(config[CONF_BLUE]) cg.add(var.set_blue(blue)) diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index e612c80f73..ef53c8042d 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -12,15 +12,15 @@ class RGBLightOutput : public light::LightOutput { void set_red(output::FloatOutput *red) { red_ = red; } void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } + 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 { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue); + state->current_values_as_rgb(&red, &green, &blue, false); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); 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 75d6082e5a..f747580f61 100644 --- a/esphome/components/rgbw/light.py +++ b/esphome/components/rgbw/light.py @@ -1,29 +1,40 @@ 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) +rgbw_ns = cg.esphome_ns.namespace("rgbw") +RGBWLightOutput = rgbw_ns.class_("RGBWLightOutput", light.LightOutput) -CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWLightOutput), - 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_WHITE): cv.use_id(output.FloatOutput), -}) +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWLightOutput), + 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_WHITE): cv.use_id(output.FloatOutput), + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield light.register_light(var, config) + await light.register_light(var, config) - red = yield cg.get_variable(config[CONF_RED]) + red = await cg.get_variable(config[CONF_RED]) cg.add(var.set_red(red)) - green = yield cg.get_variable(config[CONF_GREEN]) + green = await cg.get_variable(config[CONF_GREEN]) cg.add(var.set_green(green)) - blue = yield cg.get_variable(config[CONF_BLUE]) + blue = await cg.get_variable(config[CONF_BLUE]) cg.add(var.set_blue(blue)) - white = yield cg.get_variable(config[CONF_WHITE]) + white = await cg.get_variable(config[CONF_WHITE]) cg.add(var.set_white(white)) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index b58c7f9d54..0f55775608 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -13,16 +13,18 @@ class RGBWLightOutput : public light::LightOutput { void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_white(output::FloatOutput *white) { white_ = white; } + 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); + 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 { float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); + state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -34,6 +36,7 @@ class RGBWLightOutput : public light::LightOutput { output::FloatOutput *green_; output::FloatOutput *blue_; output::FloatOutput *white_; + bool color_interlock_{false}; }; } // namespace rgbw diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index 78f4bee630..35f77b154b 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -1,44 +1,69 @@ 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_COLD_WHITE, \ - CONF_WARM_WHITE, CONF_COLD_WHITE_COLOR_TEMPERATURE, \ - CONF_WARM_WHITE_COLOR_TEMPERATURE +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_INTERLOCK, + CONF_CONSTANT_BRIGHTNESS, + CONF_GREEN, + CONF_RED, + CONF_OUTPUT_ID, + CONF_COLD_WHITE, + CONF_WARM_WHITE, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, +) -rgbww_ns = cg.esphome_ns.namespace('rgbww') -RGBWWLightOutput = rgbww_ns.class_('RGBWWLightOutput', light.LightOutput) - -CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' - -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, -}) +rgbww_ns = cg.esphome_ns.namespace("rgbww") +RGBWWLightOutput = rgbww_ns.class_("RGBWWLightOutput", light.LightOutput) -def to_code(config): +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, +) + + +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield light.register_light(var, config) + await light.register_light(var, config) - red = yield cg.get_variable(config[CONF_RED]) + red = await cg.get_variable(config[CONF_RED]) cg.add(var.set_red(red)) - green = yield cg.get_variable(config[CONF_GREEN]) + green = await cg.get_variable(config[CONF_GREEN]) cg.add(var.set_green(green)) - blue = yield cg.get_variable(config[CONF_BLUE]) + blue = await cg.get_variable(config[CONF_BLUE]) cg.add(var.set_blue(blue)) - cwhite = yield cg.get_variable(config[CONF_COLD_WHITE]) + 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 = yield cg.get_variable(config[CONF_WARM_WHITE]) + 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 a975331a37..5a86b88595 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -17,12 +17,13 @@ class RGBWWLightOutput : public light::LightOutput { 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); + 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; @@ -43,9 +44,10 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *blue_; 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_; + bool color_interlock_{false}; }; } // namespace rgbww diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 769d3e1103..aff8fc381c 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace rotary_encoder { -static const char *TAG = "rotary_encoder"; +static const char *const TAG = "rotary_encoder"; // based on https://github.com/jkDesignDE/MechInputs/blob/master/QEIx4.cpp static const uint8_t STATE_LUT_MASK = 0x1C; // clears upper counter increment/decrement bits and pin states @@ -82,22 +82,42 @@ 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; uint16_t new_state = STATE_LOOKUP_TABLE[input_state]; if ((new_state & arg->resolution & STATE_HAS_INCREMENTED) != 0) { if (arg->counter < arg->max_value) arg->counter++; + rotation_dir = 1; } if ((new_state & arg->resolution & STATE_HAS_DECREMENTED) != 0) { if (arg->counter > arg->min_value) arg->counter--; + rotation_dir = -1; + } + + if (rotation_dir != 0) { + auto first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero + if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration) + || std::signbit(*std::prev(first_zero)) != + std::signbit(rotation_dir) // or is the last stored event the wrong direction + || *std::prev(first_zero) == std::numeric_limits::lowest() // or the last event slot is full (negative) + || *std::prev(first_zero) == std::numeric_limits::max()) { // or the last event slot is full (positive) + if (first_zero != arg->rotation_events.end()) { // we have a free rotation slot + *first_zero += rotation_dir; // store the rotation into a new slot + } else { + arg->rotation_events_overflow = true; + } + } else { + *std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot + } } arg->state = new_state; @@ -105,6 +125,22 @@ void ICACHE_RAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensor void RotaryEncoderSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up Rotary Encoder '%s'...", this->name_.c_str()); + + int32_t initial_value = 0; + switch (this->restore_mode_) { + case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->rtc_.load(&initial_value)) { + initial_value = 0; + } + break; + case ROTARY_ENCODER_ALWAYS_ZERO: + initial_value = 0; + break; + } + this->store_.counter = initial_value; + this->store_.last_read = initial_value; + this->pin_a_->setup(); this->store_.pin_a = this->pin_a_->to_isr(); this->pin_b_->setup(); @@ -114,14 +150,26 @@ 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); LOG_PIN(" Pin A: ", this->pin_a_); LOG_PIN(" Pin B: ", this->pin_b_); LOG_PIN(" Pin I: ", this->pin_i_); + + const LogString *restore_mode = LOG_STR(""); + switch (this->restore_mode_) { + case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: + restore_mode = LOG_STR("Restore (Defaults to zero)"); + break; + case ROTARY_ENCODER_ALWAYS_ZERO: + restore_mode = LOG_STR("Always zero"); + break; + } + ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode)); + switch (this->store_.resolution) { case ROTARY_ENCODER_1_PULSE_PER_CYCLE: ESP_LOGCONFIG(TAG, " Resolution: 1 Pulse Per Cycle"); @@ -135,17 +183,54 @@ void RotaryEncoderSensor::dump_config() { } } void RotaryEncoderSensor::loop() { + std::array rotation_events; + bool 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; + } + + if (rotation_events_overflow) { + ESP_LOGW(TAG, "Captured more rotation events than expected"); + } + + for (auto events : rotation_events) { + if (events == 0) // we are at the end of the recorded events + break; + + if (events > 0) { + while (events--) { + this->on_clockwise_callback_.call(); + } + } else { + while (events++) { + this->on_anticlockwise_callback_.call(); + } + } + } + if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) { this->store_.counter = 0; } int counter = this->store_.counter; - if (this->store_.last_read != counter) { + if (this->store_.last_read != counter || this->publish_initial_value_) { + if (this->restore_mode_ == ROTARY_ENCODER_RESTORE_DEFAULT_ZERO) { + this->rtc_.save(&counter); + } this->store_.last_read = counter; this->publish_state(counter); + this->publish_initial_value_ = false; } } float RotaryEncoderSensor::get_setup_priority() const { return setup_priority::DATA; } +void RotaryEncoderSensor::set_restore_mode(RotaryEncoderRestoreMode restore_mode) { + this->restore_mode_ = restore_mode; +} void RotaryEncoderSensor::set_resolution(RotaryEncoderResolution mode) { this->store_.resolution = mode; } void RotaryEncoderSensor::set_min_value(int32_t min_value) { this->store_.min_value = min_value; } void RotaryEncoderSensor::set_max_value(int32_t max_value) { this->store_.max_value = max_value; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4220645478..a69d738fa8 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -1,13 +1,21 @@ #pragma once +#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" namespace esphome { namespace rotary_encoder { +/// All possible restore modes for the rotary encoder +enum RotaryEncoderRestoreMode { + ROTARY_ENCODER_RESTORE_DEFAULT_ZERO, /// try to restore counter, otherwise set to zero + ROTARY_ENCODER_ALWAYS_ZERO, /// do not restore counter, always set to zero +}; + /// All possible resolutions for the rotary encoder enum RotaryEncoderResolution { ROTARY_ENCODER_1_PULSE_PER_CYCLE = @@ -17,8 +25,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}; @@ -27,13 +35,25 @@ struct RotaryEncoderSensorStore { int32_t last_read{0}; uint8_t state{0}; + std::array rotation_events{}; + bool rotation_events_overflow{false}; + static void gpio_intr(RotaryEncoderSensorStore *arg); }; 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 restore mode of the rotary encoder. + * + * By default (if possible) the last known counter state is restored. Otherwise the value 0 is used. + * Restoring the state can also be turned off. + * + * @param restore_mode The restore mode to use. + */ + void set_restore_mode(RotaryEncoderRestoreMode restore_mode); /** Set the resolution of the rotary encoder. * @@ -53,6 +73,7 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void set_reset_pin(GPIOPin *pin_i) { this->pin_i_ = pin_i; } void set_min_value(int32_t min_value); void set_max_value(int32_t max_value); + void set_publish_initial_value(bool publish_initial_value) { publish_initial_value_ = publish_initial_value; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -62,23 +83,52 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { float get_setup_priority() const override; + void add_on_clockwise_callback(std::function callback) { + this->on_clockwise_callback_.add(std::move(callback)); + } + + void add_on_anticlockwise_callback(std::function callback) { + this->on_anticlockwise_callback_.add(std::move(callback)); + } + 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. + bool publish_initial_value_; + ESPPreferenceObject rtc_; + RotaryEncoderRestoreMode restore_mode_{ROTARY_ENCODER_RESTORE_DEFAULT_ZERO}; RotaryEncoderSensorStore store_{}; + + CallbackManager on_clockwise_callback_; + CallbackManager on_anticlockwise_callback_; }; template class RotaryEncoderSetValueAction : public Action { public: RotaryEncoderSetValueAction(RotaryEncoderSensor *encoder) : encoder_(encoder) {} TEMPLATABLE_VALUE(int, value) + void play(Ts... x) override { this->encoder_->set_value(this->value_.value(x...)); } protected: RotaryEncoderSensor *encoder_; }; +class RotaryEncoderClockwiseTrigger : public Trigger<> { + public: + explicit RotaryEncoderClockwiseTrigger(RotaryEncoderSensor *parent) { + parent->add_on_clockwise_callback([this]() { this->trigger(); }); + } +}; + +class RotaryEncoderAnticlockwiseTrigger : public Trigger<> { + public: + explicit RotaryEncoderAnticlockwiseTrigger(RotaryEncoderSensor *parent) { + parent->add_on_anticlockwise_callback([this]() { this->trigger(); }); + } +}; + } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 214ccbd056..cd747264b3 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -2,22 +2,54 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation from esphome.components import sensor -from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \ - ICON_ROTATE_RIGHT, CONF_VALUE, CONF_PIN_A, CONF_PIN_B +from esphome.const import ( + CONF_ID, + CONF_RESOLUTION, + CONF_MIN_VALUE, + CONF_MAX_VALUE, + STATE_CLASS_NONE, + UNIT_STEPS, + ICON_ROTATE_RIGHT, + CONF_VALUE, + CONF_PIN_A, + CONF_PIN_B, + CONF_TRIGGER_ID, + CONF_RESTORE_MODE, +) -rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') -RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') +rotary_encoder_ns = cg.esphome_ns.namespace("rotary_encoder") + +RotaryEncoderRestoreMode = rotary_encoder_ns.enum("RotaryEncoderRestoreMode") +RESTORE_MODES = { + "RESTORE_DEFAULT_ZERO": RotaryEncoderRestoreMode.ROTARY_ENCODER_RESTORE_DEFAULT_ZERO, + "ALWAYS_ZERO": RotaryEncoderRestoreMode.ROTARY_ENCODER_ALWAYS_ZERO, +} + +RotaryEncoderResolution = rotary_encoder_ns.enum("RotaryEncoderResolution") RESOLUTIONS = { 1: RotaryEncoderResolution.ROTARY_ENCODER_1_PULSE_PER_CYCLE, 2: RotaryEncoderResolution.ROTARY_ENCODER_2_PULSES_PER_CYCLE, 4: RotaryEncoderResolution.ROTARY_ENCODER_4_PULSES_PER_CYCLE, } -CONF_PIN_RESET = 'pin_reset' +CONF_PIN_RESET = "pin_reset" +CONF_ON_CLOCKWISE = "on_clockwise" +CONF_ON_ANTICLOCKWISE = "on_anticlockwise" +CONF_PUBLISH_INITIAL_VALUE = "publish_initial_value" -RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component) -RotaryEncoderSetValueAction = rotary_encoder_ns.class_('RotaryEncoderSetValueAction', - automation.Action) +RotaryEncoderSensor = rotary_encoder_ns.class_( + "RotaryEncoderSensor", sensor.Sensor, cg.Component +) +RotaryEncoderSetValueAction = rotary_encoder_ns.class_( + "RotaryEncoderSetValueAction", automation.Action +) + +RotaryEncoderClockwiseTrigger = rotary_encoder_ns.class_( + "RotaryEncoderClockwiseTrigger", automation.Trigger +) +RotaryEncoderAnticlockwiseTrigger = rotary_encoder_ns.class_( + "RotaryEncoderAnticlockwiseTrigger", automation.Trigger +) def validate_min_max_value(config): @@ -25,35 +57,66 @@ def validate_min_max_value(config): min_val = config[CONF_MIN_VALUE] 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)) + raise cv.Invalid( + 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).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.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_, - cv.Optional(CONF_MAX_VALUE): cv.int_, -}).extend(cv.COMPONENT_SCHEMA), validate_min_max_value) +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + 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), + 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_, + cv.Optional(CONF_MAX_VALUE): cv.int_, + cv.Optional(CONF_PUBLISH_INITIAL_VALUE, default=False): cv.boolean, + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_ZERO"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + RotaryEncoderClockwiseTrigger + ), + } + ), + cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + RotaryEncoderAnticlockwiseTrigger + ), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + validate_min_max_value, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - pin_a = yield cg.gpio_pin_expression(config[CONF_PIN_A]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + pin_a = await cg.gpio_pin_expression(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a)) - pin_b = yield cg.gpio_pin_expression(config[CONF_PIN_B]) + pin_b = await cg.gpio_pin_expression(config[CONF_PIN_B]) cg.add(var.set_pin_b(pin_b)) + cg.add(var.set_publish_initial_value(config[CONF_PUBLISH_INITIAL_VALUE])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) if CONF_PIN_RESET in config: - pin_i = yield cg.gpio_pin_expression(config[CONF_PIN_RESET]) + pin_i = await cg.gpio_pin_expression(config[CONF_PIN_RESET]) cg.add(var.set_reset_pin(pin_i)) cg.add(var.set_resolution(config[CONF_RESOLUTION])) if CONF_MIN_VALUE in config: @@ -61,15 +124,27 @@ def to_code(config): if CONF_MAX_VALUE in config: cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + for conf in config.get(CONF_ON_CLOCKWISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ANTICLOCKWISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) -@automation.register_action('sensor.rotary_encoder.set_value', RotaryEncoderSetValueAction, - cv.Schema({ - cv.Required(CONF_ID): cv.use_id(sensor.Sensor), - cv.Required(CONF_VALUE): cv.templatable(cv.int_), - })) -def sensor_template_publish_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) + +@automation.register_action( + "sensor.rotary_encoder.set_value", + RotaryEncoderSetValueAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_VALUE): cv.templatable(cv.int_), + } + ), +) +async def sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, int) cg.add(var.set_value(template_)) - yield var + return var diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py new file mode 100644 index 0000000000..e9453896ac --- /dev/null +++ b/esphome/components/rtttl/__init__.py @@ -0,0 +1,132 @@ +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 + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@glmnet"] +CONF_RTTTL = "rtttl" +CONF_ON_FINISHED_PLAYBACK = "on_finished_playback" + +rtttl_ns = cg.esphome_ns.namespace("rtttl") + +Rtttl = rtttl_ns.class_("Rtttl", cg.Component) +PlayAction = rtttl_ns.class_("PlayAction", automation.Action) +StopAction = rtttl_ns.class_("StopAction", automation.Action) +FinishedPlaybackTrigger = rtttl_ns.class_( + "FinishedPlaybackTrigger", automation.Trigger.template() +) +IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition) + +MULTI_CONF = True + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), + cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +def validate_parent_output_config(value): + platform = value.get(CONF_PLATFORM) + PWM_GOOD = ["esp8266_pwm", "ledc"] + PWM_BAD = [ + "ac_dimmer ", + "esp32_dac", + "slow_pwm", + "mcp4725", + "pca9685", + "tlc59208f", + "my9231", + "sm16716", + ] + + if platform in PWM_BAD: + 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." + ) + + +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) + + out = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(out)) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_action( + "rtttl.play", + PlayAction, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(Rtttl), + cv.Required(CONF_RTTTL): cv.templatable(cv.string), + }, + key=CONF_RTTTL, + ), +) +async def rtttl_play_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_RTTTL], args, cg.std_string) + cg.add(var.set_value(template_)) + return var + + +@automation.register_action( + "rtttl.stop", + StopAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(Rtttl), + } + ), +) +async def rtttl_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_condition( + "rtttl.is_playing", + IsPlayingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(Rtttl), + } + ), +) +async def rtttl_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp new file mode 100644 index 0000000000..c76d4a89b0 --- /dev/null +++ b/esphome/components/rtttl/rtttl.cpp @@ -0,0 +1,187 @@ +#include "rtttl.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rtttl { + +static const char *const TAG = "rtttl"; + +static const uint32_t DOUBLE_NOTE_GAP_MS = 10; + +// These values can also be found as constants in the Tone library (Tone.h) +static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, + 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, + 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, + 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; + +void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } + +void Rtttl::play(std::string rtttl) { + rtttl_ = std::move(rtttl); + + default_duration_ = 4; + default_octave_ = 6; + int bpm = 63; + uint8_t num; + + // Get name + position_ = rtttl_.find(':'); + + // it's somewhat documented to be up to 10 characters but let's be a bit flexible here + if (position_ == std::string::npos || position_ > 15) { + ESP_LOGE(TAG, "Missing ':' when looking for name."); + return; + } + + auto name = this->rtttl_.substr(0, position_); + ESP_LOGD(TAG, "Playing song %s", name.c_str()); + + // get default duration + position_ = this->rtttl_.find("d=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'd='"); + return; + } + position_ += 2; + num = this->get_integer_(); + if (num > 0) + default_duration_ = num; + + // get default octave + position_ = rtttl_.find("o=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'o="); + return; + } + position_ += 2; + num = get_integer_(); + if (num >= 3 && num <= 7) + default_octave_ = num; + + // get BPM + position_ = rtttl_.find("b=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing b="); + return; + } + position_ += 2; + num = get_integer_(); + if (num != 0) + bpm = num; + + position_ = rtttl_.find(':', position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing second ':'"); + return; + } + position_++; + + // BPM usually expresses the number of quarter notes per minute + wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + + output_freq_ = 0; + last_note_ = millis(); + note_duration_ = 1; +} + +void Rtttl::loop() { + if (note_duration_ == 0 || millis() - last_note_ < note_duration_) + return; + + if (!rtttl_[position_]) { + output_->set_level(0.0); + ESP_LOGD(TAG, "Playback finished"); + this->on_finished_playback_callback_.call(); + note_duration_ = 0; + return; + } + + // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... + while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') + position_++; + + // first, get note duration, if available + uint8_t num = this->get_integer_(); + + if (num) + note_duration_ = wholenote_ / num; + else + note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after + + uint8_t note; + + switch (rtttl_[position_]) { + case 'c': + note = 1; + break; + case 'd': + note = 3; + break; + case 'e': + note = 5; + break; + case 'f': + note = 6; + break; + case 'g': + note = 8; + break; + case 'a': + note = 10; + break; + case 'b': + note = 12; + break; + case 'p': + default: + note = 0; + } + position_++; + + // now, get optional '#' sharp + if (rtttl_[position_] == '#') { + note++; + position_++; + } + + // now, get optional '.' dotted note + if (rtttl_[position_] == '.') { + note_duration_ += note_duration_ / 2; + position_++; + } + + // now, get scale + uint8_t scale = get_integer_(); + if (scale == 0) + scale = default_octave_; + + // Now play the note + if (note) { + auto note_index = (scale - 4) * 12 + note; + if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { + ESP_LOGE(TAG, "Note out of valid range"); + return; + } + auto freq = NOTES[note_index]; + + if (freq == output_freq_) { + // Add small silence gap between same note + output_->set_level(0.0); + delay(DOUBLE_NOTE_GAP_MS); + note_duration_ -= DOUBLE_NOTE_GAP_MS; + } + output_freq_ = freq; + + ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); + output_->update_frequency(freq); + output_->set_level(0.5); + } else { + ESP_LOGVV(TAG, "waiting: %dms", note_duration_); + output_->set_level(0.0); + } + + last_note_ = millis(); +} +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h new file mode 100644 index 0000000000..ec6fe7f98f --- /dev/null +++ b/esphome/components/rtttl/rtttl.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace rtttl { + +class Rtttl : public Component { + public: + void set_output(output::FloatOutput *output) { output_ = output; } + void play(std::string rtttl); + void stop() { + note_duration_ = 0; + output_->set_level(0.0); + } + void dump_config() override; + + bool is_playing() { return note_duration_ != 0; } + void loop() override; + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + inline uint8_t get_integer_() { + uint8_t ret = 0; + while (isdigit(rtttl_[position_])) { + ret = (ret * 10) + (rtttl_[position_++] - '0'); + } + return ret; + } + + std::string rtttl_; + size_t position_; + uint16_t wholenote_; + uint16_t default_duration_; + uint16_t default_octave_; + uint32_t last_note_; + uint16_t note_duration_; + + uint32_t output_freq_; + output::FloatOutput *output_; + + CallbackManager on_finished_playback_callback_; +}; + +template class PlayAction : public Action { + public: + PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} + TEMPLATABLE_VALUE(std::string, value) + + void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + + protected: + Rtttl *rtttl_; +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class FinishedPlaybackTrigger : public Trigger<> { + public: + explicit FinishedPlaybackTrigger(Rtttl *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/ruuvi_ble/__init__.py b/esphome/components/ruuvi_ble/__init__.py index 05ba008dd0..1e3fb4b003 100644 --- a/esphome/components/ruuvi_ble/__init__.py +++ b/esphome/components/ruuvi_ble/__init__.py @@ -3,16 +3,20 @@ import esphome.config_validation as cv from esphome.components import esp32_ble_tracker from esphome.const import CONF_ID -DEPENDENCIES = ['esp32_ble_tracker'] +DEPENDENCIES = ["esp32_ble_tracker"] -ruuvi_ble_ns = cg.esphome_ns.namespace('ruuvi_ble') -RuuviListener = ruuvi_ble_ns.class_('RuuviListener', esp32_ble_tracker.ESPBTDeviceListener) +ruuvi_ble_ns = cg.esphome_ns.namespace("ruuvi_ble") +RuuviListener = ruuvi_ble_ns.class_( + "RuuviListener", esp32_ble_tracker.ESPBTDeviceListener +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(RuuviListener), -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RuuviListener), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 7e13140e55..bdd012cf5c 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -1,12 +1,12 @@ #include "ruuvi_ble.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ruuvi_ble { -static const char *TAG = "ruuvi_ble"; +static const char *const TAG = "ruuvi_ble"; bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviParseResult &result) { const uint8_t data_type = adv_data[0]; @@ -50,7 +50,7 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; - const uint8_t power_info = (data[12] << 8) | data[13]; + const uint16_t power_info = (uint16_t(data[12] << 8) | data[13]); const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; 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 2963b777d1..9b462b4794 100644 --- a/esphome/components/ruuvitag/ruuvitag.cpp +++ b/esphome/components/ruuvitag/ruuvitag.cpp @@ -1,12 +1,12 @@ #include "ruuvitag.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ruuvitag { -static const char *TAG = "ruuvitag"; +static const char *const TAG = "ruuvitag"; void RuuviTag::dump_config() { ESP_LOGCONFIG(TAG, "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 3d9c724e7b..2bb9549195 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -1,76 +1,166 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - CONF_PRESSURE, CONF_ACCELERATION, CONF_ACCELERATION_X, CONF_ACCELERATION_Y, \ - CONF_ACCELERATION_Z, CONF_BATTERY_VOLTAGE, CONF_TX_POWER, \ - CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, UNIT_CELSIUS, \ - ICON_THERMOMETER, UNIT_PERCENT, UNIT_VOLT, UNIT_HECTOPASCAL, UNIT_G, \ - UNIT_DECIBEL_MILLIWATT, UNIT_EMPTY, ICON_WATER_PERCENT, ICON_BATTERY, \ - ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, ICON_ACCELERATION_Y, \ - ICON_ACCELERATION_Z, ICON_SIGNAL, CONF_ID +from esphome.const import ( + CONF_HUMIDITY, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + CONF_PRESSURE, + CONF_ACCELERATION, + CONF_ACCELERATION_X, + CONF_ACCELERATION_Y, + CONF_ACCELERATION_Z, + CONF_BATTERY_VOLTAGE, + CONF_TX_POWER, + CONF_MEASUREMENT_SEQUENCE_NUMBER, + CONF_MOVEMENT_COUNTER, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_HECTOPASCAL, + UNIT_G, + UNIT_DECIBEL_MILLIWATT, + ICON_GAUGE, + ICON_ACCELERATION, + ICON_ACCELERATION_X, + ICON_ACCELERATION_Y, + ICON_ACCELERATION_Z, + CONF_ID, +) -DEPENDENCIES = ['esp32_ble_tracker'] -AUTO_LOAD = ['ruuvi_ble'] +DEPENDENCIES = ["esp32_ble_tracker"] +AUTO_LOAD = ["ruuvi_ble"] -ruuvitag_ns = cg.esphome_ns.namespace('ruuvitag') +ruuvitag_ns = cg.esphome_ns.namespace("ruuvitag") RuuviTag = ruuvitag_ns.class_( - 'RuuviTag', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + "RuuviTag", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) -CONFIG_SCHEMA = cv.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_THERMOMETER, 2), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 2), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 2), - cv.Optional(CONF_ACCELERATION): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION, 3), - cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_X, 3), - cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_Y, 3), - cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_Z, 3), - cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_BATTERY, 3), - cv.Optional(CONF_TX_POWER): sensor.sensor_schema(UNIT_DECIBEL_MILLIWATT, ICON_SIGNAL, 0), - cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema(UNIT_EMPTY, ICON_GAUGE, 0), - cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema(UNIT_EMPTY, ICON_GAUGE, 0), -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RuuviTag), + 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_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_ACCELERATION): sensor.sensor_schema( + 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_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_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_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_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TX_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema( + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema( + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) if CONF_PRESSURE in config: - sens = yield sensor.new_sensor(config[CONF_PRESSURE]) + sens = await sensor.new_sensor(config[CONF_PRESSURE]) cg.add(var.set_pressure(sens)) if CONF_ACCELERATION in config: - sens = yield sensor.new_sensor(config[CONF_ACCELERATION]) + sens = await sensor.new_sensor(config[CONF_ACCELERATION]) cg.add(var.set_acceleration(sens)) if CONF_ACCELERATION_X in config: - sens = yield sensor.new_sensor(config[CONF_ACCELERATION_X]) + sens = await sensor.new_sensor(config[CONF_ACCELERATION_X]) cg.add(var.set_acceleration_x(sens)) if CONF_ACCELERATION_Y in config: - sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Y]) + sens = await sensor.new_sensor(config[CONF_ACCELERATION_Y]) cg.add(var.set_acceleration_y(sens)) if CONF_ACCELERATION_Z in config: - sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Z]) + sens = await sensor.new_sensor(config[CONF_ACCELERATION_Z]) cg.add(var.set_acceleration_z(sens)) if CONF_BATTERY_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) cg.add(var.set_battery_voltage(sens)) if CONF_TX_POWER in config: - sens = yield sensor.new_sensor(config[CONF_TX_POWER]) + sens = await sensor.new_sensor(config[CONF_TX_POWER]) cg.add(var.set_tx_power(sens)) if CONF_MOVEMENT_COUNTER in config: - sens = yield sensor.new_sensor(config[CONF_MOVEMENT_COUNTER]) + sens = await sensor.new_sensor(config[CONF_MOVEMENT_COUNTER]) cg.add(var.set_movement_counter(sens)) if CONF_MEASUREMENT_SEQUENCE_NUMBER in config: - sens = yield sensor.new_sensor(config[CONF_MEASUREMENT_SEQUENCE_NUMBER]) + sens = await sensor.new_sensor(config[CONF_MEASUREMENT_SEQUENCE_NUMBER]) cg.add(var.set_measurement_sequence_number(sens)) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py new file mode 100644 index 0000000000..ab884bfee4 --- /dev/null +++ b/esphome/components/safe_mode/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@paulmonigatti", "@jsuanet"] + +safe_mode_ns = cg.esphome_ns.namespace("safe_mode") diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py new file mode 100644 index 0000000000..2cd8892afb --- /dev/null +++ b/esphome/components/safe_mode/button/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.components.ota import OTAComponent +from esphome.const import ( + CONF_ID, + CONF_OTA, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) + +DEPENDENCIES = ["ota"] + +safe_mode_ns = cg.esphome_ns.namespace("safe_mode") +SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component) + +CONFIG_SCHEMA = ( + button.button_schema( + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ) + .extend({cv.GenerateID(): cv.declare_id(SafeModeButton)}) + .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) + + ota = await cg.get_variable(config[CONF_OTA]) + cg.add(var.set_ota(ota)) diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp new file mode 100644 index 0000000000..2b8654de46 --- /dev/null +++ b/esphome/components/safe_mode/button/safe_mode_button.cpp @@ -0,0 +1,25 @@ +#include "safe_mode_button.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.button"; + +void SafeModeButton::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; } + +void SafeModeButton::press_action() { + 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 SafeModeButton::dump_config() { LOG_BUTTON("", "Safe Mode Button", this); } + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h new file mode 100644 index 0000000000..63e0d1755e --- /dev/null +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ota/ota_component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace safe_mode { + +class SafeModeButton : public button::Button, public Component { + public: + void dump_config() override; + void set_ota(ota::OTAComponent *ota); + + protected: + ota::OTAComponent *ota_; + void press_action() override; +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py new file mode 100644 index 0000000000..b6c3e852f6 --- /dev/null +++ b/esphome/components/safe_mode/switch/__init__.py @@ -0,0 +1,41 @@ +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_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + CONF_OTA, + ENTITY_CATEGORY_CONFIG, + 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, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + } +).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 195dfef5f6..272ee75e30 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -1,10 +1,15 @@ #include "scd30.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP8266 +#include +#endif namespace esphome { namespace scd30 { -static const char *TAG = "scd30"; +static const char *const TAG = "scd30"; static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100; static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; @@ -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 @@ -43,23 +48,49 @@ void SCD30Component::setup() { ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8), uint16_t(raw_firmware_version[0] & 0xFF)); - /// Sensor initialization - if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, 0)) { - ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - - // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on - if (this->altitude_compensation_ != 0xFFFF) { - if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { - ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); + if (this->temperature_offset_ != 0) { + if (!this->write_command_(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) { + ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } } +#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 practice 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. + // + // By experimentation a delay of 20ms as already sufficient. Let's go + // safe and use 30ms delays. + delay(30); +#endif + + if (!this->write_command_(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) { + ESP_LOGE(TAG, "Sensor SCD30 error setting update interval."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } +#ifdef USE_ESP32 + delay(30); +#endif + + // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on + if (this->altitude_compensation_ != 0xFFFF) { + if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { + ESP_LOGE(TAG, "Sensor SCD30 error setting altitude compensation."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } +#ifdef USE_ESP32 + delay(30); +#endif if (!this->write_command_(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration."); @@ -67,6 +98,23 @@ void SCD30Component::setup() { this->mark_failed(); return; } +#ifdef USE_ESP32 + delay(30); +#endif + + /// Sensor initialization + if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) { + ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + // check each 500ms if data is ready, and read it in that case + this->set_interval("status-check", 500, [this]() { + if (this->is_data_ready_()) + this->update(); + }); } void SCD30Component::dump_config() { @@ -94,19 +142,15 @@ void SCD30Component::dump_config() { ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); } ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); - LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_compensation_); + ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); + ESP_LOGCONFIG(TAG, " Update interval: %ds", this->update_interval_); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } void SCD30Component::update() { - /// Check if measurement is ready before reading the value - if (!this->write_command_(SCD30_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(); @@ -152,6 +196,18 @@ void SCD30Component::update() { }); } +bool SCD30Component::is_data_ready_() { + if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { + return false; + } + delay(4); + uint16_t is_data_ready; + if (!this->read_data_(&is_data_ready, 1)) { + return false; + } + return is_data_ready == 1; +} + bool SCD30Component::write_command_(uint16_t command) { // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. return this->write_byte(command >> 8, command & 0xFF); @@ -164,7 +220,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) { @@ -192,10 +248,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; } @@ -204,13 +259,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/scd30.h b/esphome/components/scd30/scd30.h index 2c4ee51f8a..64193d0cb6 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -8,16 +8,21 @@ namespace esphome { namespace scd30 { /// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. -class SCD30Component : public PollingComponent, public i2c::I2CDevice { +class SCD30Component : public Component, public i2c::I2CDevice { public: void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } 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_ = (uint16_t)(pressure * 1000); + } + void set_temperature_offset(float offset) { temperature_offset_ = offset; } + void set_update_interval(uint16_t interval) { update_interval_ = interval; } void setup() override; - void update() override; + void update(); void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -26,6 +31,7 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { bool write_command_(uint16_t command, uint16_t data); bool read_data_(uint16_t *data, uint8_t len); uint8_t sht_crc_(uint8_t data1, uint8_t data2); + bool is_data_ready_(); enum ErrorCode { COMMUNICATION_FAILED, @@ -35,6 +41,9 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { } error_code_{UNKNOWN}; bool enable_asc_{true}; uint16_t altitude_compensation_{0xFFFF}; + uint16_t ambient_pressure_compensation_{0x0000}; + float temperature_offset_{0.0}; + uint16_t update_interval_{0xFFFF}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index b3de1b214a..cd25649f2a 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,54 +1,104 @@ -import re +from esphome import core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \ - CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT, CONF_CO2 +from esphome.const import ( + CONF_ID, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_CO2, + CONF_UPDATE_INTERVAL, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, + ICON_MOLECULE_CO2, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -scd30_ns = cg.esphome_ns.namespace('scd30') -SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) +scd30_ns = cg.esphome_ns.namespace("scd30") +SCD30Component = scd30_ns.class_("SCD30Component", cg.Component, i2c.I2CDevice) -CONF_AUTOMATIC_SELF_CALIBRATION = 'automatic_self_calibration' -CONF_ALTITUDE_COMPENSATION = 'altitude_compensation' +CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" +CONF_ALTITUDE_COMPENSATION = "altitude_compensation" +CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" +CONF_TEMPERATURE_OFFSET = "temperature_offset" -def remove_altitude_suffix(value): - return re.sub(r"\s*(?:m(?:\s+a\.s\.l)?)|(?:MAM?SL)$", '', value) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SCD30Component), + cv.Optional(CONF_CO2): sensor.sensor_schema( + 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_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, + cv.Optional(CONF_ALTITUDE_COMPENSATION): 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, default=0): cv.pressure, + cv.Optional(CONF_TEMPERATURE_OFFSET): cv.temperature, + cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All( + cv.positive_time_period_seconds, + cv.Range( + min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800) + ), + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x61)) +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SCD30Component), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, - ICON_PERIODIC_TABLE_CO2, 0), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), - cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, - cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All(remove_altitude_suffix, - cv.int_range(min=0, max=0xFFFF, - max_included=False)), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61)) - - -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_automatic_self_calibration(config[CONF_AUTOMATIC_SELF_CALIBRATION])) if CONF_ALTITUDE_COMPENSATION in config: cg.add(var.set_altitude_compensation(config[CONF_ALTITUDE_COMPENSATION])) + if CONF_AMBIENT_PRESSURE_COMPENSATION in config: + cg.add( + var.set_ambient_pressure_compensation( + config[CONF_AMBIENT_PRESSURE_COMPENSATION] + ) + ) + + if CONF_TEMPERATURE_OFFSET in config: + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + if CONF_CO2 in config: - sens = yield sensor.new_sensor(config[CONF_CO2]) + sens = await sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) 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..eacb39edf1 --- /dev/null +++ b/esphome/components/scd4x/scd4x.cpp @@ -0,0 +1,294 @@ +#include "scd4x.h" +#include "esphome/core/hal.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; + } + + uint32_t stop_measurement_delay = 0; + // 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; + } + // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after + // issuing the stop_periodic_measurement command + stop_measurement_delay = 500; + } + this->set_timeout(stop_measurement_delay, [this]() { + 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->update_ambient_pressure_compensation_(ambient_pressure_)) { + 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; + } + + if (this->ambient_pressure_source_ != nullptr) { + float pressure = this->ambient_pressure_source_->state / 1000.0f; + if (!std::isnan(pressure)) { + set_ambient_pressure_compensation(this->ambient_pressure_source_->state / 1000.0f); + } + } + + // 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(); +} +// Note pressure in bar here. Convert to hPa +void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { + ambient_pressure_compensation_ = true; + uint16_t new_ambient_pressure = (uint16_t)(pressure_in_bar * 1000); + // remove millibar from comparison to avoid frequent updates +/- 10 millibar doesn't matter + if (initialized_ && (new_ambient_pressure / 10 != ambient_pressure_ / 10)) { + update_ambient_pressure_compensation_(new_ambient_pressure); + ambient_pressure_ = new_ambient_pressure; + } else { + ESP_LOGD(TAG, "ambient pressure compensation skipped - no change required"); + } +} + +bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { + if (this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { + ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa); + return true; + } else { + ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + return false; + } +} + +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..4fe2bf14cc --- /dev/null +++ b/esphome/components/scd4x/scd4x.h @@ -0,0 +1,54 @@ +#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_in_bar); + void set_ambient_pressure_source(sensor::Sensor *pressure) { ambient_pressure_source_ = pressure; } + 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); + bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); + + 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}; + // used for compensation + sensor::Sensor *ambient_pressure_source_{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..3e814ffe78 --- /dev/null +++ b/esphome/components/scd4x/sensor.py @@ -0,0 +1,105 @@ +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" +CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" + +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, + cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( + sensor.Sensor + ), + } + ) + .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)) + + if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: + sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) + cg.add(var.set_ambient_pressure_source(sens)) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 9590679f83..9702878475 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -2,58 +2,126 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_MODE -script_ns = cg.esphome_ns.namespace('script') -Script = script_ns.class_('Script', automation.Trigger.template()) -ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) -ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) -ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) -IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) +CODEOWNERS = ["@esphome/core"] +script_ns = cg.esphome_ns.namespace("script") +Script = script_ns.class_("Script", automation.Trigger.template()) +ScriptExecuteAction = script_ns.class_("ScriptExecuteAction", automation.Action) +ScriptStopAction = script_ns.class_("ScriptStopAction", automation.Action) +ScriptWaitAction = script_ns.class_("ScriptWaitAction", automation.Action, cg.Component) +IsRunningCondition = script_ns.class_("IsRunningCondition", automation.Condition) +SingleScript = script_ns.class_("SingleScript", Script) +RestartScript = script_ns.class_("RestartScript", Script) +QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component) +ParallelScript = script_ns.class_("ParallelScript", Script) -CONFIG_SCHEMA = automation.validate_automation({ - cv.Required(CONF_ID): cv.declare_id(Script), -}) +CONF_SINGLE = "single" +CONF_RESTART = "restart" +CONF_QUEUED = "queued" +CONF_PARALLEL = "parallel" +CONF_MAX_RUNS = "max_runs" + +SCRIPT_MODES = { + CONF_SINGLE: SingleScript, + CONF_RESTART: RestartScript, + CONF_QUEUED: QueueingScript, + CONF_PARALLEL: ParallelScript, +} -def to_code(config): +def check_max_runs(value): + if CONF_MAX_RUNS not in value: + return value + if value[CONF_MODE] not in [CONF_QUEUED, CONF_PARALLEL]: + raise cv.Invalid( + "The option 'max_runs' is only valid in 'queue' and 'parallel' mode.", + path=[CONF_MAX_RUNS], + ) + return value + + +def assign_declare_id(value): + value = value.copy() + value[CONF_ID] = cv.declare_id(SCRIPT_MODES[value[CONF_MODE]])(value[CONF_ID]) + return value + + +CONFIG_SCHEMA = automation.validate_automation( + { + # Don't declare id as cv.declare_id yet, because the ID type + # 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 + ), + cv.Optional(CONF_MAX_RUNS): cv.positive_int, + }, + extra_validators=cv.All(check_max_runs, assign_declare_id), +) + + +async def to_code(config): # Register all variables first, so that scripts can use other scripts triggers = [] for conf in config: trigger = cg.new_Pvariable(conf[CONF_ID]) + # Add a human-readable name to the script + cg.add(trigger.set_name(conf[CONF_ID].id)) + + if CONF_MAX_RUNS in conf: + cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) + + if conf[CONF_MODE] == CONF_QUEUED: + await cg.register_component(trigger, conf) + triggers.append((trigger, conf)) for trigger, conf in triggers: - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) -@automation.register_action('script.execute', ScriptExecuteAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Script), -})) -def script_execute_action_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) +@automation.register_action( + "script.execute", + ScriptExecuteAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Script), + } + ), +) +async def script_execute_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('script.stop', ScriptStopAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Script) -})) -def script_stop_action_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) +@automation.register_action( + "script.stop", + ScriptStopAction, + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), +) +async def script_stop_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action('script.wait', ScriptWaitAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Script) -})) -def script_wait_action_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) +@automation.register_action( + "script.wait", + ScriptWaitAction, + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), +) +async def script_wait_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + await cg.register_component(var, {}) + return var -@automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Script) -})) -def script_is_running_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren) +@automation.register_condition( + "script.is_running", + IsRunningCondition, + automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), +) +async def script_is_running_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp new file mode 100644 index 0000000000..46bcef905b --- /dev/null +++ b/esphome/components/script/script.cpp @@ -0,0 +1,67 @@ +#include "script.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace script { + +static const char *const TAG = "script"; + +void SingleScript::execute() { + if (this->is_action_running()) { + ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + return; + } + + this->trigger(); +} + +void RestartScript::execute() { + if (this->is_action_running()) { + ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->stop_action(); + } + + this->trigger(); +} + +void QueueingScript::execute() { + if (this->is_action_running()) { + // num_runs_ is the number of *queued* instances, so total number of instances is + // num_runs_ + 1 + if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + return; + } + + ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); + this->num_runs_++; + return; + } + + this->trigger(); + // Check if the trigger was immediate and we can continue right away. + this->loop(); +} + +void QueueingScript::stop() { + this->num_runs_ = 0; + Script::stop(); +} + +void QueueingScript::loop() { + if (this->num_runs_ != 0 && !this->is_action_running()) { + this->num_runs_--; + this->trigger(); + } +} + +void ParallelScript::execute() { + if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + return; + } + this->trigger(); +} + +} // namespace script +} // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 3b97327da8..5663d32ce8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,29 +1,86 @@ #pragma once #include "esphome/core/automation.h" +#include "esphome/core/component.h" namespace esphome { namespace script { +/// The abstract base class for all script types. class Script : public Trigger<> { public: - void execute() { - bool prev = this->in_stack_; - this->in_stack_ = true; - this->trigger(); - this->in_stack_ = prev; - } - bool script_is_running() { return this->in_stack_ || this->is_running(); } + /** Execute a new instance of this script. + * + * The behavior of this function when a script is already running is defined by the subtypes + */ + virtual void execute() = 0; + /// Check if any instance of this script is currently running. + virtual bool is_running() { return this->is_action_running(); } + /// Stop all instances of this script. + virtual void stop() { this->stop_action(); } + + // Internal function to give scripts readable names. + void set_name(const std::string &name) { name_ = name; } protected: - bool in_stack_{false}; + std::string name_; +}; + +/** A script type for which only a single instance at a time is allowed. + * + * If a new instance is executed while the previous one hasn't finished yet, + * a warning is printed and the new instance is discarded. + */ +class SingleScript : public Script { + public: + void execute() override; +}; + +/** A script type that restarts scripts from the beginning when a new instance is started. + * + * If a new instance is started but another one is already running, the existing + * script is stopped and the new instance starts from the beginning. + */ +class RestartScript : public Script { + public: + void execute() override; +}; + +/** A script type that queues new instances that are created. + * + * Only one instance of the script can be active at a time. + */ +class QueueingScript : public Script, public Component { + public: + void execute() override; + void stop() override; + void loop() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int num_runs_ = 0; + int max_runs_ = 0; +}; + +/** 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 executed in parallel to the other instances. + */ +class ParallelScript : public Script { + public: + void execute() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int max_runs_ = 0; }; template class ScriptExecuteAction : public Action { public: ScriptExecuteAction(Script *script) : script_(script) {} - void play(Ts... x) override { this->script_->trigger(); } + void play(Ts... x) override { this->script_->execute(); } protected: Script *script_; @@ -43,7 +100,7 @@ template class IsRunningCondition : public Condition { public: explicit IsRunningCondition(Script *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->script_is_running(); } + bool check(Ts... x) override { return this->parent_->is_running(); } protected: Script *parent_; @@ -53,41 +110,34 @@ template class ScriptWaitAction : public Action, public C public: ScriptWaitAction(Script *script) : script_(script) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { - this->triggered_ = false; - this->play_next(x...); + this->play_next_(x...); return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (this->script_->is_running()) return; - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Script *script_; - bool triggered_{false}; std::tuple var_{}; }; diff --git a/esphome/components/sdm_meter/__init__.py b/esphome/components/sdm_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp new file mode 100644 index 0000000000..2348c88938 --- /dev/null +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -0,0 +1,106 @@ +#include "sdm_meter.h" +#include "sdm_meter_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sdm_meter { + +static const char *const TAG = "sdm_meter"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers + +void SDMMeter::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for SDMMeter!"); + return; + } + + auto sdm_meter_get_float = [&](size_t i) -> float { + uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]); + float f; + memcpy(&f, &temp, sizeof(f)); + return f; + }; + + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + + float voltage = sdm_meter_get_float(SDM_PHASE_1_VOLTAGE * 2 + (i * 4)); + float current = sdm_meter_get_float(SDM_PHASE_1_CURRENT * 2 + (i * 4)); + float active_power = sdm_meter_get_float(SDM_PHASE_1_ACTIVE_POWER * 2 + (i * 4)); + float apparent_power = sdm_meter_get_float(SDM_PHASE_1_APPARENT_POWER * 2 + (i * 4)); + float reactive_power = sdm_meter_get_float(SDM_PHASE_1_REACTIVE_POWER * 2 + (i * 4)); + float power_factor = sdm_meter_get_float(SDM_PHASE_1_POWER_FACTOR * 2 + (i * 4)); + float phase_angle = sdm_meter_get_float(SDM_PHASE_1_ANGLE * 2 + (i * 4)); + + ESP_LOGD( + TAG, + "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f VAR, PF=%.3f, " + "PA=%.3f °", + i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle); + if (phase.voltage_sensor_ != nullptr) + phase.voltage_sensor_->publish_state(voltage); + if (phase.current_sensor_ != nullptr) + phase.current_sensor_->publish_state(current); + if (phase.active_power_sensor_ != nullptr) + phase.active_power_sensor_->publish_state(active_power); + if (phase.apparent_power_sensor_ != nullptr) + phase.apparent_power_sensor_->publish_state(apparent_power); + if (phase.reactive_power_sensor_ != nullptr) + phase.reactive_power_sensor_->publish_state(reactive_power); + if (phase.power_factor_sensor_ != nullptr) + phase.power_factor_sensor_->publish_state(power_factor); + if (phase.phase_angle_sensor_ != nullptr) + phase.phase_angle_sensor_->publish_state(phase_angle); + } + + float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2); + float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2); + float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2); + float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2); + float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2); + + ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh", frequency, + import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy); + + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + 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->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); +} + +void SDMMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SDMMeter::dump_config() { + ESP_LOGCONFIG(TAG, "SDM Meter:"); + 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_); + LOG_SENSOR(" ", "Active Power", phase.active_power_sensor_); + LOG_SENSOR(" ", "Apparent Power", phase.apparent_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", phase.reactive_power_sensor_); + LOG_SENSOR(" ", "Power Factor", phase.power_factor_sensor_); + LOG_SENSOR(" ", "Phase Angle", phase.phase_angle_sensor_); + } + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); + LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); + LOG_SENSOR(" ", "Import Reactive Energy", this->import_reactive_energy_sensor_); + LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); +} + +} // namespace sdm_meter +} // namespace esphome diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h new file mode 100644 index 0000000000..07ebe65bb7 --- /dev/null +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace sdm_meter { + +class SDMMeter : 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_active_power_sensor(uint8_t phase, sensor::Sensor *active_power_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].active_power_sensor_ = active_power_sensor; + } + void set_apparent_power_sensor(uint8_t phase, sensor::Sensor *apparent_power_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].apparent_power_sensor_ = apparent_power_sensor; + } + void set_reactive_power_sensor(uint8_t phase, sensor::Sensor *reactive_power_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].reactive_power_sensor_ = reactive_power_sensor; + } + void set_power_factor_sensor(uint8_t phase, sensor::Sensor *power_factor_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].power_factor_sensor_ = power_factor_sensor; + } + void set_phase_angle_sensor(uint8_t phase, sensor::Sensor *phase_angle_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].phase_angle_sensor_ = phase_angle_sensor; + } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } + void set_import_active_energy_sensor(sensor::Sensor *import_active_energy_sensor) { + this->import_active_energy_sensor_ = import_active_energy_sensor; + } + void set_export_active_energy_sensor(sensor::Sensor *export_active_energy_sensor) { + this->export_active_energy_sensor_ = export_active_energy_sensor; + } + void set_import_reactive_energy_sensor(sensor::Sensor *import_reactive_energy_sensor) { + this->import_reactive_energy_sensor_ = import_reactive_energy_sensor; + } + void set_export_reactive_energy_sensor(sensor::Sensor *export_reactive_energy_sensor) { + this->export_reactive_energy_sensor_ = export_reactive_energy_sensor; + } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + struct SDMPhase { + bool setup{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *phase_angle_sensor_{nullptr}; + } phases_[3]; + sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *import_active_energy_sensor_{nullptr}; + sensor::Sensor *export_active_energy_sensor_{nullptr}; + sensor::Sensor *import_reactive_energy_sensor_{nullptr}; + sensor::Sensor *export_reactive_energy_sensor_{nullptr}; +}; + +} // namespace sdm_meter +} // namespace esphome diff --git a/esphome/components/sdm_meter/sdm_meter_registers.h b/esphome/components/sdm_meter/sdm_meter_registers.h new file mode 100644 index 0000000000..dd981d6f00 --- /dev/null +++ b/esphome/components/sdm_meter/sdm_meter_registers.h @@ -0,0 +1,114 @@ +#pragma once + +namespace esphome { +namespace sdm_meter { + +/* PHASE STATUS REGISTERS */ +static const uint16_t SDM_PHASE_1_VOLTAGE = 0x0000; +static const uint16_t SDM_PHASE_2_VOLTAGE = 0x0002; +static const uint16_t SDM_PHASE_3_VOLTAGE = 0x0004; +static const uint16_t SDM_PHASE_1_CURRENT = 0x0006; +static const uint16_t SDM_PHASE_2_CURRENT = 0x0008; +static const uint16_t SDM_PHASE_3_CURRENT = 0x000A; +static const uint16_t SDM_PHASE_1_ACTIVE_POWER = 0x000C; +static const uint16_t SDM_PHASE_2_ACTIVE_POWER = 0x000E; +static const uint16_t SDM_PHASE_3_ACTIVE_POWER = 0x0010; +static const uint16_t SDM_PHASE_1_APPARENT_POWER = 0x0012; +static const uint16_t SDM_PHASE_2_APPARENT_POWER = 0x0014; +static const uint16_t SDM_PHASE_3_APPARENT_POWER = 0x0016; +static const uint16_t SDM_PHASE_1_REACTIVE_POWER = 0x0018; +static const uint16_t SDM_PHASE_2_REACTIVE_POWER = 0x001A; +static const uint16_t SDM_PHASE_3_REACTIVE_POWER = 0x001C; +static const uint16_t SDM_PHASE_1_POWER_FACTOR = 0x001E; +static const uint16_t SDM_PHASE_2_POWER_FACTOR = 0x0020; +static const uint16_t SDM_PHASE_3_POWER_FACTOR = 0x0022; +static const uint16_t SDM_PHASE_1_ANGLE = 0x0024; +static const uint16_t SDM_PHASE_2_ANGLE = 0x0026; +static const uint16_t SDM_PHASE_3_ANGLE = 0x0028; + +static const uint16_t SDM_AVERAGE_L_TO_N_VOLTS = 0x002A; +static const uint16_t SDM_AVERAGE_LINE_CURRENT = 0x002E; +static const uint16_t SDM_SUM_LINE_CURRENT = 0x0030; +static const uint16_t SDM_TOTAL_SYSTEM_POWER = 0x0034; +static const uint16_t SDM_TOTAL_SYSTEM_APPARENT_POWER = 0x0038; +static const uint16_t SDM_TOTAL_SYSTEM_REACTIVE_POWER = 0x003C; +static const uint16_t SDM_TOTAL_SYSTEM_POWER_FACTOR = 0x003E; +static const uint16_t SDM_TOTAL_SYSTEM_PHASE_ANGLE = 0x0042; + +static const uint16_t SDM_FREQUENCY = 0x0046; + +static const uint16_t SDM_IMPORT_ACTIVE_ENERGY = 0x0048; +static const uint16_t SDM_EXPORT_ACTIVE_ENERGY = 0x004A; +static const uint16_t SDM_IMPORT_REACTIVE_ENERGY = 0x004C; +static const uint16_t SDM_EXPORT_REACTIVE_ENERGY = 0x004E; + +static const uint16_t SDM_VAH_SINCE_LAST_RESET = 0x0050; +static const uint16_t SDM_AH_SINCE_LAST_RESET = 0x0052; +static const uint16_t SDM_TOTAL_SYSTEM_POWER_DEMAND = 0x0054; +static const uint16_t SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND = 0x0056; +static const uint16_t SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND = 0x0058; +static const uint16_t SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND = 0x005A; +static const uint16_t SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND = 0x005C; +static const uint16_t SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND = 0x005E; +static const uint16_t SDM_TOTAL_SYSTEM_VA_DEMAND = 0x0064; +static const uint16_t SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND = 0x0066; +static const uint16_t SDM_NEUTRAL_CURRENT_DEMAND = 0x0068; +static const uint16_t SDM_MAXIMUM_NEUTRAL_CURRENT = 0x006A; +static const uint16_t SDM_LINE_1_TO_LINE_2_VOLTS = 0x00C8; +static const uint16_t SDM_LINE_2_TO_LINE_3_VOLTS = 0x00CA; +static const uint16_t SDM_LINE_3_TO_LINE_1_VOLTS = 0x00CC; +static const uint16_t SDM_AVERAGE_LINE_TO_LINE_VOLTS = 0x00CE; +static const uint16_t SDM_NEUTRAL_CURRENT = 0x00E0; + +static const uint16_t SDM_PHASE_1_LN_VOLTS_THD = 0x00EA; +static const uint16_t SDM_PHASE_2_LN_VOLTS_THD = 0x00EC; +static const uint16_t SDM_PHASE_3_LN_VOLTS_THD = 0x00EE; +static const uint16_t SDM_PHASE_1_CURRENT_THD = 0x00F0; +static const uint16_t SDM_PHASE_2_CURRENT_THD = 0x00F2; +static const uint16_t SDM_PHASE_3_CURRENT_THD = 0x00F4; + +static const uint16_t SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD = 0x00F8; +static const uint16_t SDM_AVERAGE_LINE_CURRENT_THD = 0x00FA; +static const uint16_t SDM_TOTAL_SYSTEM_POWER_FACTOR_INV = 0x00FE; +static const uint16_t SDM_PHASE_1_CURRENT_DEMAND = 0x0102; +static const uint16_t SDM_PHASE_2_CURRENT_DEMAND = 0x0104; +static const uint16_t SDM_PHASE_3_CURRENT_DEMAND = 0x0106; +static const uint16_t SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND = 0x0108; +static const uint16_t SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND = 0x010A; +static const uint16_t SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND = 0x010C; +static const uint16_t SDM_LINE_1_TO_LINE_2_VOLTS_THD = 0x014E; +static const uint16_t SDM_LINE_2_TO_LINE_3_VOLTS_THD = 0x0150; +static const uint16_t SDM_LINE_3_TO_LINE_1_VOLTS_THD = 0x0152; +static const uint16_t SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD = 0x0154; + +static const uint16_t SDM_TOTAL_ACTIVE_ENERGY = 0x0156; +static const uint16_t SDM_TOTAL_REACTIVE_ENERGY = 0x0158; + +static const uint16_t SDM_L1_IMPORT_ACTIVE_ENERGY = 0x015A; +static const uint16_t SDM_L2_IMPORT_ACTIVE_ENERGY = 0x015C; +static const uint16_t SDM_L3_IMPORT_ACTIVE_ENERGY = 0x015E; +static const uint16_t SDM_L1_EXPORT_ACTIVE_ENERGY = 0x0160; +static const uint16_t SDM_L2_EXPORT_ACTIVE_ENERGY = 0x0162; +static const uint16_t SDM_L3_EXPORT_ACTIVE_ENERGY = 0x0164; +static const uint16_t SDM_L1_TOTAL_ACTIVE_ENERGY = 0x0166; +static const uint16_t SDM_L2_TOTAL_ACTIVE_ENERGY = 0x0168; +static const uint16_t SDM_L3_TOTAL_ACTIVE_ENERGY = 0x016a; +static const uint16_t SDM_L1_IMPORT_REACTIVE_ENERGY = 0x016C; +static const uint16_t SDM_L2_IMPORT_REACTIVE_ENERGY = 0x016E; +static const uint16_t SDM_L3_IMPORT_REACTIVE_ENERGY = 0x0170; +static const uint16_t SDM_L1_EXPORT_REACTIVE_ENERGY = 0x0172; +static const uint16_t SDM_L2_EXPORT_REACTIVE_ENERGY = 0x0174; +static const uint16_t SDM_L3_EXPORT_REACTIVE_ENERGY = 0x0176; +static const uint16_t SDM_L1_TOTAL_REACTIVE_ENERGY = 0x0178; +static const uint16_t SDM_L2_TOTAL_REACTIVE_ENERGY = 0x017A; +static const uint16_t SDM_L3_TOTAL_REACTIVE_ENERGY = 0x017C; + +static const uint16_t SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY = 0x0180; +static const uint16_t SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY = 0x0182; +static const uint16_t SDM_CURRENT_RESETTABLE_IMPORT_ENERGY = 0x0184; +static const uint16_t SDM_CURRENT_RESETTABLE_EXPORT_ENERGY = 0x0186; +static const uint16_t SDM_IMPORT_POWER = 0x0500; +static const uint16_t SDM_EXPORT_POWER = 0x0502; + +} // namespace sdm_meter +} // namespace esphome diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py new file mode 100644 index 0000000000..87c99c9152 --- /dev/null +++ b/esphome/components/sdm_meter/sensor.py @@ -0,0 +1,163 @@ +from esphome.components.atm90e32.sensor import CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C +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_PHASE_ANGLE, + 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, + ICON_FLASH, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_DEGREES, + UNIT_HERTZ, + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, +) + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@polyfaces", "@jesserockz"] + +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_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_ACTIVE_POWER: sensor.sensor_schema( + 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_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_REACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=2, + 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_PHASE_ANGLE: sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, icon=ICON_FLASH, accuracy_decimals=3 + ), +} + +PHASE_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PHASE_SENSORS.items()} +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SDMMeter), + cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + 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_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_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_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + } + ) + .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_IMPORT_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_IMPORT_ACTIVE_ENERGY]) + cg.add(var.set_import_active_energy_sensor(sens)) + + if CONF_EXPORT_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_EXPORT_ACTIVE_ENERGY]) + cg.add(var.set_export_active_energy_sensor(sens)) + + if CONF_IMPORT_REACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_IMPORT_REACTIVE_ENERGY]) + cg.add(var.set_import_reactive_energy_sensor(sens)) + + if CONF_EXPORT_REACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_EXPORT_REACTIVE_ENERGY]) + cg.add(var.set_export_reactive_energy_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)) 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..107ed2902f --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -0,0 +1,153 @@ +#include "sdp3x.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.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_START_MASS_FLOW_AVG[2] = {0x36, 0x03}; +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 + } + + this->set_timeout(20, [this] { + 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; + } + + // SDP8xx + // ref: + // https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/8_Differential_Pressure/Datasheets/Sensirion_Differential_Pressure_Datasheet_SDP8xx_Digital.pdf + if (data[2] == 0x02) { + switch (data[3]) { + case 0x01: // SDP800-500Pa + ESP_LOGCONFIG(TAG, "Sensor is SDP800-500Pa"); + break; + case 0x0A: // SDP810-500Pa + ESP_LOGCONFIG(TAG, "Sensor is SDP810-500Pa"); + break; + case 0x04: // SDP801-500Pa + ESP_LOGCONFIG(TAG, "Sensor is SDP801-500Pa"); + break; + case 0x0D: // SDP811-500Pa + ESP_LOGCONFIG(TAG, "Sensor is SDP811-500Pa"); + break; + case 0x02: // SDP800-125Pa + ESP_LOGCONFIG(TAG, "Sensor is SDP800-125Pa"); + break; + case 0x0B: // SDP810-125Pa + ESP_LOGCONFIG(TAG, "Sensor is SDP810-125Pa"); + break; + } + } else if (data[2] == 0x01) { + if (data[3] == 0x01) { + ESP_LOGCONFIG(TAG, "Sensor is SDP31-500Pa"); + } else if (data[3] == 0x02) { + ESP_LOGCONFIG(TAG, "Sensor is SDP32-125Pa"); + } + } + + if (this->write(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_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]); + int16_t temperature_raw = encode_uint16(data[3], data[4]); + int16_t scale_factor_raw = encode_uint16(data[6], data[7]); + // scale factor is in Pa - convert to hPa + float pressure = pressure_raw / (scale_factor_raw * 100.0f); + ESP_LOGV(TAG, "Got raw pressure=%d, raw scale factor =%d, raw temperature=%d ", pressure_raw, scale_factor_raw, + temperature_raw); + 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..0e74d0883d --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sdp3x { + +enum MeasurementMode { MASS_FLOW_AVG, DP_AVG }; + +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; + void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } + + 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); + MeasurementMode measurement_mode_; +}; + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py new file mode 100644 index 0000000000..45f5cc4d9a --- /dev/null +++ b/esphome/components/sdp3x/sensor.py @@ -0,0 +1,50 @@ +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) + + +MeasurementMode = sdp3x_ns.enum("MeasurementMode") +MEASUREMENT_MODE = { + "mass_flow": MeasurementMode.MASS_FLOW_AVG, + "differential_pressure": MeasurementMode.DP_AVG, +} +CONF_MEASUREMENT_MODE = "measurement_mode" + +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), + cv.Optional( + CONF_MEASUREMENT_MODE, default="differential_pressure" + ): cv.enum(MEASUREMENT_MODE), + } + ) + .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) + cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE])) diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 1a5be0adc3..0c04ff557f 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sds011 { -static const char *TAG = "sds011"; +static const char *const TAG = "sds011"; static const uint8_t SDS011_MSG_REQUEST_LENGTH = 19; static const uint8_t SDS011_MSG_RESPONSE_LENGTH = 10; @@ -50,6 +50,21 @@ void SDS011Component::setup() { this->sds011_write_command_(command_data); } +void SDS011Component::set_working_state(bool working_state) { + if (this->rx_mode_only_) { + // In RX-only mode we do not setup the sensor, it is assumed to be setup + // already + return; + } + uint8_t command_data[SDS011_DATA_REQUEST_LENGTH] = {0}; + command_data[0] = SDS011_COMMAND_SLEEP; + command_data[1] = SDS011_SET_MODE; + command_data[2] = working_state ? SDS011_MODE_WORK : SDS011_MODE_SLEEP; + command_data[13] = 0xff; + command_data[14] = 0xff; + this->sds011_write_command_(command_data); +} + void SDS011Component::dump_config() { ESP_LOGCONFIG(TAG, "SDS011:"); ESP_LOGCONFIG(TAG, " Update Interval: %u min", this->update_interval_min_); diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index 83f89df237..19e0cd3efe 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -25,6 +25,7 @@ class SDS011Component : public Component, public uart::UARTDevice { void set_update_interval(uint32_t val) { /* ignore */ } void set_update_interval_min(uint8_t update_interval_min); + void set_working_state(bool working_state); protected: void sds011_write_command_(const uint8_t *command); diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index 0f750810a6..456d47ee91 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -1,14 +1,23 @@ 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_10_0, CONF_PM_2_5, CONF_RX_ONLY, - CONF_UPDATE_INTERVAL, UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON) +from esphome.const import ( + CONF_ID, + CONF_PM_10_0, + CONF_PM_2_5, + CONF_RX_ONLY, + CONF_UPDATE_INTERVAL, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -sds011_ns = cg.esphome_ns.namespace('sds011') -SDS011Component = sds011_ns.class_('SDS011Component', uart.UARTDevice, cg.Component) +sds011_ns = cg.esphome_ns.namespace("sds011") +SDS011Component = sds011_ns.class_("SDS011Component", uart.UARTDevice, cg.Component) def validate_sds011_rx_mode(value): @@ -19,37 +28,54 @@ def validate_sds011_rx_mode(value): elif value.get(CONF_RX_ONLY) and CONF_UPDATE_INTERVAL in value: # update_interval does not affect anything in rx-only mode, let's warn user about # that - raise cv.Invalid("update_interval has no effect in rx_only mode. Please remove it.", - path=['update_interval']) + raise cv.Invalid( + "update_interval has no effect in rx_only mode. Please remove it.", + path=["update_interval"], + ) return value -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(SDS011Component), - - cv.Optional(CONF_PM_2_5): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 1), - cv.Optional(CONF_PM_10_0): - sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 1), - - cv.Optional(CONF_RX_ONLY, default=False): cv.boolean, - cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_minutes, -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA), validate_sds011_rx_mode) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SDS011Component), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + 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_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, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + validate_sds011_rx_mode, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) if CONF_UPDATE_INTERVAL in config: cg.add(var.set_update_interval_min(config[CONF_UPDATE_INTERVAL])) cg.add(var.set_rx_mode_only(config[CONF_RX_ONLY])) if CONF_PM_2_5 in config: - sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + sens = await sensor.new_sensor(config[CONF_PM_2_5]) cg.add(var.set_pm_2_5_sensor(sens)) if CONF_PM_10_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) 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..e698255c25 --- /dev/null +++ b/esphome/components/selec_meter/sensor.py @@ -0,0 +1,165 @@ +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, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_APPARENT_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, + accuracy_decimals=2, + 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, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + 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, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + 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..c15036e9f9 --- /dev/null +++ b/esphome/components/select/__init__.py @@ -0,0 +1,94 @@ +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_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), + cv.GenerateID(): cv.declare_id(Select), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger), + } + ), + } +) + + +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, cg.std_string) + 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 8b41a441ad..50b9e01f17 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -4,22 +4,41 @@ namespace esphome { namespace senseair { -static const char *TAG = "senseair"; +static const char *const TAG = "senseair"; static const uint8_t SENSEAIR_REQUEST_LENGTH = 8; -static const uint8_t SENSEAIR_RESPONSE_LENGTH = 13; -static const uint8_t SENSEAIR_COMMAND_GET_PPM[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6}; +static const uint8_t SENSEAIR_PPM_STATUS_RESPONSE_LENGTH = 13; +static const uint8_t SENSEAIR_ABC_PERIOD_RESPONSE_LENGTH = 7; +static const uint8_t SENSEAIR_CAL_RESULT_RESPONSE_LENGTH = 7; +static const uint8_t SENSEAIR_COMMAND_GET_PPM_STATUS[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6}; +static const uint8_t SENSEAIR_COMMAND_CLEAR_ACK_REGISTER[] = {0xFE, 0x06, 0x00, 0x00, 0x00, 0x00, 0x9D, 0xC5}; +static const uint8_t SENSEAIR_COMMAND_BACKGROUND_CAL[] = {0xFE, 0x06, 0x00, 0x01, 0x7C, 0x06, 0x6C, 0xC7}; +static const uint8_t SENSEAIR_COMMAND_BACKGROUND_CAL_RESULT[] = {0xFE, 0x03, 0x00, 0x00, 0x00, 0x01, 0x90, 0x05}; +static const uint8_t SENSEAIR_COMMAND_ABC_ENABLE[] = {0xFE, 0x06, 0x00, 0x1F, 0x00, 0xB4, 0xAC, 0x74}; // 180 hours +static const uint8_t SENSEAIR_COMMAND_ABC_DISABLE[] = {0xFE, 0x06, 0x00, 0x1F, 0x00, 0x00, 0xAC, 0x03}; +static const uint8_t SENSEAIR_COMMAND_ABC_GET_PERIOD[] = {0xFE, 0x03, 0x00, 0x1F, 0x00, 0x01, 0xA1, 0xC3}; void SenseAirComponent::update() { - uint8_t response[SENSEAIR_RESPONSE_LENGTH]; - if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM, response)) { + uint8_t response[SENSEAIR_PPM_STATUS_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM_STATUS, response, SENSEAIR_PPM_STATUS_RESPONSE_LENGTH)) { ESP_LOGW(TAG, "Reading data from SenseAir failed!"); this->status_set_warning(); return; } if (response[0] != 0xFE || response[1] != 0x04) { - ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + ESP_LOGW(TAG, "Invalid preamble from SenseAir! %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x", + response[0], response[1], response[2], response[3], response[4], response[5], response[6], response[7], + response[8], response[9], response[10], response[11], response[12]); + this->status_set_warning(); + while (this->available()) { + uint8_t b; + if (this->read_byte(&b)) { + ESP_LOGV(TAG, " ... %02x", b); + } else { + ESP_LOGV(TAG, " ... nothing read"); + } + } return; } @@ -58,14 +77,81 @@ uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { return crc; } -bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response) { +void SenseAirComponent::background_calibration() { + // Responses are just echoes but must be read to clear the buffer + ESP_LOGD(TAG, "SenseAir Starting background calibration"); + 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() { + ESP_LOGD(TAG, "SenseAir Requesting background calibration result"); + uint8_t response[SENSEAIR_CAL_RESULT_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_BACKGROUND_CAL_RESULT, response, + SENSEAIR_CAL_RESULT_RESPONSE_LENGTH)) { + ESP_LOGE(TAG, "Requesting background calibration result from SenseAir failed!"); + return; + } + + if (response[0] != 0xFE || response[1] != 0x03) { + ESP_LOGE(TAG, "Invalid reply from SenseAir! %02x%02x%02x %02x%02x %02x%02x", response[0], response[1], response[2], + response[3], response[4], response[5], response[6]); + return; + } + + // 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"); + 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"); + 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() { + ESP_LOGD(TAG, "SenseAir Requesting ABC period"); + uint8_t response[SENSEAIR_ABC_PERIOD_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_ABC_GET_PERIOD, response, SENSEAIR_ABC_PERIOD_RESPONSE_LENGTH)) { + ESP_LOGE(TAG, "Requesting ABC period from SenseAir failed!"); + return; + } + + if (response[0] != 0xFE || response[1] != 0x03) { + ESP_LOGE(TAG, "Invalid reply from SenseAir! %02x%02x%02x %02x%02x %02x%02x", response[0], response[1], response[2], + response[3], response[4], response[5], response[6]); + return; + } + + const uint16_t hours = (uint16_t(response[3]) << 8) | response[4]; + ESP_LOGD(TAG, "SenseAir Read ABC Period: %u hours", hours); +} + +bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response, uint8_t response_length) { + // Verify we have somewhere to store the response + if (response == nullptr) { + return false; + } + // Write wake up byte required by some S8 sensor models + this->write_byte(0); this->flush(); + delay(5); this->write_array(command, SENSEAIR_REQUEST_LENGTH); - if (response == nullptr) - return true; - - bool ret = this->read_array(response, SENSEAIR_RESPONSE_LENGTH); + bool ret = this->read_array(response, response_length); this->flush(); return ret; } diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index 23bcf40b5a..c03a0848e9 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" @@ -15,12 +16,68 @@ class SenseAirComponent : public PollingComponent, public uart::UARTDevice { void update() override; void dump_config() override; + void background_calibration(); + void background_calibration_result(); + void abc_get_period(); + void abc_enable(); + void abc_disable(); + protected: uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length); - bool senseair_write_command_(const uint8_t *command, uint8_t *response); + bool senseair_write_command_(const uint8_t *command, uint8_t *response, uint8_t response_length); sensor::Sensor *co2_sensor_{nullptr}; }; +template class SenseAirBackgroundCalibrationAction : public Action { + public: + SenseAirBackgroundCalibrationAction(SenseAirComponent *senseair) : senseair_(senseair) {} + + void play(Ts... x) override { this->senseair_->background_calibration(); } + + protected: + SenseAirComponent *senseair_; +}; + +template class SenseAirBackgroundCalibrationResultAction : public Action { + public: + SenseAirBackgroundCalibrationResultAction(SenseAirComponent *senseair) : senseair_(senseair) {} + + void play(Ts... x) override { this->senseair_->background_calibration_result(); } + + protected: + SenseAirComponent *senseair_; +}; + +template class SenseAirABCEnableAction : public Action { + public: + SenseAirABCEnableAction(SenseAirComponent *senseair) : senseair_(senseair) {} + + void play(Ts... x) override { this->senseair_->abc_enable(); } + + protected: + SenseAirComponent *senseair_; +}; + +template class SenseAirABCDisableAction : public Action { + public: + SenseAirABCDisableAction(SenseAirComponent *senseair) : senseair_(senseair) {} + + void play(Ts... x) override { this->senseair_->abc_disable(); } + + protected: + SenseAirComponent *senseair_; +}; + +template class SenseAirABCGetPeriodAction : public Action { + public: + SenseAirABCGetPeriodAction(SenseAirComponent *senseair) : senseair_(senseair) {} + + void play(Ts... x) override { this->senseair_->abc_get_period(); } + + protected: + SenseAirComponent *senseair_; +}; + } // namespace senseair } // namespace esphome diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 393bfd5182..d423793873 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -1,24 +1,93 @@ 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 sensor, uart -from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION +from esphome.const import ( + CONF_CO2, + CONF_ID, + ICON_MOLECULE_CO2, + DEVICE_CLASS_CARBON_DIOXIDE, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -senseair_ns = cg.esphome_ns.namespace('senseair') -SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, uart.UARTDevice) +senseair_ns = cg.esphome_ns.namespace("senseair") +SenseAirComponent = senseair_ns.class_( + "SenseAirComponent", cg.PollingComponent, uart.UARTDevice +) +SenseAirBackgroundCalibrationAction = senseair_ns.class_( + "SenseAirBackgroundCalibrationAction", automation.Action +) +SenseAirBackgroundCalibrationResultAction = senseair_ns.class_( + "SenseAirBackgroundCalibrationResultAction", automation.Action +) +SenseAirABCEnableAction = senseair_ns.class_( + "SenseAirABCEnableAction", automation.Action +) +SenseAirABCDisableAction = senseair_ns.class_( + "SenseAirABCDisableAction", automation.Action +) +SenseAirABCGetPeriodAction = senseair_ns.class_( + "SenseAirABCGetPeriodAction", automation.Action +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SenseAirComponent), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), -}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SenseAirComponent), + cv.Required(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, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) if CONF_CO2 in config: - sens = yield sensor.new_sensor(config[CONF_CO2]) + sens = await sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) + + +CALIBRATION_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SenseAirComponent), + } +) + + +@automation.register_action( + "senseair.background_calibration", + SenseAirBackgroundCalibrationAction, + CALIBRATION_ACTION_SCHEMA, +) +@automation.register_action( + "senseair.background_calibration_result", + SenseAirBackgroundCalibrationResultAction, + CALIBRATION_ACTION_SCHEMA, +) +@automation.register_action( + "senseair.abc_enable", SenseAirABCEnableAction, CALIBRATION_ACTION_SCHEMA +) +@automation.register_action( + "senseair.abc_disable", SenseAirABCDisableAction, CALIBRATION_ACTION_SCHEMA +) +@automation.register_action( + "senseair.abc_get_period", SenseAirABCGetPeriodAction, CALIBRATION_ACTION_SCHEMA +) +async def senseair_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 605f72a103..14a15da2f1 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -4,14 +4,103 @@ 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_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, \ - CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, CONF_ICON, CONF_ID, CONF_INTERNAL, \ - CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, \ - CONF_TO, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID, \ - CONF_FORCE_UPDATE -from esphome.core import CORE, coroutine, coroutine_with_priority +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ABOVE, + CONF_ACCURACY_DECIMALS, + CONF_ALPHA, + CONF_BELOW, + CONF_ENTITY_CATEGORY, + CONF_EXPIRE_AFTER, + CONF_FILTERS, + CONF_FROM, + CONF_ICON, + CONF_ID, + CONF_ON_RAW_VALUE, + CONF_ON_VALUE, + CONF_ON_VALUE_RANGE, + CONF_QUANTILE, + CONF_SEND_EVERY, + CONF_SEND_FIRST_AT, + CONF_STATE_CLASS, + CONF_TO, + CONF_TRIGGER_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_WINDOW_SIZE, + CONF_MQTT_ID, + CONF_FORCE_UPDATE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, + DEVICE_CLASS_BATTERY, + 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_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, +) +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_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + 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, +] + +sensor_ns = cg.esphome_ns.namespace("sensor") +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="_") + IS_PLATFORM_COMPONENT = True @@ -19,270 +108,470 @@ def validate_send_first_at(value): send_first_at = value.get(CONF_SEND_FIRST_AT) 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)) + raise cv.Invalid( + f"send_first_at must be smaller than or equal to send_every! {send_first_at} <= {send_every}" + ) return value FILTER_REGISTRY = Registry() -validate_filters = cv.validate_registry('filter', FILTER_REGISTRY) +validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) def validate_datapoint(value): if isinstance(value, dict): - return cv.Schema({ - cv.Required(CONF_FROM): cv.float_, - cv.Required(CONF_TO): cv.float_, - })(value) + return cv.Schema( + { + cv.Required(CONF_FROM): cv.float_, + cv.Required(CONF_TO): cv.float_, + } + )(value) value = cv.string(value) - if '->' not in value: + if "->" not in value: raise cv.Invalid("Datapoint mapping must contain '->'") - a, b = value.split('->', 1) + a, b = value.split("->", 1) a, b = a.strip(), b.strip() - return validate_datapoint({ - CONF_FROM: cv.float_(a), - CONF_TO: cv.float_(b) - }) + return validate_datapoint({CONF_FROM: cv.float_(a), CONF_TO: cv.float_(b)}) # Base -sensor_ns = cg.esphome_ns.namespace('sensor') -Sensor = sensor_ns.class_('Sensor', cg.Nameable) -SensorPtr = Sensor.operator('ptr') +Sensor = sensor_ns.class_("Sensor", cg.EntityBase) +SensorPtr = Sensor.operator("ptr") # Triggers -SensorStateTrigger = sensor_ns.class_('SensorStateTrigger', automation.Trigger.template(cg.float_)) -SensorRawStateTrigger = sensor_ns.class_('SensorRawStateTrigger', - automation.Trigger.template(cg.float_)) -ValueRangeTrigger = sensor_ns.class_('ValueRangeTrigger', automation.Trigger.template(cg.float_), - cg.Component) -SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action) +SensorStateTrigger = sensor_ns.class_( + "SensorStateTrigger", automation.Trigger.template(cg.float_) +) +SensorRawStateTrigger = sensor_ns.class_( + "SensorRawStateTrigger", automation.Trigger.template(cg.float_) +) +ValueRangeTrigger = sensor_ns.class_( + "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component +) +SensorPublishAction = sensor_ns.class_("SensorPublishAction", automation.Action) # Filters -Filter = sensor_ns.class_('Filter') -MedianFilter = sensor_ns.class_('MedianFilter', Filter) -SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter) -ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter) -LambdaFilter = sensor_ns.class_('LambdaFilter', Filter) -OffsetFilter = sensor_ns.class_('OffsetFilter', Filter) -MultiplyFilter = sensor_ns.class_('MultiplyFilter', Filter) -FilterOutValueFilter = sensor_ns.class_('FilterOutValueFilter', Filter) -ThrottleFilter = sensor_ns.class_('ThrottleFilter', Filter) -DebounceFilter = sensor_ns.class_('DebounceFilter', Filter, cg.Component) -HeartbeatFilter = sensor_ns.class_('HeartbeatFilter', Filter, cg.Component) -DeltaFilter = sensor_ns.class_('DeltaFilter', Filter) -OrFilter = sensor_ns.class_('OrFilter', Filter) -CalibrateLinearFilter = sensor_ns.class_('CalibrateLinearFilter', Filter) -CalibratePolynomialFilter = sensor_ns.class_('CalibratePolynomialFilter', Filter) -SensorInRangeCondition = sensor_ns.class_('SensorInRangeCondition', Filter) +Filter = sensor_ns.class_("Filter") +QuantileFilter = sensor_ns.class_("QuantileFilter", Filter) +MedianFilter = sensor_ns.class_("MedianFilter", Filter) +MinFilter = sensor_ns.class_("MinFilter", Filter) +MaxFilter = sensor_ns.class_("MaxFilter", Filter) +SlidingWindowMovingAverageFilter = sensor_ns.class_( + "SlidingWindowMovingAverageFilter", Filter +) +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) +FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) +ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) +DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) +HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) +DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) +OrFilter = sensor_ns.class_("OrFilter", Filter) +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 +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({ - 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_FORCE_UPDATE, default=False): cv.boolean, - cv.Optional(CONF_EXPIRE_AFTER): cv.All(cv.requires_component('mqtt'), - cv.Any(None, cv.positive_time_period_milliseconds)), - cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorStateTrigger), - }), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorRawStateTrigger), - }), - cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.float_, - cv.Optional(CONF_BELOW): cv.float_, - }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW)), -}) +SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), + cv.GenerateID(): cv.declare_id(Sensor), + cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_STATE_CLASS): validate_state_class, + cv.Optional("last_reset_type"): cv.invalid( + "last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values." + ), + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, + cv.Optional(CONF_EXPIRE_AFTER): cv.All( + cv.requires_component("mqtt"), + cv.Any(None, cv.positive_time_period_milliseconds), + ), + cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorStateTrigger), + } + ), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorRawStateTrigger), + } + ), + cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), + cv.Optional(CONF_ABOVE): cv.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), + ), + } +) + +_UNDEF = object() -def sensor_schema(unit_of_measurement_, icon_, accuracy_decimals_): - # type: (str, str, int) -> cv.Schema - return SENSOR_SCHEMA.extend({ - cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement_): unit_of_measurement, - cv.Optional(CONF_ICON, default=icon_): icon, - cv.Optional(CONF_ACCURACY_DECIMALS, default=accuracy_decimals_): accuracy_decimals, - }) +def sensor_schema( + unit_of_measurement: str = _UNDEF, + icon: str = _UNDEF, + accuracy_decimals: int = _UNDEF, + device_class: str = _UNDEF, + state_class: str = _UNDEF, + entity_category: str = _UNDEF, +) -> cv.Schema: + schema = SENSOR_SCHEMA + if unit_of_measurement is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement + ): validate_unit_of_measurement + } + ) + 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 + ): validate_accuracy_decimals, + } + ) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) + if state_class is not _UNDEF: + schema = schema.extend( + {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} + ) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + return schema -@FILTER_REGISTRY.register('offset', OffsetFilter, cv.float_) -def offset_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config) +@FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_) +async def offset_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) -@FILTER_REGISTRY.register('multiply', MultiplyFilter, cv.float_) -def multiply_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config) +@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.float_) +async def multiply_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) -@FILTER_REGISTRY.register('filter_out', FilterOutValueFilter, cv.float_) -def filter_out_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config) +@FILTER_REGISTRY.register("filter_out", FilterOutValueFilter, cv.float_) +async def filter_out_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) -MEDIAN_SCHEMA = cv.All(cv.Schema({ - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, -}), validate_send_first_at) +QUANTILE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float, + } + ), + validate_send_first_at, +) -@FILTER_REGISTRY.register('median', MedianFilter, MEDIAN_SCHEMA) -def median_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY], - config[CONF_SEND_FIRST_AT]) +@FILTER_REGISTRY.register("quantile", QuantileFilter, QUANTILE_SCHEMA) +async def quantile_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_WINDOW_SIZE], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + config[CONF_QUANTILE], + ) -SLIDING_AVERAGE_SCHEMA = cv.All(cv.Schema({ - cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, -}), validate_send_first_at) +MEDIAN_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + } + ), + validate_send_first_at, +) -@FILTER_REGISTRY.register('sliding_window_moving_average', SlidingWindowMovingAverageFilter, - SLIDING_AVERAGE_SCHEMA) -def sliding_window_moving_average_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY], - config[CONF_SEND_FIRST_AT]) +@FILTER_REGISTRY.register("median", MedianFilter, MEDIAN_SCHEMA) +async def median_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_WINDOW_SIZE], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + ) -@FILTER_REGISTRY.register('exponential_moving_average', ExponentialMovingAverageFilter, cv.Schema({ - cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float, - cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, -})) -def exponential_moving_average_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config[CONF_ALPHA], config[CONF_SEND_EVERY]) +MIN_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + } + ), + validate_send_first_at, +) -@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.returning_lambda) -def lambda_filter_to_code(config, filter_id): - lambda_ = yield cg.process_lambda(config, [(float, 'x')], - return_type=cg.optional.template(float)) - yield cg.new_Pvariable(filter_id, lambda_) +@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA) +async def min_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_WINDOW_SIZE], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + ) -@FILTER_REGISTRY.register('delta', DeltaFilter, cv.float_) -def delta_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config) +MAX_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + } + ), + validate_send_first_at, +) -@FILTER_REGISTRY.register('or', OrFilter, validate_filters) -def or_filter_to_code(config, filter_id): - filters = yield build_filters(config) - yield cg.new_Pvariable(filter_id, filters) +@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA) +async def max_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_WINDOW_SIZE], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + ) -@FILTER_REGISTRY.register('throttle', ThrottleFilter, cv.positive_time_period_milliseconds) -def throttle_filter_to_code(config, filter_id): - yield cg.new_Pvariable(filter_id, config) +SLIDING_AVERAGE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + } + ), + validate_send_first_at, +) -@FILTER_REGISTRY.register('heartbeat', HeartbeatFilter, cv.positive_time_period_milliseconds) -def heartbeat_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register( + "sliding_window_moving_average", + SlidingWindowMovingAverageFilter, + SLIDING_AVERAGE_SCHEMA, +) +async def sliding_window_moving_average_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_WINDOW_SIZE], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + ) + + +@FILTER_REGISTRY.register( + "exponential_moving_average", + ExponentialMovingAverageFilter, + cv.Schema( + { + cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float, + cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, + } + ), +) +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) - yield cg.register_component(var, {}) - yield var + await cg.register_component(var, {}) + return var -@FILTER_REGISTRY.register('debounce', DebounceFilter, cv.positive_time_period_milliseconds) -def debounce_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) +async def lambda_filter_to_code(config, filter_id): + lambda_ = await cg.process_lambda( + config, [(float, "x")], return_type=cg.optional.template(float) + ) + return cg.new_Pvariable(filter_id, lambda_) + + +@FILTER_REGISTRY.register("delta", DeltaFilter, cv.float_) +async def delta_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) + + +@FILTER_REGISTRY.register("or", OrFilter, validate_filters) +async def or_filter_to_code(config, filter_id): + filters = await build_filters(config) + return cg.new_Pvariable(filter_id, filters) + + +@FILTER_REGISTRY.register( + "throttle", ThrottleFilter, cv.positive_time_period_milliseconds +) +async def throttle_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) + + +@FILTER_REGISTRY.register( + "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds +) +async def heartbeat_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id, config) - yield cg.register_component(var, {}) - yield var + await cg.register_component(var, {}) + return var + + +@FILTER_REGISTRY.register( + "debounce", DebounceFilter, cv.positive_time_period_milliseconds +) +async def debounce_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id, config) + await cg.register_component(var, {}) + return var def validate_not_all_from_same(config): if all(conf[CONF_FROM] == config[0][CONF_FROM] for conf in config): - raise cv.Invalid("The 'from' values of the calibrate_linear filter cannot all point " - "to the same value! Please add more values to the filter.") + raise cv.Invalid( + "The 'from' values of the calibrate_linear filter cannot all point " + "to the same value! Please add more values to the filter." + ) return config -@FILTER_REGISTRY.register('calibrate_linear', CalibrateLinearFilter, cv.All( - cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same)) -def calibrate_linear_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register( + "calibrate_linear", + CalibrateLinearFilter, + cv.All( + cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same + ), +) +async def calibrate_linear_filter_to_code(config, filter_id): x = [conf[CONF_FROM] for conf in config] y = [conf[CONF_TO] for conf in config] k, b = fit_linear(x, y) - yield cg.new_Pvariable(filter_id, k, b) + return cg.new_Pvariable(filter_id, k, b) -CONF_DATAPOINTS = 'datapoints' -CONF_DEGREE = 'degree' +CONF_DATAPOINTS = "datapoints" +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), [CONF_DEGREE]) + raise cv.Invalid( + f"Degree is too high! Maximum possible degree with given datapoints is {len(config[CONF_DATAPOINTS]) - 1}", + [CONF_DEGREE], + ) return config -@FILTER_REGISTRY.register('calibrate_polynomial', CalibratePolynomialFilter, cv.All(cv.Schema({ - cv.Required(CONF_DATAPOINTS): cv.All(cv.ensure_list(validate_datapoint), cv.Length(min=1)), - cv.Required(CONF_DEGREE): cv.positive_int, -}), validate_calibrate_polynomial)) -def calibrate_polynomial_filter_to_code(config, filter_id): +@FILTER_REGISTRY.register( + "calibrate_polynomial", + CalibratePolynomialFilter, + cv.All( + cv.Schema( + { + cv.Required(CONF_DATAPOINTS): cv.All( + cv.ensure_list(validate_datapoint), cv.Length(min=1) + ), + cv.Required(CONF_DEGREE): cv.positive_int, + } + ), + validate_calibrate_polynomial, + ), +) +async def calibrate_polynomial_filter_to_code(config, filter_id): x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]] y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]] degree = config[CONF_DEGREE] - a = [[1] + [x_**(i+1) for i in range(degree)] for x_ in x] + a = [[1] + [x_ ** (i + 1) for i in range(degree)] for x_ in x] # Column vector b = [[v] for v in y] res = [v[0] for v in _lstsq(a, b)] - yield cg.new_Pvariable(filter_id, res) + return cg.new_Pvariable(filter_id, res) -@coroutine -def build_filters(config): - yield cg.build_registry_list(FILTER_REGISTRY, config) +async def build_filters(config): + return await cg.build_registry_list(FILTER_REGISTRY, config) -@coroutine -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])) +async def setup_sensor_core_(var, config): + await setup_entity(var, config) + + if CONF_DEVICE_CLASS in config: + cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + if CONF_STATE_CLASS in config: + cg.add(var.set_state_class(config[CONF_STATE_CLASS])) if CONF_UNIT_OF_MEASUREMENT in config: cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) - if CONF_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])) if config.get(CONF_FILTERS): # must exist and not be empty - filters = yield build_filters(config[CONF_FILTERS]) + 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) - yield automation.build_automation(trigger, [(float, 'x')], conf) + await automation.build_automation(trigger, [(float, "x")], conf) for conf in config.get(CONF_ON_RAW_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [(float, 'x')], conf) + 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) - yield cg.register_component(trigger, conf) + await cg.register_component(trigger, conf) if CONF_ABOVE in conf: - template_ = yield cg.templatable(conf[CONF_ABOVE], [(float, 'x')], float) + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) cg.add(trigger.set_min(template_)) if CONF_BELOW in conf: - template_ = yield cg.templatable(conf[CONF_BELOW], [(float, 'x')], float) + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) cg.add(trigger.set_max(template_)) - yield automation.build_automation(trigger, [(float, 'x')], conf) + await automation.build_automation(trigger, [(float, "x")], conf) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) - yield mqtt.register_mqtt_component(mqtt_, config) + await mqtt.register_mqtt_component(mqtt_, config) if CONF_EXPIRE_AFTER in config: if config[CONF_EXPIRE_AFTER] is None: @@ -291,32 +580,34 @@ def setup_sensor_core_(var, config): cg.add(mqtt_.set_expire_after(config[CONF_EXPIRE_AFTER])) -@coroutine -def register_sensor(var, config): +async def register_sensor(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_sensor(var)) - yield setup_sensor_core_(var, config) + await setup_sensor_core_(var, config) -@coroutine -def new_sensor(config): +async def new_sensor(config): var = cg.new_Pvariable(config[CONF_ID]) - yield register_sensor(var, config) - yield var + await register_sensor(var, config) + return var -SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All({ - cv.Required(CONF_ID): cv.use_id(Sensor), - cv.Optional(CONF_ABOVE): cv.float_, - cv.Optional(CONF_BELOW): cv.float_, -}, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW)) +SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(Sensor), + 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('sensor.in_range', SensorInRangeCondition, - SENSOR_IN_RANGE_CONDITION_SCHEMA) -def sensor_in_range_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_condition( + "sensor.in_range", SensorInRangeCondition, SENSOR_IN_RANGE_CONDITION_SCHEMA +) +async def sensor_in_range_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(condition_id, template_arg, paren) if CONF_ABOVE in config: @@ -324,7 +615,7 @@ def sensor_in_range_to_code(config, condition_id, template_arg, args): if CONF_BELOW in config: cg.add(var.set_max(config[CONF_BELOW])) - yield var + return var def _mean(xs): @@ -366,7 +657,7 @@ def _mat_identity(n): def _mat_dot(a, b): b_t = _mat_transpose(b) - return [[sum(x*y for x, y in zip(row_a, col_b)) for col_b in b_t] for row_a in a] + return [[sum(x * y for x, y in zip(row_a, col_b)) for col_b in b_t] for row_a in a] def _mat_inverse(m): @@ -377,7 +668,7 @@ def _mat_inverse(m): for diag in range(n): # If diag element is 0, swap rows if m[diag][diag] == 0: - for i in range(diag+1, n): + for i in range(diag + 1, n): if m[i][diag] != 0: break else: @@ -413,6 +704,6 @@ def _lstsq(a, b): @coroutine_with_priority(40.0) -def to_code(config): - cg.add_define('USE_SENSOR') +async def to_code(config): + cg.add_define("USE_SENSOR") cg.add_global(sensor_ns.using) diff --git a/esphome/components/sensor/automation.cpp b/esphome/components/sensor/automation.cpp index 1e8f3f4c3e..f53c43d1f6 100644 --- a/esphome/components/sensor/automation.cpp +++ b/esphome/components/sensor/automation.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sensor { -static const char *TAG = "sensor.automation"; +static const char *const TAG = "sensor.automation"; } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 079077dba0..8cd0adbeb2 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -25,6 +25,7 @@ template class SensorPublishAction : public Action { public: SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: @@ -39,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; @@ -51,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; @@ -91,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 f7a5b5d7ad..7a8a557273 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,14 +1,15 @@ #include "filter.h" -#include "sensor.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "sensor.h" +#include namespace esphome { namespace sensor { -static const char *TAG = "sensor.filter"; +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 +30,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 +37,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,7 +67,98 @@ optional MedianFilter::new_value(float value) { return {}; } -uint32_t MedianFilter::expected_interval(uint32_t input) { return input * this->send_every_; } +// QuantileFilter +QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile) + : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {} +void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; } +optional QuantileFilter::new_value(float value) { + if (!std::isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float result = 0.0f; + if (!this->queue_.empty()) { + std::deque quantile_queue = this->queue_; + sort(quantile_queue.begin(), quantile_queue.end()); + + size_t queue_size = quantile_queue.size(); + size_t position = ceilf(queue_size * this->quantile_) - 1; + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position, queue_size); + result = quantile_queue[position]; + } + + ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING", this, result); + return result; + } + return {}; +} + +// 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 (!std::isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float min = 0.0f; + if (!this->queue_.empty()) { + std::deque::iterator it = std::min_element(queue_.begin(), queue_.end()); + min = *it; + } + + ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING", this, min); + return min; + } + return {}; +} + +// 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 (!std::isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float max = 0.0f; + if (!this->queue_.empty()) { + std::deque::iterator it = std::max_element(queue_.begin(), queue_.end()); + max = *it; + } + + ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING", this, max); + return max; + } + return {}; +} // SlidingWindowMovingAverageFilter SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, @@ -84,12 +167,12 @@ 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_.front(); - this->queue_.pop(); + this->sum_ -= this->queue_[0]; + this->queue_.pop_front(); } - this->queue_.push(value); + this->queue_.push_back(value); this->sum_ += value; } float average; @@ -99,21 +182,27 @@ optional SlidingWindowMovingAverageFilter::new_value(float value) { average = this->sum_ / this->queue_.size(); ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) -> %f", this, value, average); - if (++this->send_at_ >= this->send_every_) { - this->send_at_ = 0; + if (++this->send_at_ % this->send_every_ == 0) { + if (this->send_at_ >= 10000) { + // Recalculate to prevent floating point error accumulating + this->sum_ = 0; + for (auto v : this->queue_) + this->sum_ += v; + average = this->sum_ / this->queue_.size(); + this->send_at_ = 0; + } + ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING", this, value); return average; } 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 @@ -133,7 +222,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)) {} @@ -160,14 +273,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) @@ -192,9 +305,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_) { @@ -226,14 +339,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); }); @@ -254,7 +359,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 4c61d4c0a2..0ed7ce4801 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -1,8 +1,9 @@ #pragma once -#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include +#include namespace esphome { namespace sensor { @@ -32,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: @@ -46,6 +42,37 @@ class Filter { Sensor *parent_{nullptr}; }; +/** Simple quantile filter. + * + * Takes the quantile of the last values and pushes it out every . + */ +class QuantileFilter : public Filter { + public: + /** Construct a QuantileFilter. + * + * @param window_size The number of values that should be used in quantile calculation. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + * @param quantile float 0..1 to pick the requested quantile. Defaults to 0.9. + */ + explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + void set_quantile(float quantile); + + protected: + std::deque queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; + float quantile_; +}; + /** Simple median filter. * * Takes the median of the last values and pushes it out every . @@ -67,7 +94,61 @@ 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_; + size_t send_at_; + size_t window_size_; +}; + +/** Simple min filter. + * + * Takes the min of the last values and pushes it out every . + */ +class MinFilter : public Filter { + public: + /** Construct a MinFilter. + * + * @param window_size The number of values that the min should be returned from. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + */ + explicit MinFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + + protected: + std::deque queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; +}; + +/** Simple max filter. + * + * Takes the max of the last values and pushes it out every . + */ +class MaxFilter : public Filter { + public: + /** Construct a MaxFilter. + * + * @param window_size The number of values that the max should be returned from. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + */ + explicit MaxFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); protected: std::deque queue_; @@ -98,11 +179,9 @@ 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::queue queue_; + std::deque queue_; size_t send_every_; size_t send_at_; size_t window_size_; @@ -122,8 +201,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}; @@ -132,6 +209,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. @@ -218,8 +315,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: @@ -245,8 +340,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: @@ -275,7 +368,7 @@ class CalibrateLinearFilter : public Filter { class CalibratePolynomialFilter : public Filter { public: - CalibratePolynomialFilter(const std::vector &coefficients) : coefficients_(coefficients) {} + CalibratePolynomialFilter(std::vector coefficients) : coefficients_(std::move(coefficients)) {} optional new_value(float value) override; protected: diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index e12e55e320..793ae170c3 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -4,7 +4,56 @@ namespace esphome { namespace sensor { -static const char *TAG = "sensor"; +static const char *const TAG = "sensor"; + +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; @@ -18,38 +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(); -} -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 @@ -79,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 ""; } @@ -93,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 f23f022767..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" @@ -8,50 +10,76 @@ namespace esphome { 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 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()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ - if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ + 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()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ - if (obj->get_force_update()) { \ + if (!(obj)->unique_id().empty()) { \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, (obj)->unique_id().c_str()); \ + } \ + if ((obj)->get_force_update()) { \ ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ } \ } +/** + * Sensor state classes + */ +enum StateClass : uint8_t { + STATE_CLASS_NONE = 0, + STATE_CLASS_MEASUREMENT = 1, + STATE_CLASS_TOTAL_INCREASING = 2, +}; + +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); @@ -73,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 @@ -100,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. @@ -122,8 +131,9 @@ class Sensor : public Nameable { */ float state; - /** 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; @@ -137,63 +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 + /// 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/__init__.py b/esphome/components/servo/__init__.py index 9b06159c13..7147828a07 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -3,52 +3,83 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components.output import FloatOutput -from esphome.const import CONF_ID, CONF_IDLE_LEVEL, CONF_MAX_LEVEL, CONF_MIN_LEVEL, CONF_OUTPUT, \ - CONF_LEVEL, CONF_RESTORE +from esphome.const import ( + CONF_ID, + CONF_IDLE_LEVEL, + CONF_MAX_LEVEL, + CONF_MIN_LEVEL, + CONF_OUTPUT, + CONF_LEVEL, + CONF_RESTORE, + CONF_TRANSITION_LENGTH, +) -servo_ns = cg.esphome_ns.namespace('servo') -Servo = servo_ns.class_('Servo', cg.Component) -ServoWriteAction = servo_ns.class_('ServoWriteAction', automation.Action) -ServoDetachAction = servo_ns.class_('ServoDetachAction', automation.Action) +servo_ns = cg.esphome_ns.namespace("servo") +Servo = servo_ns.class_("Servo", cg.Component) +ServoWriteAction = servo_ns.class_("ServoWriteAction", automation.Action) +ServoDetachAction = servo_ns.class_("ServoDetachAction", automation.Action) +CONF_AUTO_DETACH_TIME = "auto_detach_time" MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.declare_id(Servo), - cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), - cv.Optional(CONF_MIN_LEVEL, default='3%'): cv.percentage, - cv.Optional(CONF_IDLE_LEVEL, default='7.5%'): cv.percentage, - cv.Optional(CONF_MAX_LEVEL, default='12%'): cv.percentage, - cv.Optional(CONF_RESTORE, default=False): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(Servo), + cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), + cv.Optional(CONF_MIN_LEVEL, default="3%"): cv.percentage, + cv.Optional(CONF_IDLE_LEVEL, default="7.5%"): cv.percentage, + cv.Optional(CONF_MAX_LEVEL, default="12%"): cv.percentage, + cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional( + CONF_AUTO_DETACH_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - out = yield cg.get_variable(config[CONF_OUTPUT]) + out = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(out)) cg.add(var.set_min_level(config[CONF_MIN_LEVEL])) cg.add(var.set_idle_level(config[CONF_IDLE_LEVEL])) cg.add(var.set_max_level(config[CONF_MAX_LEVEL])) cg.add(var.set_restore(config[CONF_RESTORE])) + cg.add(var.set_auto_detach_time(config[CONF_AUTO_DETACH_TIME])) + cg.add(var.set_transition_length(config[CONF_TRANSITION_LENGTH])) -@automation.register_action('servo.write', ServoWriteAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(Servo), - cv.Required(CONF_LEVEL): cv.templatable(cv.possibly_negative_percentage), -})) -def servo_write_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "servo.write", + ServoWriteAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Servo), + cv.Required(CONF_LEVEL): cv.templatable(cv.possibly_negative_percentage), + } + ), +) +async def servo_write_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_ = yield cg.templatable(config[CONF_LEVEL], args, float) + template_ = await cg.templatable(config[CONF_LEVEL], args, float) cg.add(var.set_value(template_)) - yield var + return var -@automation.register_action('servo.detach', ServoDetachAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Servo), -})) -def servo_detach_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) +@automation.register_action( + "servo.detach", + ServoDetachAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Servo), + } + ), +) +async def servo_detach_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index 2fade280ee..2e1ba587a2 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -1,18 +1,79 @@ #include "servo.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace servo { -static const char *TAG = "servo"; +static const char *const TAG = "servo"; -uint32_t global_servo_id = 1911044085ULL; +uint32_t global_servo_id = 1911044085ULL; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void Servo::dump_config() { ESP_LOGCONFIG(TAG, "Servo:"); ESP_LOGCONFIG(TAG, " Idle Level: %.1f%%", this->idle_level_ * 100.0f); ESP_LOGCONFIG(TAG, " Min Level: %.1f%%", this->min_level_ * 100.0f); ESP_LOGCONFIG(TAG, " Max Level: %.1f%%", this->max_level_ * 100.0f); + ESP_LOGCONFIG(TAG, " auto detach time: %d ms", this->auto_detach_time_); + ESP_LOGCONFIG(TAG, " run duration: %d ms", this->transition_length_); +} + +void Servo::loop() { + // check if auto_detach_time_ is set and servo reached target + if (this->auto_detach_time_ && this->state_ == STATE_TARGET_REACHED) { + if (millis() - this->start_millis_ > this->auto_detach_time_) { + this->detach(); + this->start_millis_ = 0; + this->state_ = STATE_DETACHED; + ESP_LOGD(TAG, "Servo detached on auto_detach_time"); + } + } + if (this->target_value_ != this->current_value_ && this->state_ == STATE_ATTACHED) { + if (this->transition_length_) { + float new_value; + float travel_diff = this->target_value_ - this->source_value_; + uint32_t target_runtime = abs((int) ((travel_diff) * this->transition_length_ * 1.0f / 2.0f)); + uint32_t current_runtime = millis() - this->start_millis_; + float percentage_run = current_runtime * 1.0f / target_runtime * 1.0f; + if (percentage_run > 1.0f) { + percentage_run = 1.0f; + } + new_value = this->target_value_ - (1.0f - percentage_run) * (this->target_value_ - this->source_value_); + this->internal_write(new_value); + } else { + this->internal_write(this->target_value_); + } + } + if (this->target_value_ == this->current_value_ && this->state_ == STATE_ATTACHED) { + this->state_ = STATE_TARGET_REACHED; + this->start_millis_ = millis(); // set current stamp for potential auto_detach_time_ check + ESP_LOGD(TAG, "Servo reached target"); + } +} + +void Servo::write(float value) { + value = clamp(value, -1.0f, 1.0f); + if (this->target_value_ == value) + this->internal_write(value); + this->target_value_ = value; + this->source_value_ = this->current_value_; + this->state_ = STATE_ATTACHED; + this->start_millis_ = millis(); + ESP_LOGD(TAG, "Servo new target: %f", value); +} + +void Servo::internal_write(float value) { + value = clamp(value, -1.0f, 1.0f); + float level; + if (value < 0.0) + level = lerp(-value, this->idle_level_, this->min_level_); + else + level = lerp(value, this->idle_level_, this->max_level_); + this->output_->set_level(level); + if (this->target_value_ == this->current_value_) { + this->save_level_(level); + } + this->current_value_ = value; } } // namespace servo diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index a37188740c..e2e3823158 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -9,23 +9,14 @@ namespace esphome { namespace servo { -extern uint32_t global_servo_id; +extern uint32_t global_servo_id; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) class Servo : public Component { public: void set_output(output::FloatOutput *output) { output_ = output; } - void write(float value) { - value = clamp(value, -1.0f, 1.0f); - - float level; - if (value < 0.0) - level = lerp(-value, this->idle_level_, this->min_level_); - else - level = lerp(value, this->idle_level_, this->max_level_); - - this->output_->set_level(level); - this->save_level_(level); - } + void loop() override; + void write(float value); + void internal_write(float value); void detach() { this->output_->set_level(0.0f); this->save_level_(0.0f); @@ -33,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); @@ -48,6 +39,8 @@ class Servo : public Component { void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } void set_restore(bool restore) { restore_ = restore; } + void set_auto_detach_time(uint32_t auto_detach_time) { auto_detach_time_ = auto_detach_time; } + void set_transition_length(uint32_t transition_length) { transition_length_ = transition_length; } protected: void save_level_(float v) { this->rtc_.save(&v); } @@ -57,13 +50,26 @@ class Servo : public Component { float idle_level_ = 0.0750f; float max_level_ = 0.1200f; bool restore_{false}; + uint32_t auto_detach_time_ = 0; + uint32_t transition_length_ = 0; ESPPreferenceObject rtc_; + uint8_t state_; + float target_value_ = 0; + float source_value_ = 0; + float current_value_ = 0; + uint32_t start_millis_ = 0; + enum State { + STATE_ATTACHED = 0, + STATE_DETACHED = 1, + STATE_TARGET_REACHED = 2, + }; }; template class ServoWriteAction : public Action { public: ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) + void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } protected: @@ -73,6 +79,7 @@ template class ServoWriteAction : public Action { template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} + void play(Ts... x) override { this->servo_->detach(); } protected: diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index a52811eb34..2596e0065d 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -1,53 +1,103 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2 +from esphome.const import ( + CONF_ID, + 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, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -sgp30_ns = cg.esphome_ns.namespace('sgp30') -SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CDevice) +sgp30_ns = cg.esphome_ns.namespace("sgp30") +SGP30Component = sgp30_ns.class_("SGP30Component", cg.PollingComponent, i2c.I2CDevice) -CONF_ECO2 = 'eco2' -CONF_TVOC = 'tvoc' -CONF_BASELINE = 'baseline' -CONF_ECO2_BASELINE = 'eco2_baseline' -CONF_TVOC_BASELINE = 'tvoc_baseline' -CONF_UPTIME = 'uptime' -CONF_COMPENSATION = 'compensation' -CONF_HUMIDITY_SOURCE = 'humidity_source' -CONF_TEMPERATURE_SOURCE = 'temperature_source' +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" +CONF_TEMPERATURE_SOURCE = "temperature_source" -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SGP30Component), - cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, - ICON_PERIODIC_TABLE_CO2, 0), - cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), - cv.Optional(CONF_BASELINE): cv.Schema({ - cv.Required(CONF_ECO2_BASELINE): cv.hex_uint16_t, - cv.Required(CONF_TVOC_BASELINE): cv.hex_uint16_t, - }), - cv.Optional(CONF_COMPENSATION): cv.Schema({ - cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor) - }), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x58)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SGP30Component), + cv.Required(CONF_ECO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + 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, + cv.Required(CONF_TVOC_BASELINE): cv.hex_uint16_t, + } + ), + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), + } + ), + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(i2c.i2c_device_schema(0x58)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_ECO2 in config: - sens = yield sensor.new_sensor(config[CONF_ECO2]) + sens = await sensor.new_sensor(config[CONF_ECO2]) cg.add(var.set_eco2_sensor(sens)) if CONF_TVOC in config: - sens = yield sensor.new_sensor(config[CONF_TVOC]) + 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])) @@ -55,7 +105,7 @@ def to_code(config): if CONF_COMPENSATION in config: compensation_config = config[CONF_COMPENSATION] - sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) cg.add(var.set_humidity_sensor(sens)) - sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 8c148b8e83..4157fd55cf 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,10 +1,13 @@ #include "sgp30.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" +#include namespace esphome { namespace sgp30 { -static const char *TAG = "sgp30"; +static const char *const TAG = "sgp30"; static const uint16_t SGP30_CMD_GET_SERIAL_ID = 0x3682; static const uint16_t SGP30_CMD_GET_FEATURESET = 0x202f; @@ -16,11 +19,18 @@ static const uint16_t SGP30_CMD_SET_IAQ_BASELINE = 0x201E; // Sensor baseline should first be relied on after 1H of operation, // if the sensor starts with a baseline value provided -const long IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; +const uint32_t IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; // Sensor baseline could first be relied on after 12H of operation, // if the sensor starts without any prior baseline value provided -const long IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200; +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 +49,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 +83,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 +135,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 || + (uint32_t) abs(this->baselines_storage_.eco2 - this->eco2_baseline_) > MAXIMUM_STORAGE_DIFF || + (uint32_t) 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 +174,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 +184,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 { @@ -162,16 +212,17 @@ void SGP30Component::send_env_data_() { void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline) { uint8_t data[7]; data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; - data[1] = eco2_baseline >> 8; - data[2] = eco2_baseline & 0xFF; + data[1] = tvoc_baseline >> 8; + data[2] = tvoc_baseline & 0xFF; data[3] = sht_crc_(data[1], data[2]); - data[4] = tvoc_baseline >> 8; - data[5] = tvoc_baseline & 0xFF; + data[4] = eco2_baseline >> 8; + data[5] = eco2_baseline & 0xFF; data[6] = sht_crc_(data[4], data[5]); 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 +247,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_); @@ -204,11 +255,14 @@ void SGP30Component::dump_config() { } else { ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); } - ESP_LOGCONFIG(TAG, " Warm up time: %lds", this->required_warm_up_time_); + 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 +277,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 +293,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 +334,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 +345,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 27572e9c46..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; } @@ -33,7 +42,10 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { uint8_t sht_crc_(uint8_t data1, uint8_t data2); uint64_t serial_number_; uint16_t featureset_; - long required_warm_up_time_; + 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/__init__.py b/esphome/components/sgp40/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.cpp b/esphome/components/sgp40/sensirion_voc_algorithm.cpp new file mode 100644 index 0000000000..d76b776641 --- /dev/null +++ b/esphome/components/sgp40/sensirion_voc_algorithm.cpp @@ -0,0 +1,628 @@ + +#include "sensirion_voc_algorithm.h" + +namespace esphome { +namespace sgp40 { + +/* The VOC code were originally created by + * https://github.com/Sensirion/embedded-sgp + * The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +/*!< the maximum value of fix16_t */ +#define FIX16_MAXIMUM 0x7FFFFFFF +/*!< the minimum value of fix16_t */ +static const uint32_t FIX16_MINIMUM = 0x80000000; +/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not + * specified */ +static const uint32_t FIX16_OVERFLOW = 0x80000000; +/*!< fix16_t value of 1 */ +const uint32_t FIX16_ONE = 0x00010000; + +inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; } + +inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); } + +/*! Multiplies the two given fix16_t's and returns the result. */ +static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1); + +/*! Divides the first given fix16_t by the second and returns the result. */ +static fix16_t fix16_div(fix16_t a, fix16_t b); + +/*! Returns the square root of the given fix16_t. */ +static fix16_t fix16_sqrt(fix16_t in_value); + +/*! Returns the exponent (e^) of the given fix16_t. */ +static fix16_t fix16_exp(fix16_t in_value); + +static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) { + // Each argument is divided to 16-bit parts. + // AB + // * CD + // ----------- + // BD 16 * 16 -> 32 bit products + // CB + // AD + // AC + // |----| 64 bit product + int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16); + uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF); + + int32_t ac = a * c; + int32_t ad_cb = a * d + c * b; + uint32_t bd = b * d; + + int32_t product_hi = ac + (ad_cb >> 16); // NOLINT + + // Handle carry from lower 32 bits to upper part of result. + uint32_t ad_cb_temp = ad_cb << 16; // NOLINT + uint32_t product_lo = bd + ad_cb_temp; + if (product_lo < bd) + product_hi++; + +#ifndef FIXMATH_NO_OVERFLOW + // The upper 17 bits should all be the same (the sign). + if (product_hi >> 31 != product_hi >> 15) + return FIX16_OVERFLOW; +#endif + +#ifdef FIXMATH_NO_ROUNDING + return (product_hi << 16) | (product_lo >> 16); +#else + // Subtracting 0x8000 (= 0.5) and then using signed right shift + // achieves proper rounding to result-1, except in the corner + // case of negative numbers and lowest word = 0x8000. + // To handle that, we also have to subtract 1 for negative numbers. + uint32_t product_lo_tmp = product_lo; + product_lo -= 0x8000; + product_lo -= (uint32_t) product_hi >> 31; + if (product_lo > product_lo_tmp) + product_hi--; + + // Discard the lowest 16 bits. Note that this is not exactly the same + // as dividing by 0x10000. For example if product = -1, result will + // also be -1 and not 0. This is compensated by adding +1 to the result + // and compensating this in turn in the rounding above. + fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT + result += 1; + return result; +#endif +} + +static fix16_t fix16_div(fix16_t a, fix16_t b) { + // This uses the basic binary restoring division algorithm. + // It appears to be faster to do the whole division manually than + // trying to compose a 64-bit divide out of 32-bit divisions on + // platforms without hardware divide. + + if (b == 0) + return FIX16_MINIMUM; + + uint32_t remainder = (a >= 0) ? a : (-a); + uint32_t divider = (b >= 0) ? b : (-b); + + uint32_t quotient = 0; + uint32_t bit = 0x10000; + + /* The algorithm requires D >= R */ + while (divider < remainder) { + divider <<= 1; + bit <<= 1; + } + +#ifndef FIXMATH_NO_OVERFLOW + if (!bit) + return FIX16_OVERFLOW; +#endif + + if (divider & 0x80000000) { + // Perform one step manually to avoid overflows later. + // We know that divider's bottom bit is 0 here. + if (remainder >= divider) { + quotient |= bit; + remainder -= divider; + } + divider >>= 1; + bit >>= 1; + } + + /* Main division loop */ + while (bit && remainder) { + if (remainder >= divider) { + quotient |= bit; + remainder -= divider; + } + + remainder <<= 1; + bit >>= 1; + } + +#ifndef FIXMATH_NO_ROUNDING + if (remainder >= divider) { + quotient++; + } +#endif + + fix16_t result = quotient; + + /* Figure out the sign of result */ + if ((a ^ b) & 0x80000000) { +#ifndef FIXMATH_NO_OVERFLOW + if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare) + return FIX16_OVERFLOW; +#endif + + result = -result; + } + + return result; +} + +static fix16_t fix16_sqrt(fix16_t in_value) { + // It is assumed that x is not negative + + uint32_t num = in_value; + uint32_t result = 0; + uint32_t bit; + uint8_t n; + + bit = (uint32_t) 1 << 30; + while (bit > num) + bit >>= 2; + + // The main part is executed twice, in order to avoid + // using 64 bit values in computations. + for (n = 0; n < 2; n++) { + // First we get the top 24 bits of the answer. + while (bit) { + if (num >= result + bit) { + num -= result + bit; + result = (result >> 1) + bit; + } else { + result = (result >> 1); + } + bit >>= 2; + } + + if (n == 0) { + // Then process it again to get the lowest 8 bits. + if (num > 65535) { + // The remainder 'num' is too large to be shifted left + // by 16, so we have to add 1 to result manually and + // adjust 'num' accordingly. + // num = a - (result + 0.5)^2 + // = num + result^2 - (result + 0.5)^2 + // = num - result - 0.5 + num -= result; + num = (num << 16) - 0x8000; + result = (result << 16) + 0x8000; + } else { + num <<= 16; + result <<= 16; + } + + bit = 1 << 14; + } + } + +#ifndef FIXMATH_NO_ROUNDING + // Finally, if next bit would have been 1, round the result upwards. + if (num > result) { + result++; + } +#endif + + return (fix16_t) result; +} + +static fix16_t fix16_exp(fix16_t in_value) { + // Function to approximate exp(); optimized more for code size than speed + + // exp(x) for x = +/- {1, 1/8, 1/64, 1/512} + fix16_t x = in_value; + static const uint8_t NUM_EXP_VALUES = 4; + static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)}; + static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)}; + const fix16_t *exp_values; + + fix16_t res, arg; + uint16_t i; + + if (x >= F16(10.3972)) + return FIX16_MAXIMUM; + if (x <= F16(-11.7835)) + return 0; + + if (x < 0) { + x = -x; + exp_values = EXP_NEG_VALUES; + } else { + exp_values = EXP_POS_VALUES; + } + + res = FIX16_ONE; + arg = FIX16_ONE; + for (i = 0; i < NUM_EXP_VALUES; i++) { + while (x >= arg) { + res = fix16_mul(res, exp_values[i]); + x -= arg; + } + arg >>= 3; + } + return res; +} + +static void voc_algorithm_init_instances(VocAlgorithmParams *params); +static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params); +static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params); +static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, + fix16_t gating_max_duration_minutes); +static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std, + fix16_t uptime_gamma); +static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params); +static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params); +static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params, + fix16_t voc_index_from_prior); +static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw, + fix16_t voc_index_from_prior); +static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params); +static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l, + fix16_t x0, fix16_t k); +static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample); +static void voc_algorithm_mox_model_init(VocAlgorithmParams *params); +static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean); +static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw); +static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params); +static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset); +static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample); +static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params); +static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params); +static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample); + +void voc_algorithm_init(VocAlgorithmParams *params) { + params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT); + params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS); + params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES); + params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL); + params->mUptime = F16(0.); + params->mSraw = F16(0.); + params->mVoc_Index = 0; + voc_algorithm_init_instances(params); +} + +static void voc_algorithm_init_instances(VocAlgorithmParams *params) { + voc_algorithm_mean_variance_estimator_init(params); + voc_algorithm_mean_variance_estimator_set_parameters( + params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes); + voc_algorithm_mox_model_init(params); + voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), + voc_algorithm_mean_variance_estimator_get_mean(params)); + voc_algorithm_sigmoid_scaled_init(params); + voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset); + voc_algorithm_adaptive_lowpass_init(params); + voc_algorithm_adaptive_lowpass_set_parameters(params); +} + +void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) { + *state0 = voc_algorithm_mean_variance_estimator_get_mean(params); + *state1 = voc_algorithm_mean_variance_estimator_get_std(params); +} + +void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) { + voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA)); + params->mSraw = state0; +} + +void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, + int32_t learning_time_hours, int32_t gating_max_duration_minutes, + int32_t std_initial) { + params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset)); + params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours)); + params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes)); + params->mSraw_Std_Initial = (fix16_from_int(std_initial)); + voc_algorithm_init_instances(params); +} + +void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) { + if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) { + params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); + } else { + if (((sraw > 0) && (sraw < 65000))) { + if ((sraw < 20001)) { + sraw = 20001; + } else if ((sraw > 52767)) { + sraw = 52767; + } + params->mSraw = (fix16_from_int((sraw - 20000))); + } + params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw); + params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index); + params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index); + if ((params->mVoc_Index < F16(0.5))) { + params->mVoc_Index = F16(0.5); + } + if ((params->mSraw > F16(0.))) { + voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index); + voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), + voc_algorithm_mean_variance_estimator_get_mean(params)); + } + } + *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5)))); +} + +static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) { + voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.)); + voc_algorithm_mean_variance_estimator_init_instances(params); +} + +static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) { + voc_algorithm_mean_variance_estimator_sigmoid_init(params); +} + +static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, + fix16_t gating_max_duration_minutes) { + params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes; + params->m_Mean_Variance_Estimator_Initialized = false; + params->m_Mean_Variance_Estimator_Mean = F16(0.); + params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.); + params->m_Mean_Variance_Estimator_Std = std_initial; + params->m_Mean_Variance_Estimator_Gamma = + (fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))), + (tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))))); + params->m_Mean_Variance_Estimator_Gamma_Initial_Mean = + F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / + (VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator_Gamma_Initial_Variance = + F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / + (VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.); + params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.); + params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.); + params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.); + params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.); +} + +static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std, + fix16_t uptime_gamma) { + params->m_Mean_Variance_Estimator_Mean = mean; + params->m_Mean_Variance_Estimator_Std = std; + params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma; + params->m_Mean_Variance_Estimator_Initialized = true; +} + +static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) { + return params->m_Mean_Variance_Estimator_Std; +} + +static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) { + return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset); +} + +static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params, + fix16_t voc_index_from_prior) { + fix16_t uptime_limit; + fix16_t sigmoid_gamma_mean; + fix16_t gamma_mean; + fix16_t gating_threshold_mean; + fix16_t sigmoid_gating_mean; + fix16_t sigmoid_gamma_variance; + fix16_t gamma_variance; + fix16_t gating_threshold_variance; + fix16_t sigmoid_gating_variance; + + uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL)); + if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) { + params->m_Mean_Variance_Estimator_Uptime_Gamma = + (params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); + } + if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) { + params->m_Mean_Variance_Estimator_Uptime_Gating = + (params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); + } + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN), + F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN)); + sigmoid_gamma_mean = + voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma); + gamma_mean = + (params->m_Mean_Variance_Estimator_Gamma + + (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma), + sigmoid_gamma_mean))); + gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) + + (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), + voc_algorithm_mean_variance_estimator_sigmoid_process( + params, params->m_Mean_Variance_Estimator_Uptime_Gating)))); + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean, + F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); + params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean)); + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters( + params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE)); + sigmoid_gamma_variance = + voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma); + gamma_variance = + (params->m_Mean_Variance_Estimator_Gamma + + (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma), + (sigmoid_gamma_variance - sigmoid_gamma_mean)))); + gating_threshold_variance = + (F16(VOC_ALGORITHM_GATING_THRESHOLD) + + (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), + voc_algorithm_mean_variance_estimator_sigmoid_process( + params, params->m_Mean_Variance_Estimator_Uptime_Gating)))); + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance, + F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); + params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance)); + params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = + (params->m_Mean_Variance_Estimator_Gating_Duration_Minutes + + (fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)), + ((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) - + F16(VOC_ALGORITHM_GATING_MAX_RATIO))))); + if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) { + params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.); + } + if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes > + params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) { + params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.); + } +} + +static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw, + fix16_t voc_index_from_prior) { + fix16_t delta_sgp; + fix16_t c; + fix16_t additional_scaling; + + if ((!params->m_Mean_Variance_Estimator_Initialized)) { + params->m_Mean_Variance_Estimator_Initialized = true; + params->m_Mean_Variance_Estimator_Sraw_Offset = sraw; + params->m_Mean_Variance_Estimator_Mean = F16(0.); + } else { + if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) || + (params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) { + params->m_Mean_Variance_Estimator_Sraw_Offset = + (params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean); + params->m_Mean_Variance_Estimator_Mean = F16(0.); + } + sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset); + voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior); + delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean), + F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING))); + if ((delta_sgp < F16(0.))) { + c = (params->m_Mean_Variance_Estimator_Std - delta_sgp); + } else { + c = (params->m_Mean_Variance_Estimator_Std + delta_sgp); + } + additional_scaling = F16(1.); + if ((c > F16(1440.))) { + additional_scaling = F16(4.); + } + params->m_Mean_Variance_Estimator_Std = (fix16_mul( + fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) - + params->m_Mean_Variance_Estimator_Gamma_Variance)))), + fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std, + (fix16_div(params->m_Mean_Variance_Estimator_Std, + (fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING), + additional_scaling)))))) + + (fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)), + additional_scaling)), + delta_sgp)))))); + params->m_Mean_Variance_Estimator_Mean = + (params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp))); + } +} + +static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) { + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.)); +} + +static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l, + fix16_t x0, fix16_t k) { + params->m_Mean_Variance_Estimator_Sigmoid_L = l; + params->m_Mean_Variance_Estimator_Sigmoid_K = k; + params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0; +} + +static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) { + fix16_t x; + + x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0))); + if ((x < F16(-50.))) { + return params->m_Mean_Variance_Estimator_Sigmoid_L; + } else if ((x > F16(50.))) { + return F16(0.); + } else { + return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x)))); + } +} + +static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) { + voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.)); +} + +static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) { + params->m_Mox_Model_Sraw_Std = sraw_std; + params->m_Mox_Model_Sraw_Mean = sraw_mean; +} + +static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) { + return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean), + (-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))), + F16(VOC_ALGORITHM_VOC_INDEX_GAIN))); +} + +static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) { + voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.)); +} + +static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) { + params->m_Sigmoid_Scaled_Offset = offset; +} + +static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) { + fix16_t x; + fix16_t shift; + + x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0)))); + if ((x < F16(-50.))) { + return F16(VOC_ALGORITHM_SIGMOID_L); + } else if ((x > F16(50.))) { + return F16(0.); + } else { + if ((sample >= F16(0.))) { + shift = + (fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.))); + return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift); + } else { + return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))), + (fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x)))))); + } + } +} + +static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) { + voc_algorithm_adaptive_lowpass_set_parameters(params); +} + +static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) { + params->m_Adaptive_Lowpass_A1 = + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass_A2 = + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass_Initialized = false; +} + +static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) { + fix16_t abs_delta; + fix16_t f1; + fix16_t tau_a; + fix16_t a3; + + if ((!params->m_Adaptive_Lowpass_Initialized)) { + params->m_Adaptive_Lowpass_X1 = sample; + params->m_Adaptive_Lowpass_X2 = sample; + params->m_Adaptive_Lowpass_X3 = sample; + params->m_Adaptive_Lowpass_Initialized = true; + } + params->m_Adaptive_Lowpass_X1 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) + + (fix16_mul(params->m_Adaptive_Lowpass_A1, sample))); + params->m_Adaptive_Lowpass_X2 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) + + (fix16_mul(params->m_Adaptive_Lowpass_A2, sample))); + abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2); + if ((abs_delta < F16(0.))) { + abs_delta = (-abs_delta); + } + f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta))); + tau_a = + ((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST)); + a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a))); + params->m_Adaptive_Lowpass_X3 = + ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample))); + return params->m_Adaptive_Lowpass_X3; +} +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.h b/esphome/components/sgp40/sensirion_voc_algorithm.h new file mode 100644 index 0000000000..adef6b29e8 --- /dev/null +++ b/esphome/components/sgp40/sensirion_voc_algorithm.h @@ -0,0 +1,147 @@ +#pragma once +#include +namespace esphome { +namespace sgp40 { + +/* The VOC code were originally created by + * https://github.com/Sensirion/embedded-sgp + * The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +using fix16_t = int32_t; + +#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5))) + +static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.); +static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.); +static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.); +static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.); +static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.); +static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.); +static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.); +static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75)); +static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01); +static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.); +static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45)); +static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01); +static const float VOC_ALGORITHM_GATING_THRESHOLD(340.); +static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.); +static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09); +static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.)); +static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3); +static const float VOC_ALGORITHM_SIGMOID_L(500.); +static const float VOC_ALGORITHM_SIGMOID_K(-0.0065); +static const float VOC_ALGORITHM_SIGMOID_X0(213.); +static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.); +static const float VOC_ALGORITHM_LP_TAU_FAST(20.0); +static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0); +static const float VOC_ALGORITHM_LP_ALPHA(-0.2); +static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.)); +static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.); +static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.); + +/** + * Struct to hold all the states of the VOC algorithm. + */ +struct VocAlgorithmParams { + fix16_t mVoc_Index_Offset; + fix16_t mTau_Mean_Variance_Hours; + fix16_t mGating_Max_Duration_Minutes; + fix16_t mSraw_Std_Initial; + fix16_t mUptime; + fix16_t mSraw; + fix16_t mVoc_Index; + fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes; + bool m_Mean_Variance_Estimator_Initialized; + fix16_t m_Mean_Variance_Estimator_Mean; + fix16_t m_Mean_Variance_Estimator_Sraw_Offset; + fix16_t m_Mean_Variance_Estimator_Std; + fix16_t m_Mean_Variance_Estimator_Gamma; + fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean; + fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance; + fix16_t m_Mean_Variance_Estimator_Gamma_Mean; + fix16_t m_Mean_Variance_Estimator_Gamma_Variance; + fix16_t m_Mean_Variance_Estimator_Uptime_Gamma; + fix16_t m_Mean_Variance_Estimator_Uptime_Gating; + fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes; + fix16_t m_Mean_Variance_Estimator_Sigmoid_L; + fix16_t m_Mean_Variance_Estimator_Sigmoid_K; + fix16_t m_Mean_Variance_Estimator_Sigmoid_X0; + fix16_t m_Mox_Model_Sraw_Std; + fix16_t m_Mox_Model_Sraw_Mean; + fix16_t m_Sigmoid_Scaled_Offset; + fix16_t m_Adaptive_Lowpass_A1; + fix16_t m_Adaptive_Lowpass_A2; + bool m_Adaptive_Lowpass_Initialized; + fix16_t m_Adaptive_Lowpass_X1; + fix16_t m_Adaptive_Lowpass_X2; + fix16_t m_Adaptive_Lowpass_X3; +}; + +/** + * Initialize the VOC algorithm parameters. Call this once at the beginning or + * whenever the sensor stopped measurements. + * @param params Pointer to the VocAlgorithmParams struct + */ +void voc_algorithm_init(VocAlgorithmParams *params); + +/** + * Get current algorithm states. Retrieved values can be used in + * voc_algorithm_set_states() to resume operation after a short interruption, + * skipping initial learning phase. This feature can only be used after at least + * 3 hours of continuous operation. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be stored + * @param state1 State1 to be stored + */ +void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1); + +/** + * Set previously retrieved algorithm states to resume operation after a short + * interruption, skipping initial learning phase. This feature should not be + * used after inerruptions of more than 10 minutes. Call this once after + * voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if + * desired. Otherwise, the algorithm will start with initial learning phase. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be restored + * @param state1 State1 to be restored + */ +void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1); + +/** + * Set parameters to customize the VOC algorithm. Call this once after + * voc_algorithm_init(), if desired. Otherwise, the default values will be used. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param voc_index_offset VOC index representing typical (average) + * conditions. Range 1..250, default 100 + * @param learning_time_hours Time constant of long-term estimator. + * Past events will be forgotten after about + * twice the learning time. + * Range 1..72 [hours], default 12 [hours] + * @param gating_max_duration_minutes Maximum duration of gating (freeze of + * estimator during high VOC index signal). + * 0 (no gating) or range 1..720 [minutes], + * default 180 [minutes] + * @param std_initial Initial estimate for standard deviation. + * Lower value boosts events during initial + * learning period, but may result in larger + * device-to-device variations. + * Range 10..500, default 50 + */ +void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, + int32_t learning_time_hours, int32_t gating_max_duration_minutes, + int32_t std_initial); + +/** + * Calculate the VOC index value from the raw sensor value. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param sraw Raw value from the SGP40 sensor + * @param voc_index Calculated VOC index value from the raw sensor value. Zero + * during initial blackout period and 1..500 afterwards + */ +void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index); +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py new file mode 100644 index 0000000000..7b96f867af --- /dev/null +++ b/esphome/components/sgp40/sensor.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + ICON_RADIATOR, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@SenexCrenshaw"] + +sgp40_ns = cg.esphome_ns.namespace("sgp40") +SGP40Component = sgp40_ns.class_( + "SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONF_COMPENSATION = "compensation" +CONF_HUMIDITY_SOURCE = "humidity_source" +CONF_TEMPERATURE_SOURCE = "temperature_source" +CONF_STORE_BASELINE = "store_baseline" +CONF_VOC_BASELINE = "voc_baseline" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(SGP40Component), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, + cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), + }, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x59)) +) + + +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) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + cg.add(var.set_temperature_sensor(sens)) + + cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + + if CONF_VOC_BASELINE in config: + cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp new file mode 100644 index 0000000000..9561efcde2 --- /dev/null +++ b/esphome/components/sgp40/sgp40.cpp @@ -0,0 +1,345 @@ +#include "sgp40.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace sgp40 { + +static const char *const TAG = "sgp40"; + +void SGP40Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SGP40..."); + + // Serial Number identification + if (!this->write_command_(SGP40_CMD_GET_SERIAL_ID)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + uint16_t raw_serial_number[3]; + + if (!this->read_data_(raw_serial_number, 3)) { + this->mark_failed(); + return; + } + 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: %" PRIu64, this->serial_number_); + + // Featureset identification for future use + if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) { + ESP_LOGD(TAG, "raw_featureset write_command_ failed"); + this->mark_failed(); + return; + } + uint16_t raw_featureset[1]; + if (!this->read_data_(raw_featureset, 1)) { + ESP_LOGD(TAG, "raw_featureset read_data_ failed"); + this->mark_failed(); + return; + } + + this->featureset_ = raw_featureset[0]; + if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { + ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), + SGP40_FEATURESET); + this->mark_failed(); + return; + } + + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + voc_algorithm_init(&this->voc_algorithm_params_); + + if (this->store_baseline_) { + // 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_)) { + this->state0_ = this->baselines_storage_.state0; + this->state1_ = this->baselines_storage_.state1; + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, + baselines_storage_.state1); + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + + if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) { + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, + baselines_storage_.state1); + voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0, + this->baselines_storage_.state1); + } + } + + 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, "Self-test started"); + if (!this->write_command_(SGP40_CMD_SELF_TEST)) { + this->error_code_ = COMMUNICATION_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, "Self-test read_data_ failed"); + this->mark_failed(); + return; + } + + if (reply[0] == 0xD400) { + this->self_test_complete_ = true; + ESP_LOGD(TAG, "Self-test completed"); + return; + } + + ESP_LOGD(TAG, "Self-test failed"); + this->mark_failed(); + }); +} + +/** + * @brief Combined the measured gasses, temperature, and humidity + * to calculate the VOC Index + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return int32_t The VOC Index + */ +int32_t SGP40Component::measure_voc_index_() { + int32_t voc_index; + + uint16_t sraw = measure_raw_(); + + if (sraw == UINT16_MAX) + return UINT16_MAX; + + this->status_clear_warning(); + + voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index); + + // Store baselines after defined interval or if the difference between current and stored baseline becomes too + // much + if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { + voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_); + if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF || + (uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) { + this->seconds_since_last_store_ = 0; + this->baselines_storage_.state0 = this->state0_; + this->baselines_storage_.state1 = this->state1_; + + if (this->pref_.save(&this->baselines_storage_)) { + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0, + baselines_storage_.state1); + } else { + ESP_LOGW(TAG, "Could not store VOC baselines"); + } + } + } + + return voc_index; +} + +/** + * @brief Return the raw gas measurement + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return uint16_t The current raw gas measurement + */ +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 (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + humidity = 50; + } + + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + temperature = 25; + } + + uint8_t command[8]; + + command[0] = 0x26; + command[1] = 0x0F; + + uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); + command[2] = rhticks >> 8; + command[3] = rhticks & 0xFF; + command[4] = generate_crc_(command + 2, 2); + uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); + command[5] = tempticks >> 8; + command[6] = tempticks & 0xFF; + command[7] = generate_crc_(command + 5, 2); + + if (this->write(command, 8) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGD(TAG, "write error"); + return UINT16_MAX; + } + delay(250); // NOLINT + uint16_t raw_data[1]; + + if (!this->read_data_(raw_data, 1)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read_data_ error"); + return UINT16_MAX; + } + return raw_data[0]; +} + +uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) { + // calculates 8-Bit checksum with given polynomial + uint8_t crc = SGP40_CRC8_INIT; + + for (uint8_t i = 0; i < datalen; i++) { + crc ^= data[i]; + for (uint8_t b = 0; b < 8; b++) { + if (crc & 0x80) + crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL; + else + crc <<= 1; + } + } + return crc; +} + +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_, this->voc_index_); + return; + } +} + +void SGP40Component::update() { + if (this->samples_read_ < this->samples_to_stabalize_) { + return; + } + + if (this->voc_index_ != UINT16_MAX) { + this->status_clear_warning(); + this->publish_state(this->voc_index_); + } else { + this->status_set_warning(); + } +} + +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: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); + ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT); + } + LOG_UPDATE_INTERVAL(this); + + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } +} + +bool SGP40Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SGP40Component::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 SGP40Component::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; +} + +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h new file mode 100644 index 0000000000..c854b21060 --- /dev/null +++ b/esphome/components/sgp40/sgp40.h @@ -0,0 +1,95 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/application.h" +#include "esphome/core/preferences.h" +#include "sensirion_voc_algorithm.h" + +#include + +namespace esphome { +namespace sgp40 { + +struct SGP40Baselines { + int32_t state0; + int32_t state1; +} PACKED; // NOLINT + +// commands and constants +static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library +static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial +static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC +static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word + +// Commands + +static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP40_CMD_SELF_TEST = 0x280e; + +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; + +// Store anyway if the baseline difference exceeds the max storage diff value +const uint32_t MAXIMUM_STORAGE_DIFF = 50; + +class SGP40Component; + +/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors. +class SGP40Component : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { + public: + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + 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; } + + protected: + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + int16_t sensirion_init_sensors_(); + int16_t sgp40_probe_(); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + uint64_t serial_number_; + uint16_t featureset_; + int32_t measure_voc_index_(); + uint8_t generate_crc_(const uint8_t *data, uint8_t datalen); + uint16_t measure_raw_(); + ESPPreferenceObject pref_; + uint32_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; + + /** + * @brief Request the sensor to perform a self-test, returning the result + * + * @return true: success false:failure + */ + void self_test_(); + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + UNKNOWN + } error_code_{UNKNOWN}; +}; +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index 9bbdc47eec..b9e7bce733 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -1,30 +1,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, ICON_WATER_PERCENT, \ - ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -sht3xd_ns = cg.esphome_ns.namespace('sht3xd') -SHT3XDComponent = sht3xd_ns.class_('SHT3XDComponent', cg.PollingComponent, i2c.I2CDevice) +sht3xd_ns = cg.esphome_ns.namespace("sht3xd") +SHT3XDComponent = sht3xd_ns.class_( + "SHT3XDComponent", cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SHT3XDComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x44)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SHT3XDComponent), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index 559fdc21ab..56a43d5161 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sht3xd { -static const char *TAG = "sht3xd"; +static const char *const TAG = "sht3xd"; static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3780; static const uint16_t SHT3XD_COMMAND_READ_STATUS = 0xF32D; @@ -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/__init__.py b/esphome/components/sht4x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py new file mode 100644 index 0000000000..a66ca1a526 --- /dev/null +++ b/esphome/components/sht4x/sensor.py @@ -0,0 +1,101 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + CONF_HUMIDITY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +sht4x_ns = cg.esphome_ns.namespace("sht4x") + +SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice) + +CONF_PRECISION = "precision" +SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION") +PRECISION_OPTIONS = { + "High": SHT4XPRECISION.SHT4X_PRECISION_HIGH, + "Med": SHT4XPRECISION.SHT4X_PRECISION_MED, + "Low": SHT4XPRECISION.SHT4X_PRECISION_LOW, +} + +CONF_HEATER_POWER = "heater_power" +SHT4XHEATERPOWER = sht4x_ns.enum("SHT4XHEATERPOWER") +HEATER_POWER_OPTIONS = { + "High": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_HIGH, + "Med": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_MED, + "Low": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_LOW, +} + +CONF_HEATER_TIME = "heater_time" +SHT4XHEATERTIME = sht4x_ns.enum("SHT4XHEATERTIME") +HEATER_TIME_OPTIONS = { + "Long": SHT4XHEATERTIME.SHT4X_HEATERTIME_LONG, + "Short": SHT4XHEATERTIME.SHT4X_HEATERTIME_SHORT, +} + +CONF_HEATER_MAX_DUTY = "heater_max_duty" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SHT4XComponent), + 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_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS), + cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum( + HEATER_POWER_OPTIONS + ), + cv.Optional(CONF_HEATER_TIME, default="Long"): cv.enum(HEATER_TIME_OPTIONS), + cv.Optional(CONF_HEATER_MAX_DUTY, default=0.0): cv.float_range( + min=0.0, max=0.05 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) + +TYPES = { + CONF_TEMPERATURE: "set_temp_sensor", + CONF_HUMIDITY: "set_humidity_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_precision_value(config[CONF_PRECISION])) + cg.add(var.set_heater_power_value(config[CONF_HEATER_POWER])) + cg.add(var.set_heater_time_value(config[CONF_HEATER_TIME])) + cg.add(var.set_heater_duty_value(config[CONF_HEATER_MAX_DUTY])) + + 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/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp new file mode 100644 index 0000000000..248f32c4de --- /dev/null +++ b/esphome/components/sht4x/sht4x.cpp @@ -0,0 +1,89 @@ +#include "sht4x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sht4x { + +static const char *const TAG = "sht4x"; + +static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; + +void SHT4XComponent::start_heater_() { + uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; + + ESP_LOGD(TAG, "Heater turning on"); + this->write(cmd, 1); +} + +void SHT4XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sht4x..."); + + if (this->duty_cycle_ > 0.0) { + uint32_t heater_interval = (uint32_t)(this->heater_time_ / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %i", heater_interval); + + if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x39; + } else { + this->heater_command_ = 0x32; + } + } else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x2F; + } else { + this->heater_command_ = 0x24; + } + } else { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x1E; + } else { + this->heater_command_ = 0x15; + } + } + ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); + + this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); + } +} + +void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); } + +void SHT4XComponent::update() { + uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]}; + + // Send command + this->write(cmd, 1); + + this->set_timeout(10, [this]() { + const uint8_t num_bytes = 6; + uint8_t buffer[num_bytes]; + + // Read measurement + bool read_status = this->read_bytes_raw(buffer, num_bytes); + + if (read_status) { + // Evaluate and publish measurements + if (this->temp_sensor_ != nullptr) { + // Temp is contained in the first 16 bits + float sensor_value_temp = (buffer[0] << 8) + buffer[1]; + float temp = -45 + 175 * sensor_value_temp / 65535; + + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + // Relative humidity is in the last 16 bits + float sensor_value_rh = (buffer[3] << 8) + buffer[4]; + float rh = -6 + 125 * sensor_value_rh / 65535; + + this->humidity_sensor_->publish_state(rh); + } + } else { + ESP_LOGD(TAG, "Sensor read failed"); + } + }); +} + +} // namespace sht4x +} // namespace esphome diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h new file mode 100644 index 0000000000..8694bd9879 --- /dev/null +++ b/esphome/components/sht4x/sht4x.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sht4x { + +enum SHT4XPRECISION { SHT4X_PRECISION_HIGH = 0, SHT4X_PRECISION_MED, SHT4X_PRECISION_LOW }; + +enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEATERPOWER_LOW }; + +enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 }; + +class SHT4XComponent : 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_precision_value(SHT4XPRECISION precision) { this->precision_ = precision; }; + void set_heater_power_value(SHT4XHEATERPOWER heater_power) { this->heater_power_ = heater_power; }; + void set_heater_time_value(SHT4XHEATERTIME heater_time) { this->heater_time_ = heater_time; }; + void set_heater_duty_value(float duty_cycle) { this->duty_cycle_ = duty_cycle; }; + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + SHT4XPRECISION precision_; + SHT4XHEATERPOWER heater_power_; + SHT4XHEATERTIME heater_time_; + float duty_cycle_; + + void start_heater_(); + uint8_t heater_command_; + + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace sht4x +} // namespace esphome diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index eb215078e7..ba2283a9b4 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -1,32 +1,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, ICON_WATER_PERCENT, \ - ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -shtcx_ns = cg.esphome_ns.namespace('shtcx') -SHTCXComponent = shtcx_ns.class_('SHTCXComponent', cg.PollingComponent, i2c.I2CDevice) +shtcx_ns = cg.esphome_ns.namespace("shtcx") +SHTCXComponent = shtcx_ns.class_("SHTCXComponent", cg.PollingComponent, i2c.I2CDevice) -SHTCXType = shtcx_ns.enum('SHTCXType') +SHTCXType = shtcx_ns.enum("SHTCXType") -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SHTCXComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x70)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SHTCXComponent), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x70)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index d67031febf..f2fb6bd5c3 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -1,10 +1,11 @@ #include "shtcx.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace shtcx { -static const char *TAG = "shtcx"; +static const char *const TAG = "shtcx"; static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098; static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; @@ -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/__init__.py b/esphome/components/shutdown/__init__.py index e69de29bb2..480a6f3e31 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core", "@jsuanet"] diff --git a/esphome/components/shutdown/button/__init__.py b/esphome/components/shutdown/button/__init__.py new file mode 100644 index 0000000000..51cd6d6da2 --- /dev/null +++ b/esphome/components/shutdown/button/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_ID, + ENTITY_CATEGORY_CONFIG, + ICON_POWER, +) + +shutdown_ns = cg.esphome_ns.namespace("shutdown") +ShutdownButton = shutdown_ns.class_("ShutdownButton", button.Button, cg.Component) + +CONFIG_SCHEMA = ( + button.button_schema(entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER) + .extend({cv.GenerateID(): cv.declare_id(ShutdownButton)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) diff --git a/esphome/components/shutdown/button/shutdown_button.cpp b/esphome/components/shutdown/button/shutdown_button.cpp new file mode 100644 index 0000000000..be88a10d49 --- /dev/null +++ b/esphome/components/shutdown/button/shutdown_button.cpp @@ -0,0 +1,33 @@ +#include "shutdown_button.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 { + +static const char *const TAG = "shutdown.button"; + +void ShutdownButton::dump_config() { LOG_BUTTON("", "Shutdown Button", this); } +void ShutdownButton::press_action() { + ESP_LOGI(TAG, "Shutting down..."); + // Let MQTT settle a bit + delay(100); // NOLINT + App.run_safe_shutdown_hooks(); +#ifdef USE_ESP8266 + ESP.deepSleep(0); // NOLINT(readability-static-accessed-through-instance) +#endif +#ifdef USE_ESP32 + esp_deep_sleep_start(); +#endif +} + +} // namespace shutdown +} // namespace esphome diff --git a/esphome/components/shutdown/button/shutdown_button.h b/esphome/components/shutdown/button/shutdown_button.h new file mode 100644 index 0000000000..d0094c899d --- /dev/null +++ b/esphome/components/shutdown/button/shutdown_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace shutdown { + +class ShutdownButton : public button::Button, public Component { + public: + void dump_config() override; + + protected: + void press_action() override; +}; + +} // namespace shutdown +} // namespace esphome diff --git a/esphome/components/shutdown/switch.py b/esphome/components/shutdown/switch.py deleted file mode 100644 index 9826f9bbe5..0000000000 --- a/esphome/components/shutdown/switch.py +++ /dev/null @@ -1,21 +0,0 @@ -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 - -shutdown_ns = cg.esphome_ns.namespace('shutdown') -ShutdownSwitch = shutdown_ns.class_('ShutdownSwitch', switch.Switch, cg.Component) - -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(ShutdownSwitch), - - cv.Optional(CONF_INVERTED): cv.invalid("Shutdown switches do not support inverted mode!"), - - cv.Optional(CONF_ICON, default=ICON_POWER): switch.icon -}).extend(cv.COMPONENT_SCHEMA) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield switch.register_switch(var, config) diff --git a/esphome/components/shutdown/switch/__init__.py b/esphome/components/shutdown/switch/__init__.py new file mode 100644 index 0000000000..49970b4c2f --- /dev/null +++ b/esphome/components/shutdown/switch/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ENTITY_CATEGORY_CONFIG, + ICON_POWER, +) + +shutdown_ns = cg.esphome_ns.namespace("shutdown") +ShutdownSwitch = shutdown_ns.class_("ShutdownSwitch", switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ShutdownSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Shutdown switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_POWER): switch.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + } +).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) diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/switch/shutdown_switch.cpp similarity index 64% rename from esphome/components/shutdown/shutdown_switch.cpp rename to esphome/components/shutdown/switch/shutdown_switch.cpp index ce33cd187f..a5f9a92982 100644 --- a/esphome/components/shutdown/shutdown_switch.cpp +++ b/esphome/components/shutdown/switch/shutdown_switch.cpp @@ -1,11 +1,19 @@ #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 { -static const char *TAG = "shutdown.switch"; +static const char *const TAG = "shutdown.switch"; void ShutdownSwitch::dump_config() { LOG_SWITCH("", "Shutdown Switch", this); } void ShutdownSwitch::write_state(bool state) { @@ -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/shutdown/shutdown_switch.h b/esphome/components/shutdown/switch/shutdown_switch.h similarity index 100% rename from esphome/components/shutdown/shutdown_switch.h rename to esphome/components/shutdown/switch/shutdown_switch.h diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c64112570a..0887b8640f 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -4,56 +4,93 @@ from esphome import automation from esphome.const import CONF_ID, CONF_TRIGGER_ID from esphome.components import uart -DEPENDENCIES = ['uart'] - -sim800l_ns = cg.esphome_ns.namespace('sim800l') -Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) - -Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger', - automation.Trigger.template(cg.std_string, - cg.std_string)) - -# Actions -Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) - +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@glmnet"] MULTI_CONF = True -CONF_ON_SMS_RECEIVED = 'on_sms_received' -CONF_RECIPIENT = 'recipient' -CONF_MESSAGE = 'message' +sim800l_ns = cg.esphome_ns.namespace("sim800l") +Sim800LComponent = sim800l_ns.class_("Sim800LComponent", cg.Component) -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(Sim800LComponent), - cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Sim800LReceivedMessageTrigger), - }), -}).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA)) +Sim800LReceivedMessageTrigger = sim800l_ns.class_( + "Sim800LReceivedMessageTrigger", + automation.Trigger.template(cg.std_string, cg.std_string), +) + +# Actions +Sim800LSendSmsAction = sim800l_ns.class_("Sim800LSendSmsAction", automation.Action) +Sim800LDialAction = sim800l_ns.class_("Sim800LDialAction", automation.Action) + +CONF_ON_SMS_RECEIVED = "on_sms_received" +CONF_RECIPIENT = "recipient" +CONF_MESSAGE = "message" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sim800LComponent), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Sim800LReceivedMessageTrigger + ), + } + ), + } + ) + .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 +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) for conf in config.get(CONF_ON_SMS_RECEIVED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [(cg.std_string, 'message'), - (cg.std_string, 'sender')], conf) + await automation.build_automation( + trigger, [(cg.std_string, "message"), (cg.std_string, "sender")], conf + ) -SIM800L_SEND_SMS_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.use_id(Sim800LComponent), - cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict), - cv.Required(CONF_MESSAGE): cv.templatable(cv.string), -}) +SIM800L_SEND_SMS_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(Sim800LComponent), + cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + } +) -@automation.register_action('sim800l.send_sms', Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA) -def sim800l_send_sms_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "sim800l.send_sms", Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA +) +async def sim800l_send_sms_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_ = yield cg.templatable(config[CONF_RECIPIENT], args, cg.std_string) + template_ = await cg.templatable(config[CONF_RECIPIENT], args, cg.std_string) cg.add(var.set_recipient(template_)) - template_ = yield cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) cg.add(var.set_message(template_)) - yield var + return var + + +SIM800L_DIAL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(Sim800LComponent), + cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action("sim800l.dial", Sim800LDialAction, SIM800L_DIAL_SCHEMA) +async def sim800l_dial_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_RECIPIENT], args, cg.std_string) + cg.add(var.set_recipient(template_)) + return var diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 9f8c733fa9..eb6d62ca33 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -1,11 +1,11 @@ #include "sim800l.h" #include "esphome/core/log.h" -#include +#include namespace esphome { namespace sim800l { -static const char* TAG = "sim800l"; +static const char *const TAG = "sim800l"; const char ASCII_CR = 0x0D; const char ASCII_LF = 0x0A; @@ -20,6 +20,9 @@ void Sim800LComponent::update() { if (this->registered_ && this->send_pending_) { this->send_cmd_("AT+CSCS=\"GSM\""); this->state_ = STATE_SENDINGSMS1; + } else if (this->registered_ && this->dial_pending_) { + this->send_cmd_("AT+CSCS=\"GSM\""); + this->state_ = STATE_DIALING1; } else { this->send_cmd_("AT"); this->state_ = STATE_CHECK_AT; @@ -37,7 +40,7 @@ void Sim800LComponent::update() { } } -void Sim800LComponent::send_cmd_(std::string message) { +void Sim800LComponent::send_cmd_(const std::string &message) { ESP_LOGV(TAG, "S: %s - %d", message.c_str(), this->state_); this->watch_dog_ = 0; this->write_str(message.c_str()); @@ -125,7 +128,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (message.compare(0, 5, "+CSQ:") == 0) { size_t comma = message.find(',', 6); if (comma != 6) { - this->rssi_ = strtol(message.substr(6, comma - 6).c_str(), nullptr, 10); + this->rssi_ = parse_number(message.substr(6, comma - 6)).value_or(0); ESP_LOGD(TAG, "RSSI: %d", this->rssi_); } } @@ -143,7 +146,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { while (end != start) { item++; if (item == 1) { // Slot Index - this->parse_index_ = strtol(message.substr(start, end - start).c_str(), nullptr, 10); + this->parse_index_ = parse_number(message.substr(start, end - start)).value_or(0); } // item 2 = STATUS, usually "REC UNERAD" if (item == 3) { // recipient @@ -212,6 +215,23 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->expect_ack_ = true; } break; + case STATE_DIALING1: + this->send_cmd_("ATD" + this->recipient_ + ';'); + this->state_ = STATE_DIALING2; + break; + case STATE_DIALING2: + if (message == "OK") { + // Dialing + ESP_LOGD(TAG, "Dialing: '%s'", this->recipient_.c_str()); + this->state_ = STATE_INIT; + this->dial_pending_ = false; + } else { + this->registered_ = false; + this->state_ = STATE_INIT; + this->send_cmd_("AT+CMEE=2"); + this->write(26); + } + break; default: ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_); break; @@ -248,7 +268,7 @@ void Sim800LComponent::loop() { } } -void Sim800LComponent::send_sms(std::string recipient, std::string message) { +void Sim800LComponent::send_sms(const std::string &recipient, const std::string &message) { ESP_LOGD(TAG, "Sending to %s: %s", recipient.c_str(), message.c_str()); this->recipient_ = recipient; this->outgoing_message_ = message; @@ -259,6 +279,12 @@ void Sim800LComponent::dump_config() { ESP_LOGCONFIG(TAG, "SIM800L:"); ESP_LOGCONFIG(TAG, " RSSI: %d dB", this->rssi_); } +void Sim800LComponent::dial(const std::string &recipient) { + ESP_LOGD(TAG, "Dialing %s", recipient.c_str()); + this->recipient_ = recipient; + this->dial_pending_ = true; + this->update(); +} } // namespace sim800l } // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index 696eb8890f..21e9ac4a50 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" @@ -29,7 +31,9 @@ enum State { STATE_RECEIVEDSMS, STATE_DELETEDSMS, STATE_DISABLE_ECHO, - STATE_PARSE_SMS_OK + STATE_PARSE_SMS_OK, + STATE_DIALING1, + STATE_DIALING2 }; class Sim800LComponent : public uart::UARTDevice, public PollingComponent { @@ -41,10 +45,11 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { void add_on_sms_received_callback(std::function callback) { this->callback_.add(std::move(callback)); } - void send_sms(std::string recipient, std::string message); + void send_sms(const std::string &recipient, const std::string &message); + void dial(const std::string &recipient); protected: - void send_cmd_(std::string); + void send_cmd_(const std::string &); void parse_cmd_(std::string); std::string sender_; @@ -60,6 +65,7 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { std::string recipient_; std::string outgoing_message_; bool send_pending_; + bool dial_pending_; CallbackManager callback_; }; @@ -68,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(message, sender); }); + [this](const std::string &message, const std::string &sender) { this->trigger(message, sender); }); } }; @@ -88,5 +94,19 @@ template class Sim800LSendSmsAction : public Action { Sim800LComponent *parent_; }; +template class Sim800LDialAction : public Action { + public: + Sim800LDialAction(Sim800LComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, recipient) + + void play(Ts... x) { + auto recipient = this->recipient_.value(x...); + this->parent_->dial(recipient); + } + + protected: + Sim800LComponent *parent_; +}; + } // namespace sim800l } // namespace esphome diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py index f7b26a953a..4f44582eba 100644 --- a/esphome/components/slow_pwm/output.py +++ b/esphome/components/slow_pwm/output.py @@ -19,11 +19,11 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield output.register_output(var, config) + await cg.register_component(var, config) + await output.register_output(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) cg.add(var.set_period(config[CONF_PERIOD])) diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 04a0d86bf7..9b2589e735 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace slow_pwm { -static const char *TAG = "output.slow_pwm"; +static const char *const TAG = "output.slow_pwm"; void SlowPWMOutput::setup() { this->pin_->setup(); @@ -12,7 +12,7 @@ void SlowPWMOutput::setup() { } void SlowPWMOutput::loop() { - unsigned long now = millis(); + uint32_t now = millis(); float scaled_state = this->state_ * this->period_; if (now - this->period_start_time_ > this->period_) { 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/__init__.py b/esphome/components/sm16716/__init__.py index 4e342588f9..ec8ed722f3 100644 --- a/esphome/components/sm16716/__init__.py +++ b/esphome/components/sm16716/__init__.py @@ -1,30 +1,37 @@ 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_CHANNELS, CONF_NUM_CHIPS) +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, + CONF_NUM_CHANNELS, + CONF_NUM_CHIPS, +) -AUTO_LOAD = ['output'] -sm16716_ns = cg.esphome_ns.namespace('sm16716') -SM16716 = sm16716_ns.class_('SM16716', cg.Component) +AUTO_LOAD = ["output"] +sm16716_ns = cg.esphome_ns.namespace("sm16716") +SM16716 = sm16716_ns.class_("SM16716", cg.Component) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SM16716), - cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_NUM_CHANNELS, default=3): cv.int_range(min=3, max=255), - cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM16716), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHANNELS, default=3): cv.int_range(min=3, max=255), + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) cg.add(var.set_data_pin(data)) - clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) cg.add(var.set_clock_pin(clock)) cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS])) diff --git a/esphome/components/sm16716/output.py b/esphome/components/sm16716/output.py index 93c9ed4ce1..e9b92f68f8 100644 --- a/esphome/components/sm16716/output.py +++ b/esphome/components/sm16716/output.py @@ -4,22 +4,24 @@ from esphome.components import output from esphome.const import CONF_CHANNEL, CONF_ID from . import SM16716 -DEPENDENCIES = ['sm16716'] +DEPENDENCIES = ["sm16716"] -Channel = SM16716.class_('Channel', output.FloatOutput) +Channel = SM16716.class_("Channel", output.FloatOutput) -CONF_SM16716_ID = 'sm16716_id' -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716), - cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), -}).extend(cv.COMPONENT_SCHEMA) +CONF_SM16716_ID = "sm16716_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield output.register_output(var, config) + await output.register_output(var, config) - parent = yield cg.get_variable(config[CONF_SM16716_ID]) + parent = await cg.get_variable(config[CONF_SM16716_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp index bc8e4fc1f4..373fbd4766 100644 --- a/esphome/components/sm16716/sm16716.cpp +++ b/esphome/components/sm16716/sm16716.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sm16716 { -static const char *TAG = "sm16716"; +static const char *const TAG = "sm16716"; void SM16716::setup() { ESP_LOGCONFIG(TAG, "Setting up SM16716OutputComponent..."); 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/__init__.py b/esphome/components/sm2135/__init__.py new file mode 100644 index 0000000000..68a9094518 --- /dev/null +++ b/esphome/components/sm2135/__init__.py @@ -0,0 +1,33 @@ +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, +) + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@BoukeHaarsma23"] + +sm2135_ns = cg.esphome_ns.namespace("sm2135") +SM2135 = sm2135_ns.class_("SM2135", cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM2135), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + 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)) diff --git a/esphome/components/sm2135/output.py b/esphome/components/sm2135/output.py new file mode 100644 index 0000000000..5cd969c6a5 --- /dev/null +++ b/esphome/components/sm2135/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 SM2135 + +DEPENDENCIES = ["sm2135"] +CODEOWNERS = ["@BoukeHaarsma23"] + +Channel = SM2135.class_("Channel", output.FloatOutput) + +CONF_SM2135_ID = "sm2135_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM2135_ID): cv.use_id(SM2135), + 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_SM2135_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp new file mode 100644 index 0000000000..8bd64972c9 --- /dev/null +++ b/esphome/components/sm2135/sm2135.cpp @@ -0,0 +1,81 @@ +#include "sm2135.h" +#include "esphome/core/log.h" + +// Tnx to the work of https://github.com/arendst (Tasmota) for making the initial version of the driver + +namespace esphome { +namespace sm2135 { + +static const char *const TAG = "sm2135"; + +static const uint8_t SM2135_ADDR_MC = 0xC0; // Max current register +static const uint8_t SM2135_ADDR_CH = 0xC1; // RGB or CW channel select register +static const uint8_t SM2135_ADDR_R = 0xC2; // Red color +static const uint8_t SM2135_ADDR_G = 0xC3; // Green color +static const uint8_t SM2135_ADDR_B = 0xC4; // Blue color +static const uint8_t SM2135_ADDR_C = 0xC5; // Cold +static const uint8_t SM2135_ADDR_W = 0xC6; // Warm + +static const uint8_t SM2135_RGB = 0x00; // RGB channel +static const uint8_t SM2135_CW = 0x80; // CW channel (Chip default) + +static const uint8_t SM2135_10MA = 0x00; +static const uint8_t SM2135_15MA = 0x01; +static const uint8_t SM2135_20MA = 0x02; // RGB max current (Chip default) +static const uint8_t SM2135_25MA = 0x03; +static const uint8_t SM2135_30MA = 0x04; // CW max current (Chip default) +static const uint8_t SM2135_35MA = 0x05; +static const uint8_t SM2135_40MA = 0x06; +static const uint8_t SM2135_45MA = 0x07; // Max value for RGB +static const uint8_t SM2135_50MA = 0x08; +static const uint8_t SM2135_55MA = 0x09; +static const uint8_t SM2135_60MA = 0x0A; + +static const uint8_t SM2135_CURRENT = (SM2135_20MA << 4) | SM2135_10MA; + +void SM2135::setup() { + ESP_LOGCONFIG(TAG, "Setting up SM2135OutputComponent..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->pwm_amounts_.resize(5, 0); +} +void SM2135::dump_config() { + ESP_LOGCONFIG(TAG, "SM2135:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); +} + +void SM2135::loop() { + if (!this->update_) + return; + + uint8_t data[6]; + if (this->update_channel_ == 3 || this->update_channel_ == 4) { + // No color so must be Cold/Warm + data[0] = SM2135_ADDR_MC; + data[1] = SM2135_CURRENT; + data[2] = SM2135_CW; + this->write_buffer_(data, 3); + delay(1); + data[0] = SM2135_ADDR_C; + data[1] = this->pwm_amounts_[4]; // Warm + data[2] = this->pwm_amounts_[3]; // Cold + this->write_buffer_(data, 3); + } else { + // Color + data[0] = SM2135_ADDR_MC; + data[1] = SM2135_CURRENT; + data[2] = SM2135_RGB; + data[3] = this->pwm_amounts_[1]; // Green + data[4] = this->pwm_amounts_[0]; // Red + data[5] = this->pwm_amounts_[2]; // Blue + this->write_buffer_(data, 6); + } + + this->update_ = false; +} + +} // namespace sm2135 +} // namespace esphome diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h new file mode 100644 index 0000000000..0277e9ba1c --- /dev/null +++ b/esphome/components/sm2135/sm2135.h @@ -0,0 +1,83 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace sm2135 { + +class SM2135 : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + + 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(SM2135 *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0xff); + this->parent_->set_channel_value_(this->channel_, amount); + } + + SM2135 *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint8_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; + } + void write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); + } + + void write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); + } + + void write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); + } + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t update_channel_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace sm2135 +} // namespace esphome diff --git a/esphome/components/sm300d2/__init__.py b/esphome/components/sm300d2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py new file mode 100644 index 0000000000..0c3c54f200 --- /dev/null +++ b/esphome/components/sm300d2/sensor.py @@ -0,0 +1,117 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_CO2, + CONF_FORMALDEHYDE, + CONF_TVOC, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_TEMPERATURE, + CONF_HUMIDITY, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_CELSIUS, + UNIT_PERCENT, + ICON_MOLECULE_CO2, + ICON_FLASK, + ICON_CHEMICAL_WEAPON, + ICON_GRAIN, +) + +DEPENDENCIES = ["uart"] + +sm300d2_ns = cg.esphome_ns.namespace("sm300d2") +SM300D2Sensor = sm300d2_ns.class_("SM300D2Sensor", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM300D2Sensor), + 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_FORMALDEHYDE): sensor.sensor_schema( + 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_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_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_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_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(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) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + if CONF_FORMALDEHYDE in config: + sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE]) + cg.add(var.set_formaldehyde_sensor(sens)) + if CONF_TVOC in config: + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc_sensor(sens)) + 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)) + if CONF_PM_10_0 in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp new file mode 100644 index 0000000000..c726faec48 --- /dev/null +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -0,0 +1,108 @@ +#include "sm300d2.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm300d2 { + +static const char *const TAG = "sm300d2"; +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(); + + bool read_success = read_array(response, SM300D2_RESPONSE_LENGTH); + + if (!read_success) { + ESP_LOGW(TAG, "Reading data from SM300D2 failed!"); + status_set_warning(); + return; + } + + if (response[0] != 0x3C || response[1] != 0x02) { + ESP_LOGW(TAG, "Invalid preamble for SM300D2 response!"); + this->status_set_warning(); + return; + } + + uint16_t calculated_checksum = this->sm300d2_checksum_(response); + // 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(); + return; + } + + this->status_clear_warning(); + + ESP_LOGW(TAG, "Successfully read SM300D2 data"); + + const uint16_t co2 = (response[2] * 256) + response[3]; + const uint16_t formaldehyde = (response[4] * 256) + response[5]; + 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]; + // A negative value is indicated by adding 0x80 (128) to the temperature value + const float temperature = ((response[12] + (response[13] * 0.1f)) > 128) + ? (((response[12] + (response[13] * 0.1f)) - 128) * -1) + : response[12] + (response[13] * 0.1f); + const float humidity = response[14] + (response[15] * 0.1f); + + ESP_LOGD(TAG, "Received CO₂: %u ppm", co2); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + + ESP_LOGD(TAG, "Received Formaldehyde: %u µg/m³", formaldehyde); + if (this->formaldehyde_sensor_ != nullptr) + this->formaldehyde_sensor_->publish_state(formaldehyde); + + ESP_LOGD(TAG, "Received TVOC: %u µg/m³", tvoc); + if (this->tvoc_sensor_ != nullptr) + this->tvoc_sensor_->publish_state(tvoc); + + ESP_LOGD(TAG, "Received PM2.5: %u µg/m³", pm_2_5); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + + 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); + + ESP_LOGD(TAG, "Received Temperature: %.2f °C", temperature); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + + ESP_LOGD(TAG, "Received Humidity: %.2f percent", humidity); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); +} + +uint16_t SM300D2Sensor::sm300d2_checksum_(uint8_t *ptr) { + uint8_t sum = 0; + for (int i = 0; i < (SM300D2_RESPONSE_LENGTH - 1); i++) { + sum += *ptr++; + } + return sum; +} + +void SM300D2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "SM300D2:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + this->check_uart_settings(9600); +} + +} // namespace sm300d2 +} // namespace esphome diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h new file mode 100644 index 0000000000..88c04e9813 --- /dev/null +++ b/esphome/components/sm300d2/sm300d2.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace sm300d2 { + +class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } + void set_tvoc_sensor(sensor::Sensor *tvoc_sensor) { tvoc_sensor_ = tvoc_sensor; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_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; } + + void update() override; + void dump_config() override; + + protected: + uint16_t sm300d2_checksum_(uint8_t *ptr); + + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *formaldehyde_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace sm300d2 +} // namespace esphome diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py new file mode 100644 index 0000000000..630abc8bca --- /dev/null +++ b/esphome/components/sn74hc595/__init__.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +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 = [] +MULTI_CONF = True + +sn74hc595_ns = cg.esphome_ns.namespace("sn74hc595") + +SN74HC595Component = sn74hc595_ns.class_("SN74HC595Component", cg.Component) +SN74HC595GPIOPin = sn74hc595_ns.class_("SN74HC595GPIOPin", cg.GPIOPin) + +CONF_SN74HC595 = "sn74hc595" +CONF_LATCH_PIN = "latch_pin" +CONF_OE_PIN = "oe_pin" +CONF_SR_COUNT = "sr_count" +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(SN74HC595Component), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LATCH_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(1, 4), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data_pin)) + clock_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock_pin)) + latch_pin = await cg.gpio_pin_expression(config[CONF_LATCH_PIN]) + cg.add(var.set_latch_pin(latch_pin)) + if CONF_OE_PIN in config: + oe_pin = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe_pin)) + cg.add(var.set_sr_count(config[CONF_SR_COUNT])) + + +def _validate_output_mode(value): + if value is not True: + 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_range(min=0, max=31), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_OUTPUT, default=True): cv.All( + cv.boolean, _validate_output_mode + ), + }, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, SN74HC595_PIN_SCHEMA) +async def sn74hc595_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_SN74HC595]) + 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 new file mode 100644 index 0000000000..5ebf50e5cb --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -0,0 +1,75 @@ +#include "sn74hc595.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sn74hc595 { + +static const char *const TAG = "sn74hc595"; + +void SN74HC595Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); + + if (this->have_oe_pin_) { // disable output + this->oe_pin_->setup(); + this->oe_pin_->digital_write(true); + } + + // initialize output pins + this->clock_pin_->setup(); + this->data_pin_->setup(); + 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_(); +} + +void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } + +bool SN74HC595Component::digital_read_(uint8_t pin) { return this->output_bits_ >> pin; } + +void SN74HC595Component::digital_write_(uint8_t pin, bool value) { + uint32_t mask = 1UL << pin; + this->output_bits_ &= ~mask; + if (value) + this->output_bits_ |= mask; + this->write_gpio_(); +} + +bool SN74HC595Component::write_gpio_() { + for (int i = this->sr_count_ - 1; i >= 0; i--) { + uint8_t data = (uint8_t)(this->output_bits_ >> (8 * i) & 0xff); + for (int j = 0; j < 8; j++) { + this->data_pin_->digital_write(data & (1 << (7 - j))); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } + } + + // pulse latch to activate new values + this->latch_pin_->digital_write(true); + this->latch_pin_->digital_write(false); + + // enable output if configured + if (this->have_oe_pin_) { + this->oe_pin_->digital_write(false); + } + + return true; +} + +float SN74HC595Component::get_setup_priority() const { return setup_priority::IO; } + +void SN74HC595GPIOPin::digital_write(bool value) { + this->parent_->digital_write_(this->pin_, value != this->inverted_); +} +std::string SN74HC595GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via SN74HC595", pin_); + return buffer; +} + +} // namespace sn74hc595 +} // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h new file mode 100644 index 0000000000..784019c3a6 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sn74hc595 { + +class SN74HC595Component : public Component { + public: + SN74HC595Component() = default; + + void setup() override; + float get_setup_priority() const override; + void dump_config() override; + + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_clock_pin(GPIOPin *pin) { clock_pin_ = pin; } + void set_latch_pin(GPIOPin *pin) { latch_pin_ = pin; } + void set_oe_pin(GPIOPin *pin) { + oe_pin_ = pin; + have_oe_pin_ = true; + } + void set_sr_count(uint8_t count) { sr_count_ = count; } + + protected: + friend class SN74HC595GPIOPin; + bool digital_read_(uint8_t pin); + void digital_write_(uint8_t pin, bool value); + bool write_gpio_(); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *latch_pin_; + GPIOPin *oe_pin_; + uint8_t sr_count_; + bool have_oe_pin_{false}; + uint32_t output_bits_{0x00}; +}; + +/// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. +class SN74HC595GPIOPin : public GPIOPin { + public: + void setup() override {} + void pin_mode(gpio::Flags flags) override {} + bool digital_read() override { return false; } + void digital_write(bool value) override; + std::string dump_summary() const override; + + 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 +} // namespace esphome diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index c10a3e5ac3..21fcb96842 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -1,27 +1,35 @@ #include "sntp_component.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include "lwip/apps/sntp.h" +#ifdef USE_ESP_IDF +#include "esp_sntp.h" #endif -#ifdef ARDUINO_ARCH_ESP8266 +#endif +#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 { -static const char *TAG = "sntp"; +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 @@ -32,11 +40,10 @@ void SNTPComponent::setup() { if (!this->server_3_.empty()) { sntp_setservername(2, strdup(this->server_3_.c_str())); } - -#ifdef ARDUINO_ARCH_ESP8266 - // let localtime/gmtime handle timezones, not sntp - sntp_set_timezone(0); +#ifdef USE_ESP_IDF + sntp_set_sync_interval(this->get_update_interval()); #endif + sntp_init(); } void SNTPComponent::dump_config() { @@ -46,6 +53,16 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Server 3: '%s'", this->server_3_.c_str()); ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } +void SNTPComponent::update() { +#ifndef USE_ESP_IDF + // force resync + if (sntp_enabled()) { + sntp_stop(); + this->has_time_ = false; + sntp_init(); + } +#endif +} void SNTPComponent::loop() { if (this->has_time_) return; @@ -54,9 +71,9 @@ 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: %04d-%02d-%02d %02d:%02d:%02d", 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/sntp_component.h b/esphome/components/sntp/sntp_component.h index 785f458d6c..4c70a6b09f 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -24,6 +24,7 @@ class SNTPComponent : public time::RealTimeClock { } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void update() override; void loop() override; protected: diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 798600075e..b1362f5421 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,29 +1,37 @@ from esphome.components import time as time_ import esphome.config_validation as cv import esphome.codegen as cg +from esphome.core import CORE from esphome.const import CONF_ID, CONF_SERVERS -DEPENDENCIES = ['network'] -sntp_ns = cg.esphome_ns.namespace('sntp') -SNTPComponent = sntp_ns.class_('SNTPComponent', time_.RealTimeClock) +DEPENDENCIES = ["network"] +sntp_ns = cg.esphome_ns.namespace("sntp") +SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] -CONFIG_SCHEMA = time_.TIME_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SNTPComponent), - cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): - cv.All(cv.ensure_list(cv.domain), cv.Length(min=1, max=3)), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SNTPComponent), + cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( + cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) + ), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) servers = config[CONF_SERVERS] - servers += [''] * (3 - len(servers)) + servers += [""] * (3 - len(servers)) cg.add(var.set_servers(*servers)) - yield cg.register_component(var, config) - yield time_.register_time(var, config) + await cg.register_component(var, config) + await time_.register_time(var, config) + + if CORE.is_esp8266 and len(servers) > 1: + # We need LwIP features enabled to get 3 SNTP servers (not just one) + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY") 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..d57413c739 --- /dev/null +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -0,0 +1,574 @@ +#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) { + return 0; + } + 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; + } + + if (read == 0) { + errno = EWOULDBLOCK; + return -1; + } + + 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 ((size_t) 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 ((size_t) 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/__init__.py b/esphome/components/speed/__init__.py index 7c7d64ed14..b75374863b 100644 --- a/esphome/components/speed/__init__.py +++ b/esphome/components/speed/__init__.py @@ -1,3 +1,3 @@ import esphome.codegen as cg -speed_ns = cg.esphome_ns.namespace('speed') +speed_ns = cg.esphome_ns.namespace("speed") diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 65ee5960f0..0ee31c76a0 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,32 +1,44 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, \ - CONF_OUTPUT_ID, CONF_SPEED, CONF_LOW, CONF_MEDIUM, CONF_HIGH +from esphome.const import ( + CONF_OSCILLATION_OUTPUT, + CONF_OUTPUT, + CONF_DIRECTION_OUTPUT, + CONF_OUTPUT_ID, + CONF_SPEED, + CONF_SPEED_COUNT, +) from .. import speed_ns -SpeedFan = speed_ns.class_('SpeedFan', cg.Component) +SpeedFan = speed_ns.class_("SpeedFan", cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_SPEED, default={}): cv.Schema({ - cv.Optional(CONF_LOW, default=0.33): cv.percentage, - cv.Optional(CONF_MEDIUM, default=0.66): cv.percentage, - cv.Optional(CONF_HIGH, default=1.0): cv.percentage, - }), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), + cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_SPEED): cv.invalid( + "Configuring individual speeds is deprecated." + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): - output_ = yield cg.get_variable(config[CONF_OUTPUT]) - state = yield fan.create_fan_state(config) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], state, output_) - yield cg.register_component(var, config) - speeds = config[CONF_SPEED] - cg.add(var.set_speeds(speeds[CONF_LOW], speeds[CONF_MEDIUM], speeds[CONF_HIGH])) +async def to_code(config): + output_ = await cg.get_variable(config[CONF_OUTPUT]) + state = await fan.create_fan_state(config) + var = cg.new_Pvariable( + config[CONF_OUTPUT_ID], state, output_, config[CONF_SPEED_COUNT] + ) + await cg.register_component(var, config) if CONF_OSCILLATION_OUTPUT in config: - oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) + oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3bfbc1fc3c..cb10db4ed4 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -1,19 +1,23 @@ #include "speed_fan.h" +#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { namespace speed { -static const char *TAG = "speed.fan"; +static const char *const TAG = "speed.fan"; void SpeedFan::dump_config() { ESP_LOGCONFIG(TAG, "Fan '%s':", this->fan_->get_name().c_str()); if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void SpeedFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -26,12 +30,7 @@ void SpeedFan::loop() { { float speed = 0.0f; if (this->fan_->state) { - if (this->fan_->speed == fan::FAN_SPEED_LOW) - speed = this->low_speed_; - else if (this->fan_->speed == fan::FAN_SPEED_MEDIUM) - speed = this->medium_speed_; - else if (this->fan_->speed == fan::FAN_SPEED_HIGH) - speed = this->high_speed_; + speed = static_cast(this->fan_->speed) / static_cast(this->speed_count_); } ESP_LOGD(TAG, "Setting speed: %.2f", speed); this->output_->set_level(speed); @@ -46,8 +45,21 @@ void SpeedFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + 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/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 74910bda94..6b7fa0b0f2 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -10,26 +10,22 @@ namespace speed { class SpeedFan : public Component { public: - SpeedFan(fan::FanState *fan, output::FloatOutput *output) : fan_(fan), output_(output) {} + SpeedFan(fan::FanState *fan, output::FloatOutput *output, int speed_count) + : fan_(fan), output_(output), speed_count_(speed_count) {} void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } - void set_speeds(float low, float medium, float high) { - this->low_speed_ = low; - this->medium_speed_ = medium; - this->high_speed_ = high; - } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } protected: fan::FanState *fan_; output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; - float low_speed_{}; - float medium_speed_{}; - float high_speed_{}; + output::BinaryOutput *direction_{nullptr}; bool next_update_{true}; + int speed_count_{}; }; } // namespace speed diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 69899d1e84..c917fe1ad8 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,48 +1,98 @@ 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, CONF_ID, CONF_MISO_PIN, CONF_MOSI_PIN, CONF_SPI_ID, \ - CONF_CS_PIN -from esphome.core import coroutine, coroutine_with_priority +from esphome.const import ( + CONF_CLK_PIN, + CONF_ID, + CONF_MISO_PIN, + CONF_MOSI_PIN, + CONF_SPI_ID, + CONF_CS_PIN, +) +from esphome.core import coroutine_with_priority, CORE -spi_ns = cg.esphome_ns.namespace('spi') -SPIComponent = spi_ns.class_('SPIComponent', cg.Component) -SPIDevice = spi_ns.class_('SPIDevice') +CODEOWNERS = ["@esphome/core"] +spi_ns = cg.esphome_ns.namespace("spi") +SPIComponent = spi_ns.class_("SPIComponent", cg.Component) +SPIDevice = spi_ns.class_("SPIDevice") MULTI_CONF = True -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(SPIComponent), - cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, -}), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN)) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SPIComponent), + cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, + } + ), + cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN), +) @coroutine_with_priority(1.0) -def to_code(config): +async def to_code(config): cg.add_global(spi_ns.using) var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_clk(clk)) if CONF_MISO_PIN in config: - miso = yield cg.gpio_pin_expression(config[CONF_MISO_PIN]) + miso = await cg.gpio_pin_expression(config[CONF_MISO_PIN]) cg.add(var.set_miso(miso)) if CONF_MOSI_PIN in config: - mosi = yield cg.gpio_pin_expression(config[CONF_MOSI_PIN]) + mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) - -SPI_DEVICE_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, -}) + if CORE.is_esp32 and CORE.using_arduino: + cg.add_library("SPI", None) + if CORE.is_esp8266: + cg.add_library("SPI", None) -@coroutine -def register_spi_device(var, config): - parent = yield cg.get_variable(config[CONF_SPI_ID]) +def spi_device_schema(cs_pin_required=True): + """Create a schema for an SPI device. + :param cs_pin_required: If true, make the CS_PIN required in the config. + :return: The SPI device schema, `extend` this in your config schema. + """ + schema = { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + } + if cs_pin_required: + schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema + else: + schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema + return cv.Schema(schema) + + +async def register_spi_device(var, config): + parent = await cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) - cg.add(var.set_cs_pin(pin)) + if CONF_CS_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(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 bf2a18955a..d427e2c91b 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -6,41 +6,58 @@ namespace esphome { namespace spi { -static const char *TAG = "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(); } - ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); - this->active_cs_->digital_write(true); - this->active_cs_ = nullptr; +#endif // USE_SPI_ARDUINO_BACKEND + if (this->active_cs_) { + this->active_cs_->digital_write(true); + this->active_cs_ = nullptr; + } } void SPIComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); 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 (clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) { - // pass - } else if (clk_pin == 14 && miso_pin == 12 && mosi_pin == 13) { - // pass - } else { + if (has_mosi && !mosi_->is_internal()) use_hw_spi = false; + if (use_hw_spi) { + auto *clk_internal = (InternalGPIOPin *) clk_; + auto *miso_internal = (InternalGPIOPin *) miso_; + auto *mosi_internal = (InternalGPIOPin *) mosi_; + + if (clk_internal->is_inverted()) + use_hw_spi = false; + if (has_miso && miso_internal->is_inverted()) + use_hw_spi = false; + if (has_mosi && mosi_internal->is_inverted()) + use_hw_spi = false; + + if (use_hw_spi) { + clk_pin = clk_internal->get_pin(); + miso_pin = has_miso ? miso_internal->get_pin() : -1; + mosi_pin = has_mosi ? mosi_internal->get_pin() : -1; + } } +#ifdef USE_ESP8266 + if (!(clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) && + !(clk_pin == 14 && (!has_miso || miso_pin == 12) && (!has_mosi || mosi_pin == 13))) + use_hw_spi = false; if (use_hw_spi) { this->hw_spi_ = &SPI; @@ -48,8 +65,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; @@ -59,13 +76,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(); @@ -80,32 +98,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) { @@ -151,15 +165,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 ccef6192f3..6c3fd17e56 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 { @@ -50,11 +58,15 @@ enum SPIClockPhase { */ enum SPIDataRate : uint32_t { DATA_RATE_1KHZ = 1000, + DATA_RATE_75KHZ = 75000, DATA_RATE_200KHZ = 200000, DATA_RATE_1MHZ = 1000000, DATA_RATE_2MHZ = 2000000, DATA_RATE_4MHZ = 4000000, DATA_RATE_8MHZ = 8000000, + DATA_RATE_10MHZ = 10000000, + DATA_RATE_20MHZ = 20000000, + DATA_RATE_40MHZ = 40000000, }; class SPIComponent : public Component { @@ -68,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(); } @@ -87,20 +103,52 @@ 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); + } + + 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]); + } + } + 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]); } @@ -108,38 +156,68 @@ class SPIComponent : public Component { template uint8_t transfer_byte(uint8_t data) { - if (this->hw_spi_ != nullptr) { - return this->hw_spi_->transfer(data); +#ifdef USE_SPI_ARDUINO_BACKEND + if (this->miso_ != nullptr) { + if (this->hw_spi_ != nullptr) { + return this->hw_spi_->transfer(data); + } else { + return this->transfer_(data); + } } - 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) { - this->hw_spi_->transfer(data, length); + if (this->miso_ != nullptr) { + this->hw_spi_->transfer(data, length); + } else { + this->hw_spi_->writeBytes(data, length); + } return; } - for (size_t i = 0; i < length; i++) { - data[i] = this->transfer_byte(data[i]); +#endif // USE_SPI_ARDUINO_BACKEND + + if (this->miso_ != nullptr) { + for (size_t i = 0; i < length; i++) { + data[i] = this->transfer_byte(data[i]); + } + } else { + this->write_array(data, length); } } template void enable(GPIOPin *cs) { - 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); + uint8_t data_mode = SPI_MODE0; + if (!CLOCK_POLARITY && CLOCK_PHASE) { + data_mode = SPI_MODE1; + } else if (CLOCK_POLARITY && !CLOCK_PHASE) { + data_mode = SPI_MODE2; + } else if (CLOCK_POLARITY && CLOCK_PHASE) { + data_mode = SPI_MODE3; + } 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 - this->active_cs_ = cs; - this->active_cs_->digital_write(false); + if (cs != nullptr) { + this->active_cs_ = cs; + this->active_cs_->digital_write(false); + } } void disable(); @@ -149,10 +227,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); @@ -160,7 +234,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_; }; @@ -174,8 +250,10 @@ class SPIDevice { void set_cs_pin(GPIOPin *cs) { cs_ = cs; } void spi_setup() { - this->cs_->setup(); - this->cs_->digital_write(true); + if (this->cs_) { + this->cs_->setup(); + this->cs_->digital_write(true); + } } void enable() { this->parent_->template enable(this->cs_); } @@ -198,6 +276,14 @@ class SPIDevice { return this->parent_->template write_byte(data); } + void write_byte16(uint16_t data) { + return this->parent_->template write_byte16(data); + } + + void write_array16(const uint16_t *data, size_t length) { + this->parent_->template write_array16(data, length); + } + void write_array(const uint8_t *data, size_t length) { this->parent_->template write_array(data, length); } diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index c45758be4e..27264cf942 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -1,82 +1,150 @@ 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_4_0, CONF_PM_10_0, \ - CONF_PMC_0_5, CONF_PMC_1_0, CONF_PMC_2_5, CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, \ - UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, UNIT_MICROMETER, \ - ICON_CHEMICAL_WEAPON, ICON_COUNTER, ICON_RULER +from esphome.const import ( + CONF_ID, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_4_0, + CONF_PM_10_0, + CONF_PMC_0_5, + CONF_PMC_1_0, + CONF_PMC_2_5, + CONF_PMC_4_0, + CONF_PMC_10_0, + CONF_PM_SIZE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_COUNTS_PER_CUBIC_METER, + UNIT_MICROMETER, + ICON_CHEMICAL_WEAPON, + ICON_COUNTER, + ICON_RULER, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -sps30_ns = cg.esphome_ns.namespace('sps30') -SPS30Component = sps30_ns.class_('SPS30Component', cg.PollingComponent, i2c.I2CDevice) +sps30_ns = cg.esphome_ns.namespace("sps30") +SPS30Component = sps30_ns.class_("SPS30Component", cg.PollingComponent, i2c.I2CDevice) -CONFIG_SCHEMA = cv.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), - cv.Optional(CONF_PM_2_5): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, 2), - cv.Optional(CONF_PM_4_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, 2), - cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, 2), - cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, 2), - cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, 2), - cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, 2), - cv.Optional(CONF_PMC_4_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, 2), - cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, 2), - cv.Optional(CONF_PM_SIZE): sensor.sensor_schema(UNIT_MICROMETER, - ICON_RULER, 0), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x69)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SPS30Component), + 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_4_0): sensor.sensor_schema( + 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_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_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_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_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_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_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_of_measurement=UNIT_MICROMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x69)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_PM_1_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + sens = await sensor.new_sensor(config[CONF_PM_1_0]) cg.add(var.set_pm_1_0_sensor(sens)) if CONF_PM_2_5 in config: - sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + sens = await sensor.new_sensor(config[CONF_PM_2_5]) cg.add(var.set_pm_2_5_sensor(sens)) if CONF_PM_4_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_4_0]) + sens = await sensor.new_sensor(config[CONF_PM_4_0]) cg.add(var.set_pm_4_0_sensor(sens)) if CONF_PM_10_0 in config: - sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) if CONF_PMC_0_5 in config: - sens = yield sensor.new_sensor(config[CONF_PMC_0_5]) + sens = await sensor.new_sensor(config[CONF_PMC_0_5]) cg.add(var.set_pmc_0_5_sensor(sens)) if CONF_PMC_1_0 in config: - sens = yield sensor.new_sensor(config[CONF_PMC_1_0]) + sens = await sensor.new_sensor(config[CONF_PMC_1_0]) cg.add(var.set_pmc_1_0_sensor(sens)) if CONF_PMC_2_5 in config: - sens = yield sensor.new_sensor(config[CONF_PMC_2_5]) + sens = await sensor.new_sensor(config[CONF_PMC_2_5]) cg.add(var.set_pmc_2_5_sensor(sens)) if CONF_PMC_4_0 in config: - sens = yield sensor.new_sensor(config[CONF_PMC_4_0]) + sens = await sensor.new_sensor(config[CONF_PMC_4_0]) cg.add(var.set_pmc_4_0_sensor(sens)) if CONF_PMC_10_0 in config: - sens = yield sensor.new_sensor(config[CONF_PMC_10_0]) + sens = await sensor.new_sensor(config[CONF_PMC_10_0]) cg.add(var.set_pmc_10_0_sensor(sens)) if CONF_PM_SIZE in config: - sens = yield sensor.new_sensor(config[CONF_PM_SIZE]) + sens = await sensor.new_sensor(config[CONF_PM_SIZE]) cg.add(var.set_pm_size_sensor(sens)) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 181bf44189..6160120564 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sps30 { -static const char *TAG = "sps30"; +static const char *const TAG = "sps30"; static const uint16_t SPS30_CMD_GET_ARTICLE_CODE = 0xD025; static const uint16_t SPS30_CMD_GET_SERIAL_NUMBER = 0xD033; @@ -32,14 +32,11 @@ void SPS30Component::setup() { return; } - uint16_t raw_firmware_version[4]; - if (!this->read_data_(raw_firmware_version, 4)) { + if (!this->read_data_(&raw_firmware_version_, 1)) { this->error_code_ = FIRMWARE_VERSION_READ_FAILED; this->mark_failed(); return; } - ESP_LOGD(TAG, " Firmware version v%0d.%02d", (raw_firmware_version[0] >> 8), - uint16_t(raw_firmware_version[0] & 0xFF)); /// Serial number identification if (!this->write_command_(SPS30_CMD_GET_SERIAL_NUMBER)) { this->error_code_ = SERIAL_NUMBER_REQUEST_FAILED; @@ -59,6 +56,8 @@ void SPS30Component::setup() { this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; this->start_continuous_measurement_(); }); } @@ -93,10 +92,17 @@ void SPS30Component::dump_config() { } LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Serial Number: '%s'", this->serial_number_); - LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); - LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); - LOG_SENSOR(" ", "PM4", this->pm_4_0_sensor_); - LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); + ESP_LOGCONFIG(TAG, " Firmware version v%0d.%0d", (raw_firmware_version_ >> 8), + uint16_t(raw_firmware_version_ & 0xFF)); + LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM10 Weight Concentration", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "PM1.0 Number Concentration", this->pmc_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5 Number Concentration", this->pmc_2_5_sensor_); + LOG_SENSOR(" ", "PM4 Number Concentration", this->pmc_4_0_sensor_); + LOG_SENSOR(" ", "PM10 Number Concentration", this->pmc_10_0_sensor_); + LOG_SENSOR(" ", "PM typical size", this->pm_size_sensor_); } void SPS30Component::update() { @@ -123,8 +129,8 @@ void SPS30Component::update() { return; } - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + uint16_t raw_read_status; + if (!this->read_data_(&raw_read_status, 1) || raw_read_status == 0x00) { ESP_LOGD(TAG, "Sensor measurement not ready yet."); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked @@ -242,10 +248,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 +259,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/sps30/sps30.h b/esphome/components/sps30/sps30.h index 2f977252a5..bae33a46e1 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -33,6 +33,7 @@ class SPS30Component : public PollingComponent, public i2c::I2CDevice { bool read_data_(uint16_t *data, uint8_t len); uint8_t sht_crc_(uint8_t data1, uint8_t data2); char serial_number_[17] = {0}; /// Terminating NULL character + uint16_t raw_firmware_version_; bool start_continuous_measurement_(); uint8_t skipped_data_read_cycles_ = 0; diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 047ddddcac..e4f62e5ff9 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -2,49 +2,98 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN, \ - CONF_BRIGHTNESS -from esphome.core import coroutine +from esphome.const import ( + CONF_EXTERNAL_VCC, + CONF_LAMBDA, + 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') +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, - 'SSD1306_96X16': SSD1306Model.SSD1306_MODEL_96_16, - 'SSD1306_64X48': SSD1306Model.SSD1306_MODEL_64_48, - 'SH1106_128X32': SSD1306Model.SH1106_MODEL_128_32, - 'SH1106_128X64': SSD1306Model.SH1106_MODEL_128_64, - 'SH1106_96X16': SSD1306Model.SH1106_MODEL_96_16, - 'SH1106_64X48': SSD1306Model.SH1106_MODEL_64_48, + "SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32, + "SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64, + "SSD1306_96X16": SSD1306Model.SSD1306_MODEL_96_16, + "SSD1306_64X48": SSD1306Model.SSD1306_MODEL_64_48, + "SSD1306_64X32": SSD1306Model.SSD1306_MODEL_64_32, + "SH1106_128X32": SSD1306Model.SH1106_MODEL_128_32, + "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="_") -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_EXTERNAL_VCC): cv.boolean, -}).extend(cv.polling_component_schema('1s')) + +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 -@coroutine -def setup_ssd1036(var, config): - yield cg.register_component(var, config) - yield display.register_display(var, config) +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_ssd1306(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) if CONF_RESET_PIN in config: - reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) if CONF_BRIGHTNESS in config: - cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + 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_Y])) + 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_ = yield cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index d60f7dc985..4b9feb10ce 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -5,13 +5,16 @@ namespace esphome { namespace ssd1306_base { -static const char *TAG = "sd1306"; +static const char *const TAG = "ssd1306"; + +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; @@ -26,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: @@ -64,47 +94,44 @@ void SSD1306::setup() { case SSD1306_MODEL_128_64: case SH1106_MODEL_128_64: case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: this->command(0x12); break; } - this->command(SSD1306_COMMAND_SET_CONTRAST); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - this->command(0x8F); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SH1106_MODEL_64_48: - this->command(int(255 * (this->brightness_))); - break; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - if (this->external_vcc_) - this->command(0x10); - else - this->command(0xAF); - 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); - this->command(SSD1306_COMMAND_DISPLAY_ON); + // Contrast and brighrness + // SSD1306 does not have brightness setting + set_contrast(this->contrast_); + if (this->is_ssd1305_()) + 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(); } void SSD1306::display() { if (this->is_sh1106_()) { @@ -115,12 +142,13 @@ 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); + case SSD1306_MODEL_64_32: + 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; } @@ -136,17 +164,48 @@ 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 + 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() { + this->command(SSD1306_COMMAND_DISPLAY_ON); + this->is_on_ = true; +} +void SSD1306::turn_off() { + this->command(SSD1306_COMMAND_DISPLAY_OFF); + this->is_on_ = false; +} int SSD1306::get_height_internal() { switch (this->model_) { case SSD1306_MODEL_128_32: + case SSD1306_MODEL_64_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: @@ -164,11 +223,14 @@ 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: return 96; case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: return 64; default: @@ -178,21 +240,20 @@ int SSD1306::get_width_internal() { size_t SSD1306::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; } - -void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y & 0x07; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); } } -void SSD1306::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1306::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } @@ -214,6 +275,8 @@ const char *SSD1306::model_str_() { return "SSD1306 128x32"; case SSD1306_MODEL_128_64: return "SSD1306 128x64"; + case SSD1306_MODEL_64_32: + return "SSD1306 64x32"; case SSD1306_MODEL_96_16: return "SSD1306 96x16"; case SSD1306_MODEL_64_48: @@ -226,6 +289,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 128x64"; default: return "Unknown"; } diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 8adf3c1b87..c77b1985e4 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 { @@ -12,10 +12,13 @@ enum SSD1306Model { SSD1306_MODEL_128_64, SSD1306_MODEL_96_16, SSD1306_MODEL_64_48, + SSD1306_MODEL_64_32, SH1106_MODEL_128_32, 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,10 +32,20 @@ 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 set_brightness(float brightness) { this->brightness_ = brightness; } - + 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(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; @@ -40,8 +53,9 @@ 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, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; @@ -51,7 +65,14 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { SSD1306Model model_{SSD1306_MODEL_128_64}; 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 eaa656f26b..c51ab5f93e 100644 --- a/esphome/components/ssd1306_i2c/display.py +++ b/esphome/components/ssd1306_i2c/display.py @@ -1,21 +1,29 @@ 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'] -DEPENDENCIES = ['i2c'] +AUTO_LOAD = ["ssd1306_base"] +DEPENDENCIES = ["i2c"] -ssd1306_i2c = cg.esphome_ns.namespace('ssd1306_i2c') -I2CSSD1306 = ssd1306_i2c.class_('I2CSSD1306', ssd1306_base.SSD1306, i2c.I2CDevice) +ssd1306_i2c = cg.esphome_ns.namespace("ssd1306_i2c") +I2CSSD1306 = ssd1306_i2c.class_("I2CSSD1306", ssd1306_base.SSD1306, i2c.I2CDevice) -CONFIG_SCHEMA = cv.All(ssd1306_base.SSD1306_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(I2CSSD1306), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x3C)), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) +CONFIG_SCHEMA = cv.All( + ssd1306_base.SSD1306_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2CSSD1306), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x3C)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield ssd1306_base.setup_ssd1036(var, config) - yield i2c.register_i2c_device(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 34a963e532..64b09c0672 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -4,14 +4,14 @@ namespace esphome { namespace ssd1306_i2c { -static const char *TAG = "ssd1306_i2c"; +static const char *const TAG = "ssd1306_i2c"; void I2CSSD1306::setup() { ESP_LOGCONFIG(TAG, "Setting up I2C SSD1306..."); this->init_reset_(); - this->parent_->raw_begin_transmission(this->address_); - if (!this->parent_->raw_end_transmission(this->address_)) { + 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) { @@ -35,12 +40,12 @@ void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void HOT I2CSSD1306::write_display_data() { if (this->is_sh1106_()) { uint32_t i = 0; - for (uint8_t page = 0; page < this->get_height_internal() / 8; page++) { + for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { this->command(0xB0 + page); // row this->command(0x02); // lower column this->command(0x10); // higher column - for (uint8_t x = 0; x < this->get_width_internal() / 16; x++) { + for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { uint8_t data[16]; for (uint8_t &j : data) j = this->buffer_[i++]; diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 65f0df1c51..0af1168bde 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -2,25 +2,33 @@ 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'] -DEPENDENCIES = ['spi'] +AUTO_LOAD = ["ssd1306_base"] +DEPENDENCIES = ["spi"] -ssd1306_spi = cg.esphome_ns.namespace('ssd1306_spi') -SPISSD1306 = ssd1306_spi.class_('SPISSD1306', ssd1306_base.SSD1306, spi.SPIDevice) +ssd1306_spi = cg.esphome_ns.namespace("ssd1306_spi") +SPISSD1306 = ssd1306_spi.class_("SPISSD1306", ssd1306_base.SSD1306, spi.SPIDevice) -CONFIG_SCHEMA = cv.All(ssd1306_base.SSD1306_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SPISSD1306), - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) +CONFIG_SCHEMA = cv.All( + ssd1306_base.SSD1306_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPISSD1306), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield ssd1306_base.setup_ssd1036(var, config) - yield spi.register_spi_device(var, config) + await ssd1306_base.setup_ssd1306(var, config) + await spi.register_spi_device(var, config) - dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index d87f412f70..7f025d77cd 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace ssd1306_spi { -static const char *TAG = "ssd1306_spi"; +static const char *const TAG = "ssd1306_spi"; void SPISSD1306::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1306..."); @@ -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) { @@ -32,12 +37,12 @@ void SPISSD1306::command(uint8_t value) { } void HOT SPISSD1306::write_display_data() { if (this->is_sh1106_()) { - for (uint8_t y = 0; y < this->get_height_internal() / 8; y++) { + for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { this->command(0xB0 + y); this->command(0x02); this->command(0x10); this->dc_pin_->digital_write(true); - for (uint8_t x = 0; x < this->get_width_internal(); x++) { + for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { this->enable(); this->write_byte(this->buffer_[x + y * this->get_width_internal()]); this->disable(); diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py new file mode 100644 index 0000000000..434caf4e35 --- /dev/null +++ b/esphome/components/ssd1322_base/__init__.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_EXTERNAL_VCC, + CONF_LAMBDA, + CONF_MODEL, + CONF_RESET_PIN, +) + +CODEOWNERS = ["@kbx81"] + +ssd1322_base_ns = cg.esphome_ns.namespace("ssd1322_base") +SSD1322 = ssd1322_base_ns.class_("SSD1322", cg.PollingComponent, display.DisplayBuffer) +SSD1322Model = ssd1322_base_ns.enum("SSD1322Model") + +MODELS = { + "SSD1322_256X64": SSD1322Model.SSD1322_MODEL_256_64, +} + +SSD1322_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1322_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Required(CONF_MODEL): SSD1322_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_ssd1322(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_EXTERNAL_VCC in config: + cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp new file mode 100644 index 0000000000..520248a66e --- /dev/null +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -0,0 +1,204 @@ +#include "ssd1322_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1322_base { + +static const char *const TAG = "ssd1322"; + +static const uint8_t SSD1322_MAX_CONTRAST = 255; +static const uint8_t SSD1322_COLORMASK = 0x0f; +static const uint8_t SSD1322_COLORSHIFT = 4; +static const uint8_t SSD1322_PIXELSPERBYTE = 2; + +static const uint8_t SSD1322_ENABLEGRAYSCALETABLE = 0x00; +static const uint8_t SSD1322_SETCOLUMNADDRESS = 0x15; +static const uint8_t SSD1322_WRITERAM = 0x5C; +static const uint8_t SSD1322_READRAM = 0x5D; +static const uint8_t SSD1322_SETROWADDRESS = 0x75; +static const uint8_t SSD1322_SETREMAP = 0xA0; +static const uint8_t SSD1322_SETSTARTLINE = 0xA1; +static const uint8_t SSD1322_SETOFFSET = 0xA2; +static const uint8_t SSD1322_SETMODEALLOFF = 0xA4; +static const uint8_t SSD1322_SETMODEALLON = 0xA5; +static const uint8_t SSD1322_SETMODENORMAL = 0xA6; +static const uint8_t SSD1322_SETMODEINVERTED = 0xA7; +static const uint8_t SSD1322_ENABLEPARTIALDISPLAY = 0xA8; +static const uint8_t SSD1322_EXITPARTIALDISPLAY = 0xA9; +static const uint8_t SSD1322_SETFUNCTIONSELECTION = 0xAB; +static const uint8_t SSD1322_SETDISPLAYOFF = 0xAE; +static const uint8_t SSD1322_SETDISPLAYON = 0xAF; +static const uint8_t SSD1322_SETPHASELENGTH = 0xB1; +static const uint8_t SSD1322_SETFRONTCLOCKDIVIDER = 0xB3; +static const uint8_t SSD1322_DISPLAYENHANCEMENTA = 0xB4; +static const uint8_t SSD1322_SETGPIO = 0xB5; +static const uint8_t SSD1322_SETSECONDPRECHARGEPERIOD = 0xB6; +static const uint8_t SSD1322_SETGRAYSCALETABLE = 0xB8; +static const uint8_t SSD1322_SELECTDEFAULTLINEARGRAYSCALETABLE = 0xB9; +static const uint8_t SSD1322_SETPRECHARGEVOLTAGE = 0xBB; +static const uint8_t SSD1322_SETVCOMHVOLTAGE = 0xBE; +static const uint8_t SSD1322_SETCONTRAST = 0xC1; +static const uint8_t SSD1322_MASTERCURRENTCONTROL = 0xC7; +static const uint8_t SSD1322_SETMULTIPLEXRATIO = 0xCA; +static const uint8_t SSD1322_DISPLAYENHANCEMENTB = 0xD1; +static const uint8_t SSD1322_SETCOMMANDLOCK = 0xFD; + +static const uint8_t SSD1322_SETCOMMANDLOCK_UNLOCK = 0x12; +static const uint8_t SSD1322_SETCOMMANDLOCK_LOCK = 0x16; + +void SSD1322::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1322_SETCOMMANDLOCK); + this->data(SSD1322_SETCOMMANDLOCK_UNLOCK); + this->turn_off(); + this->command(SSD1322_SETFRONTCLOCKDIVIDER); + this->data(0x91); + this->command(SSD1322_SETMULTIPLEXRATIO); + this->data(0x3F); + this->command(SSD1322_SETOFFSET); + this->data(0x00); + this->command(SSD1322_SETSTARTLINE); + this->data(0x00); + this->command(SSD1322_SETREMAP); + this->data(0x14); + this->data(0x11); + this->command(SSD1322_SETGPIO); + this->data(0x00); + this->command(SSD1322_SETFUNCTIONSELECTION); + this->data(0x01); + this->command(SSD1322_DISPLAYENHANCEMENTA); + this->data(0xA0); + this->data(0xFD); + this->command(SSD1322_MASTERCURRENTCONTROL); + this->data(0x0F); + this->command(SSD1322_SETPHASELENGTH); + this->data(0xE2); + this->command(SSD1322_DISPLAYENHANCEMENTB); + this->data(0x82); + this->data(0x20); + this->command(SSD1322_SETPRECHARGEVOLTAGE); + this->data(0x1F); + this->command(SSD1322_SETSECONDPRECHARGEPERIOD); + this->data(0x08); + this->command(SSD1322_SETVCOMHVOLTAGE); + this->data(0x07); + this->command(SSD1322_SETMODENORMAL); + this->command(SSD1322_EXITPARTIALDISPLAY); + // this->command(SSD1322_SELECTDEFAULTLINEARGRAYSCALETABLE); + this->command(SSD1322_SETGRAYSCALETABLE); + // gamma ~2.2 + this->data(24); + this->data(29); + this->data(36); + this->data(43); + this->data(51); + this->data(60); + this->data(70); + this->data(81); + this->data(93); + this->data(105); + this->data(118); + this->data(132); + this->data(147); + this->data(163); + 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 +} +void SSD1322::display() { + this->command(SSD1322_SETCOLUMNADDRESS); // set column address + this->data(0x1C); // set column start address + this->data(0x5B); // set column end address + this->command(SSD1322_SETROWADDRESS); // set row address + this->data(0x00); // set row start address + this->data(0x3F); // set last row + this->command(SSD1322_WRITERAM); // write + + this->write_display_data(); +} +void SSD1322::update() { + this->do_update_(); + this->display(); +} +void SSD1322::set_brightness(float brightness) { + 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_))); +} +bool SSD1322::is_on() { return this->is_on_; } +void SSD1322::turn_on() { + this->command(SSD1322_SETDISPLAYON); + this->is_on_ = true; +} +void SSD1322::turn_off() { + this->command(SSD1322_SETDISPLAYOFF); + this->is_on_ = false; +} +int SSD1322::get_height_internal() { + switch (this->model_) { + case SSD1322_MODEL_256_64: + return 64; + default: + return 0; + } +} +int SSD1322::get_width_internal() { + switch (this->model_) { + case SSD1322_MODEL_256_64: + return 256; + default: + return 0; + } +} +size_t SSD1322::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1322_PIXELSPERBYTE; +} +void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1322_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1322_PIXELSPERBYTE); + uint8_t shift = (1u - (x % SSD1322_PIXELSPERBYTE)) * SSD1322_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1322_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1322_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; +} +void SSD1322::fill(Color color) { + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); + uint8_t fill = (color4 & SSD1322_COLORMASK) | ((color4 & SSD1322_COLORMASK) << SSD1322_COLORSHIFT); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1322::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1322::model_str_() { + switch (this->model_) { + case SSD1322_MODEL_256_64: + return "SSD1322 256x64"; + default: + return "Unknown"; + } +} + +} // namespace ssd1322_base +} // namespace esphome diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h new file mode 100644 index 0000000000..6a790c0199 --- /dev/null +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1322_base { + +enum SSD1322Model { + SSD1322_MODEL_256_64 = 0, +}; + +class SSD1322 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1322Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void data(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + 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_(); + const char *model_str_(); + + SSD1322Model model_{SSD1322_MODEL_256_64}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1322_base +} // namespace esphome diff --git a/esphome/components/ssd1322_spi/__init__.py b/esphome/components/ssd1322_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py new file mode 100644 index 0000000000..88b3a53355 --- /dev/null +++ b/esphome/components/ssd1322_spi/display.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1322_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@kbx81"] + +AUTO_LOAD = ["ssd1322_base"] +DEPENDENCIES = ["spi"] + +ssd1322_spi = cg.esphome_ns.namespace("ssd1322_spi") +SPISSD1322 = ssd1322_spi.class_("SPISSD1322", ssd1322_base.SSD1322, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + ssd1322_base.SSD1322_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPISSD1322), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ssd1322_base.setup_ssd1322(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.cpp b/esphome/components/ssd1322_spi/ssd1322_spi.cpp new file mode 100644 index 0000000000..50c46c4d02 --- /dev/null +++ b/esphome/components/ssd1322_spi/ssd1322_spi.cpp @@ -0,0 +1,72 @@ +#include "ssd1322_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1322_spi { + +static const char *const TAG = "ssd1322_spi"; + +void SPISSD1322::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1322..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1322::setup(); +} +void SPISSD1322::dump_config() { + LOG_DISPLAY("", "SPI SSD1322", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1322::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void SPISSD1322::data(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1322::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1322_spi +} // namespace esphome diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.h b/esphome/components/ssd1322_spi/ssd1322_spi.h new file mode 100644 index 0000000000..316742706e --- /dev/null +++ b/esphome/components/ssd1322_spi/ssd1322_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1322_base/ssd1322_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1322_spi { + +class SPISSD1322 : public ssd1322_base::SSD1322, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + void data(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1322_spi +} // namespace esphome diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 69e11ec0d1..68be287d2a 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -2,41 +2,54 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN -from esphome.core import coroutine +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_EXTERNAL_VCC, + CONF_LAMBDA, + CONF_MODEL, + CONF_RESET_PIN, +) -ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') -SSD1325 = ssd1325_base_ns.class_('SSD1325', cg.PollingComponent, display.DisplayBuffer) -SSD1325Model = ssd1325_base_ns.enum('SSD1325Model') +CODEOWNERS = ["@kbx81"] + +ssd1325_base_ns = cg.esphome_ns.namespace("ssd1325_base") +SSD1325 = ssd1325_base_ns.class_("SSD1325", cg.PollingComponent, display.DisplayBuffer) +SSD1325Model = ssd1325_base_ns.enum("SSD1325Model") MODELS = { - 'SSD1325_128X32': SSD1325Model.SSD1325_MODEL_128_32, - 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, - 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, - 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, + "SSD1325_128X32": SSD1325Model.SSD1325_MODEL_128_32, + "SSD1325_128X64": SSD1325Model.SSD1325_MODEL_128_64, + "SSD1325_96X16": SSD1325Model.SSD1325_MODEL_96_16, + "SSD1325_64X48": SSD1325Model.SSD1325_MODEL_64_48, + "SSD1327_128X128": SSD1325Model.SSD1327_MODEL_128_128, } SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") -SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ - cv.Required(CONF_MODEL): SSD1325_MODEL, - cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, -}).extend(cv.polling_component_schema('1s')) +SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Required(CONF_MODEL): SSD1325_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, + } +).extend(cv.polling_component_schema("1s")) -@coroutine -def setup_ssd1036(var, config): - yield cg.register_component(var, config) - yield display.register_display(var, config) +async def setup_ssd1325(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) if CONF_RESET_PIN in config: - reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 3079e19cc8..60e46f573f 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -5,10 +5,12 @@ namespace esphome { namespace ssd1325_base { -static const char *TAG = "ssd1325"; +static const char *const TAG = "ssd1325"; -static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; +static const uint8_t SSD1325_MAX_CONTRAST = 127; +static const uint8_t SSD1325_COLORMASK = 0x0f; +static const uint8_t SSD1325_COLORSHIFT = 4; +static const uint8_t SSD1325_PIXELSPERBYTE = 2; static const uint8_t SSD1325_SETCOLADDR = 0x15; static const uint8_t SSD1325_SETROWADDR = 0x75; @@ -33,6 +35,7 @@ static const uint8_t SSD1325_SETROWPERIOD = 0xB2; static const uint8_t SSD1325_SETCLOCK = 0xB3; static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETDEFAULTGRAYTABLE = 0xB9; static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; static const uint8_t SSD1325_SETVSL = 0xBF; @@ -44,31 +47,57 @@ static const uint8_t SSD1325_COPY = 0x25; void SSD1325::setup() { this->init_internal_(this->get_buffer_length_()); - this->command(SSD1325_DISPLAYOFF); /* display off */ - this->command(SSD1325_SETCLOCK); /* set osc division */ - this->command(0xF1); /* 145 */ - this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ - this->command(0x3f); /* duty = 1/64 */ - this->command(SSD1325_SETOFFSET); /* set display offset --- */ - this->command(0x4C); /* 76 */ - this->command(SSD1325_SETSTARTLINE); /*set start line */ - this->command(0x00); /* ------ */ - this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(SSD1325_DISPLAYOFF); // display off + this->command(SSD1325_SETCLOCK); // set osc division + this->command(0xF1); // 145 + this->command(SSD1325_SETMULTIPLEX); // multiplex ratio + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x7f); // duty = height - 1 + else + this->command(0x3f); // duty = 1/64 + this->command(SSD1325_SETOFFSET); // set display offset + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x00); // 0 + else + this->command(0x4C); // 76 + this->command(SSD1325_SETSTARTLINE); // set start line + this->command(0x00); // ... + this->command(SSD1325_MASTERCONFIG); // Set Master Config DC/DC Converter this->command(0x02); - this->command(SSD1325_SETREMAP); /* set segment remap------ */ - this->command(0x56); - this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(SSD1325_SETREMAP); // set segment remapping + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x53); // COM bottom-up, split odd/even, enable column and nibble remapping + else + this->command(0x50); // COM bottom-up, split odd/even + this->command(SSD1325_SETCURRENT + 0x2); // Set Full Current Range this->command(SSD1325_SETGRAYTABLE); - this->command(0x01); - this->command(0x11); - this->command(0x22); - this->command(0x32); - this->command(0x43); - this->command(0x54); - this->command(0x65); - this->command(0x76); - this->command(SSD1325_SETCONTRAST); /* set contrast current */ - this->command(0x7F); // max! + // gamma ~2.2 + if (this->model_ == SSD1327_MODEL_128_128) { + this->command(0); + this->command(1); + this->command(2); + this->command(3); + this->command(6); + this->command(8); + this->command(12); + this->command(16); + this->command(20); + this->command(26); + this->command(32); + this->command(39); + this->command(46); + this->command(54); + this->command(63); + } else { + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + } this->command(SSD1325_SETROWPERIOD); this->command(0x51); this->command(SSD1325_SETPHASELEN); @@ -78,19 +107,25 @@ void SSD1325::setup() { this->command(SSD1325_SETPRECHARGECOMPENABLE); this->command(0x28); this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin - this->command(0x1C); //? - this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x1C); + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin this->command(0x0D | 0x02); - this->command(SSD1325_NORMALDISPLAY); /* set display mode */ - this->command(SSD1325_DISPLAYON); /* display ON */ + this->command(SSD1325_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 } void SSD1325::display() { - this->command(SSD1325_SETCOLADDR); /* set column address */ - this->command(0x00); /* set column start address */ - this->command(0x3F); /* set column end address */ - this->command(SSD1325_SETROWADDR); /* set row address */ - this->command(0x00); /* set row start address */ - this->command(0x3F); /* set row end address */ + this->command(SSD1325_SETCOLADDR); // set column address + this->command(0x00); // set column start address + this->command(0x3F); // set column end address + this->command(SSD1325_SETROWADDR); // set row address + this->command(0x00); // set row start address + if (this->model_ == SSD1327_MODEL_128_128) + this->command(127); // set last row + else + this->command(63); // set last row this->write_display_data(); } @@ -98,6 +133,27 @@ void SSD1325::update() { this->do_update_(); this->display(); } +void SSD1325::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1325_SETCONTRAST); + this->command(int(SSD1325_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1325::is_on() { return this->is_on_; } +void SSD1325::turn_on() { + this->command(SSD1325_DISPLAYON); + this->is_on_ = true; +} +void SSD1325::turn_off() { + this->command(SSD1325_DISPLAYOFF); + this->is_on_ = false; +} int SSD1325::get_height_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: @@ -108,6 +164,8 @@ int SSD1325::get_height_internal() { return 16; case SSD1325_MODEL_64_48: return 48; + case SSD1327_MODEL_128_128: + return 128; default: return 0; } @@ -116,6 +174,7 @@ int SSD1325::get_width_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: case SSD1325_MODEL_128_64: + case SSD1327_MODEL_128_128: return 128; case SSD1325_MODEL_96_16: return 96; @@ -126,23 +185,25 @@ int SSD1325::get_width_internal() { } } size_t SSD1325::get_buffer_length_() { - return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1325_PIXELSPERBYTE; } - -void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; - - uint16_t pos = x + (y / 8) * this->get_width_internal(); - uint8_t subpos = y % 8; - if (color) { - this->buffer_[pos] |= (1 << subpos); - } else { - this->buffer_[pos] &= ~(1 << subpos); - } + uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1325_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1325_PIXELSPERBYTE); + uint8_t shift = (x % SSD1325_PIXELSPERBYTE) * SSD1325_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1325_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; } -void SSD1325::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1325::fill(Color color) { + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); + uint8_t fill = (color4 & SSD1325_COLORMASK) | ((color4 & SSD1325_COLORMASK) << SSD1325_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } @@ -168,6 +229,8 @@ const char *SSD1325::model_str_() { return "SSD1325 96x16"; case SSD1325_MODEL_64_48: return "SSD1325 64x48"; + case SSD1327_MODEL_128_128: + return "SSD1327 128x128"; default: return "Unknown"; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index e227f68f86..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 { @@ -12,6 +12,7 @@ enum SSD1325Model { SSD1325_MODEL_128_64, SSD1325_MODEL_96_16, SSD1325_MODEL_64_48, + SSD1327_MODEL_128_128, }; class SSD1325 : public PollingComponent, public display::DisplayBuffer { @@ -25,16 +26,21 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1325Model 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_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; virtual void write_display_data() = 0; void init_reset_(); - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; @@ -44,6 +50,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { SSD1325Model model_{SSD1325_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + bool is_on_{false}; + float brightness_{1.0}; }; } // namespace ssd1325_base diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index 4615d45393..a86dc751d5 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -4,23 +4,31 @@ from esphome import pins from esphome.components import spi, ssd1325_base from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES -AUTO_LOAD = ['ssd1325_base'] -DEPENDENCIES = ['spi'] +CODEOWNERS = ["@kbx81"] -ssd1325_spi = cg.esphome_ns.namespace('ssd1325_spi') -SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevice) +AUTO_LOAD = ["ssd1325_base"] +DEPENDENCIES = ["spi"] -CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SPISSD1325), - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) +ssd1325_spi = cg.esphome_ns.namespace("ssd1325_spi") +SPISSD1325 = ssd1325_spi.class_("SPISSD1325", ssd1325_base.SSD1325, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + ssd1325_base.SSD1325_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPISSD1325), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield ssd1325_base.setup_ssd1036(var, config) - yield spi.register_spi_device(var, config) + await ssd1325_base.setup_ssd1325(var, config) + await spi.register_spi_device(var, config) - dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 399700f1dd..98f48b8538 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -5,13 +5,14 @@ namespace esphome { namespace ssd1325_spi { -static const char *TAG = "ssd1325_spi"; +static const char *const TAG = "ssd1325_spi"; void SPISSD1325::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT - this->cs_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT this->init_reset_(); delay(500); // NOLINT @@ -20,43 +21,38 @@ void SPISSD1325::setup() { void SPISSD1325::dump_config() { LOG_DISPLAY("", "SPI SSD1325", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" CS Pin: ", this->cs_); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); LOG_UPDATE_INTERVAL(this); } void SPISSD1325::command(uint8_t value) { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(false); delay(1); this->enable(); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); this->write_byte(value); - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } void HOT SPISSD1325::write_display_data() { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(true); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); delay(1); this->enable(); - for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { - for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once - uint8_t left8 = this->buffer_[y * 16 + x]; - uint8_t right8 = this->buffer_[y * 16 + x + 1]; - for (uint8_t p = 0; p < 8; p++) { - uint8_t d = 0; - if (left8 & (1 << p)) - d |= 0xF0; - if (right8 & (1 << p)) - d |= 0x0F; - this->write_byte(d); - } - } - } - this->cs_->digital_write(true); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py new file mode 100644 index 0000000000..eada66a6e3 --- /dev/null +++ b/esphome/components/ssd1327_base/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN + +CODEOWNERS = ["@kbx81"] + +ssd1327_base_ns = cg.esphome_ns.namespace("ssd1327_base") +SSD1327 = ssd1327_base_ns.class_("SSD1327", cg.PollingComponent, display.DisplayBuffer) +SSD1327Model = ssd1327_base_ns.enum("SSD1327Model") + +MODELS = { + "SSD1327_128X128": SSD1327Model.SSD1327_MODEL_128_128, +} + +SSD1327_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1327_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Required(CONF_MODEL): SSD1327_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_ssd1327(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp new file mode 100644 index 0000000000..4cb8d17a3d --- /dev/null +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -0,0 +1,178 @@ +#include "ssd1327_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1327_base { + +static const char *const TAG = "ssd1327"; + +static const uint8_t SSD1327_MAX_CONTRAST = 127; +static const uint8_t SSD1327_COLORMASK = 0x0f; +static const uint8_t SSD1327_COLORSHIFT = 4; +static const uint8_t SSD1327_PIXELSPERBYTE = 2; + +static const uint8_t SSD1327_SETCOLUMNADDRESS = 0x15; +static const uint8_t SSD1327_SETROWADDRESS = 0x75; +static const uint8_t SSD1327_SETCONTRAST = 0x81; +static const uint8_t SSD1327_SETREMAP = 0xA0; +static const uint8_t SSD1327_SETSTARTLINE = 0xA1; +static const uint8_t SSD1327_SETOFFSET = 0xA2; +static const uint8_t SSD1327_NORMALDISPLAY = 0xA4; +static const uint8_t SSD1327_DISPLAYALLON = 0xA5; +static const uint8_t SSD1327_DISPLAYALLOFF = 0xA6; +static const uint8_t SSD1327_INVERTDISPLAY = 0xA7; +static const uint8_t SSD1327_SETMULTIPLEX = 0xA8; +static const uint8_t SSD1327_FUNCTIONSELECTIONA = 0xAB; +static const uint8_t SSD1327_DISPLAYOFF = 0xAE; +static const uint8_t SSD1327_DISPLAYON = 0xAF; +static const uint8_t SSD1327_SETPHASELENGTH = 0xB1; +static const uint8_t SSD1327_SETFRONTCLOCKDIVIDER = 0xB3; +static const uint8_t SSD1327_SETGPIO = 0xB5; +static const uint8_t SSD1327_SETSECONDPRECHARGEPERIOD = 0xB6; +static const uint8_t SSD1327_SETGRAYSCALETABLE = 0xB8; +static const uint8_t SSD1327_SELECTDEFAULTLINEARGRAYSCALETABLE = 0xB9; +static const uint8_t SSD1327_SETPRECHARGEVOLTAGE = 0xBC; +static const uint8_t SSD1327_SETVCOMHVOLTAGE = 0xBE; +static const uint8_t SSD1327_FUNCTIONSELECTIONB = 0xD5; +static const uint8_t SSD1327_SETCOMMANDLOCK = 0xFD; +static const uint8_t SSD1327_HORIZONTALSCROLLRIGHTSETUP = 0x26; +static const uint8_t SSD1327_HORIZONTALSCROLLLEFTSETUP = 0x27; +static const uint8_t SSD1327_DEACTIVATESCROLL = 0x2E; +static const uint8_t SSD1327_ACTIVATESCROLL = 0x2F; + +void SSD1327::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->turn_off(); // display OFF + this->command(SSD1327_SETFRONTCLOCKDIVIDER); // set osc division + this->command(0xF1); // 145 + this->command(SSD1327_SETMULTIPLEX); // multiplex ratio + this->command(0x7f); // duty = height - 1 + this->command(SSD1327_SETOFFSET); // set display offset + this->command(0x00); // 0 + this->command(SSD1327_SETSTARTLINE); // set start line + this->command(0x00); // ... + this->command(SSD1327_SETREMAP); // set segment remapping + this->command(0x53); // COM bottom-up, split odd/even, enable column and nibble remapping + this->command(SSD1327_SETGRAYSCALETABLE); + // gamma ~2.2 + this->command(0); + this->command(1); + this->command(2); + this->command(3); + this->command(6); + this->command(8); + this->command(12); + this->command(16); + this->command(20); + this->command(26); + this->command(32); + this->command(39); + this->command(46); + this->command(54); + this->command(63); + this->command(SSD1327_SETPHASELENGTH); + this->command(0x55); + this->command(SSD1327_SETVCOMHVOLTAGE); // Set High Voltage Level of COM Pin + 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 +} +void SSD1327::display() { + this->command(SSD1327_SETCOLUMNADDRESS); // set column address + this->command(0x00); // set column start address + this->command(0x3F); // set column end address + this->command(SSD1327_SETROWADDRESS); // set row address + this->command(0x00); // set row start address + this->command(127); // set last row + + this->write_display_data(); +} +void SSD1327::update() { + if (!this->is_failed()) { + this->do_update_(); + this->display(); + } +} +void SSD1327::set_brightness(float brightness) { + // validation + 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_))); +} +bool SSD1327::is_on() { return this->is_on_; } +void SSD1327::turn_on() { + this->command(SSD1327_DISPLAYON); + this->is_on_ = true; +} +void SSD1327::turn_off() { + this->command(SSD1327_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1327::get_height_internal() { + switch (this->model_) { + case SSD1327_MODEL_128_128: + return 128; + default: + return 0; + } +} +int SSD1327::get_width_internal() { + switch (this->model_) { + case SSD1327_MODEL_128_128: + return 128; + default: + return 0; + } +} +size_t SSD1327::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1327_PIXELSPERBYTE; +} +void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1327_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1327_PIXELSPERBYTE); + uint8_t shift = (x % SSD1327_PIXELSPERBYTE) * SSD1327_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1327_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1327_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; +} +void SSD1327::fill(Color color) { + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); + uint8_t fill = (color4 & SSD1327_COLORMASK) | ((color4 & SSD1327_COLORMASK) << SSD1327_COLORSHIFT); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1327::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1327::model_str_() { + switch (this->model_) { + case SSD1327_MODEL_128_128: + return "SSD1327 128x128"; + default: + return "Unknown"; + } +} + +} // namespace ssd1327_base +} // namespace esphome diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h new file mode 100644 index 0000000000..35b021c71b --- /dev/null +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1327_base { + +enum SSD1327Model { + SSD1327_MODEL_128_128 = 0, +}; + +class SSD1327 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1327Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + 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_(); + const char *model_str_(); + + SSD1327Model model_{SSD1327_MODEL_128_128}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1327_base +} // namespace esphome diff --git a/esphome/components/ssd1327_i2c/__init__.py b/esphome/components/ssd1327_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1327_i2c/display.py b/esphome/components/ssd1327_i2c/display.py new file mode 100644 index 0000000000..7b581cc92c --- /dev/null +++ b/esphome/components/ssd1327_i2c/display.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ssd1327_base, i2c +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@kbx81"] + +AUTO_LOAD = ["ssd1327_base"] +DEPENDENCIES = ["i2c"] + +ssd1327_i2c = cg.esphome_ns.namespace("ssd1327_i2c") +I2CSSD1327 = ssd1327_i2c.class_("I2CSSD1327", ssd1327_base.SSD1327, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + ssd1327_base.SSD1327_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2CSSD1327), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x3D)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ssd1327_base.setup_ssd1327(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp new file mode 100644 index 0000000000..e9e047bfb6 --- /dev/null +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp @@ -0,0 +1,44 @@ +#include "ssd1327_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ssd1327_i2c { + +static const char *const TAG = "ssd1327_i2c"; + +void I2CSSD1327::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2C SSD1327..."); + this->init_reset_(); + + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + SSD1327::setup(); +} +void I2CSSD1327::dump_config() { + LOG_DISPLAY("", "I2C SSD1327", this); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_UPDATE_INTERVAL(this); + + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with SSD1327 failed!"); + } +} +void I2CSSD1327::command(uint8_t value) { this->write_byte(0x00, value); } +void HOT I2CSSD1327::write_display_data() { + for (uint32_t i = 0; i < this->get_buffer_length_();) { + uint8_t data[16]; + for (uint8_t &j : data) + j = this->buffer_[i++]; + this->write_bytes(0x40, data, sizeof(data)); + } +} + +} // namespace ssd1327_i2c +} // namespace esphome diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.h b/esphome/components/ssd1327_i2c/ssd1327_i2c.h new file mode 100644 index 0000000000..dd292f9936 --- /dev/null +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1327_base/ssd1327_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ssd1327_i2c { + +class I2CSSD1327 : public ssd1327_base::SSD1327, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + protected: + void command(uint8_t value) override; + void write_display_data() override; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; +}; + +} // namespace ssd1327_i2c +} // namespace esphome diff --git a/esphome/components/ssd1327_spi/__init__.py b/esphome/components/ssd1327_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py new file mode 100644 index 0000000000..138e85eecd --- /dev/null +++ b/esphome/components/ssd1327_spi/display.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1327_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@kbx81"] + +AUTO_LOAD = ["ssd1327_base"] +DEPENDENCIES = ["spi"] + +ssd1327_spi = cg.esphome_ns.namespace("ssd1327_spi") +SPISSD1327 = ssd1327_spi.class_("SPISSD1327", ssd1327_base.SSD1327, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + ssd1327_base.SSD1327_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPISSD1327), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ssd1327_base.setup_ssd1327(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.cpp b/esphome/components/ssd1327_spi/ssd1327_spi.cpp new file mode 100644 index 0000000000..1dd2b73e66 --- /dev/null +++ b/esphome/components/ssd1327_spi/ssd1327_spi.cpp @@ -0,0 +1,59 @@ +#include "ssd1327_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1327_spi { + +static const char *const TAG = "ssd1327_spi"; + +void SPISSD1327::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1327..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1327::setup(); +} +void SPISSD1327::dump_config() { + LOG_DISPLAY("", "SPI SSD1327", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1327::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1327::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1327_spi +} // namespace esphome diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.h b/esphome/components/ssd1327_spi/ssd1327_spi.h new file mode 100644 index 0000000000..6f7abea96f --- /dev/null +++ b/esphome/components/ssd1327_spi/ssd1327_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1327_base/ssd1327_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1327_spi { + +class SPISSD1327 : public ssd1327_base::SSD1327, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1327_spi +} // namespace esphome diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py new file mode 100644 index 0000000000..067f55a252 --- /dev/null +++ b/esphome/components/ssd1331_base/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_RESET_PIN + +CODEOWNERS = ["@kbx81"] + +ssd1331_base_ns = cg.esphome_ns.namespace("ssd1331_base") +SSD1331 = ssd1331_base_ns.class_("SSD1331", cg.PollingComponent, display.DisplayBuffer) + +SSD1331_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_ssd1331(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) + + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp new file mode 100644 index 0000000000..88764c3d90 --- /dev/null +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -0,0 +1,153 @@ +#include "ssd1331_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1331_base { + +static const char *const TAG = "ssd1331"; + +static const uint16_t SSD1331_COLORMASK = 0xffff; +static const uint8_t SSD1331_MAX_CONTRASTA = 0x91; +static const uint8_t SSD1331_MAX_CONTRASTB = 0x50; +static const uint8_t SSD1331_MAX_CONTRASTC = 0x7D; +static const uint8_t SSD1331_BYTESPERPIXEL = 2; +// SSD1331 Commands +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 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 +static const uint8_t SSD1331_MASTERCURRENT = 0x87; // Master current control +static const uint8_t SSD1331_SETREMAP = 0xA0; // Set re-map & data format +static const uint8_t SSD1331_STARTLINE = 0xA1; // Set display start line +static const uint8_t SSD1331_DISPLAYOFFSET = 0xA2; // Set display offset +static const uint8_t SSD1331_NORMALDISPLAY = 0xA4; // Set display to normal mode +static const uint8_t SSD1331_DISPLAYALLON = 0xA5; // Set entire display ON +static const uint8_t SSD1331_DISPLAYALLOFF = 0xA6; // Set entire display OFF +static const uint8_t SSD1331_INVERTDISPLAY = 0xA7; // Invert display +static const uint8_t SSD1331_SETMULTIPLEX = 0xA8; // Set multiplex ratio +static const uint8_t SSD1331_SETMASTER = 0xAD; // Set master configuration +static const uint8_t SSD1331_DISPLAYOFF = 0xAE; // Display OFF (sleep mode) +static const uint8_t SSD1331_DISPLAYON = 0xAF; // Normal Brightness Display ON +static const uint8_t SSD1331_POWERMODE = 0xB0; // Power save mode +static const uint8_t SSD1331_PRECHARGE = 0xB1; // Phase 1 and 2 period adjustment +static const uint8_t SSD1331_CLOCKDIV = 0xB3; // Set display clock divide ratio/oscillator frequency +static const uint8_t SSD1331_PRECHARGEA = 0x8A; // Set second pre-charge speed for color A +static const uint8_t SSD1331_PRECHARGEB = 0x8B; // Set second pre-charge speed for color B +static const uint8_t SSD1331_PRECHARGEC = 0x8C; // Set second pre-charge speed for color C +static const uint8_t SSD1331_PRECHARGELEVEL = 0xBB; // Set pre-charge voltage +static const uint8_t SSD1331_VCOMH = 0xBE; // Set Vcomh voltge + +void SSD1331::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1331_DISPLAYOFF); // 0xAE + this->command(SSD1331_SETREMAP); // 0xA0 + this->command(0x72); // RGB Color + this->command(SSD1331_STARTLINE); // 0xA1 + this->command(0x0); + this->command(SSD1331_DISPLAYOFFSET); // 0xA2 + this->command(0x0); + this->command(SSD1331_NORMALDISPLAY); // 0xA4 + this->command(SSD1331_SETMULTIPLEX); // 0xA8 + this->command(0x3F); // 0x3F 1/64 duty + this->command(SSD1331_SETMASTER); // 0xAD + this->command(0x8E); + this->command(SSD1331_POWERMODE); // 0xB0 + this->command(0x0B); + this->command(SSD1331_PRECHARGE); // 0xB1 + this->command(0x31); + this->command(SSD1331_CLOCKDIV); // 0xB3 + this->command(0xF0); // 7:4 = Oscillator Frequency, 3:0 = CLK Div Ratio, (A[3:0]+1 = 1..16) + this->command(SSD1331_PRECHARGEA); // 0x8A + this->command(0x64); + this->command(SSD1331_PRECHARGEB); // 0x8B + this->command(0x78); + this->command(SSD1331_PRECHARGEC); // 0x8C + this->command(0x64); + this->command(SSD1331_PRECHARGELEVEL); // 0xBB + this->command(0x3A); + this->command(SSD1331_VCOMH); // 0xBE + this->command(0x3E); + this->command(SSD1331_MASTERCURRENT); // 0x87 + this->command(0x06); + 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 +} +void SSD1331::display() { + this->command(SSD1331_SETCOLUMN); // set column address + this->command(0x00); // set column start address + this->command(0x5F); // set column end address + this->command(SSD1331_SETROW); // set row address + this->command(0x00); // set row start address + this->command(0x3F); // set last row + this->write_display_data(); +} +void SSD1331::update() { + this->do_update_(); + this->display(); +} +void SSD1331::set_brightness(float brightness) { + // validation + 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_))); + this->command(SSD1331_CONTRASTB); // 0x82 + this->command(int(SSD1331_MAX_CONTRASTB * (this->brightness_))); + this->command(SSD1331_CONTRASTC); // 0x83 + this->command(int(SSD1331_MAX_CONTRASTC * (this->brightness_))); +} +bool SSD1331::is_on() { return this->is_on_; } +void SSD1331::turn_on() { + this->command(SSD1331_DISPLAYON); + this->is_on_ = true; +} +void SSD1331::turn_off() { + this->command(SSD1331_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1331::get_height_internal() { return 64; } +int SSD1331::get_width_internal() { return 96; } +size_t SSD1331::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * size_t(SSD1331_BYTESPERPIXEL); +} +void HOT SSD1331::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + const uint32_t color565 = display::ColorUtil::color_to_565(color); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x + y * this->get_width_internal()) * SSD1331_BYTESPERPIXEL; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} +void SSD1331::fill(Color color) { + const uint32_t color565 = display::ColorUtil::color_to_565(color); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + if (i & 1) { + this->buffer_[i] = color565 & 0xff; + } else { + this->buffer_[i] = (color565 >> 8) & 0xff; + } +} +void SSD1331::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +} // namespace ssd1331_base +} // namespace esphome diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h new file mode 100644 index 0000000000..b889a47fbe --- /dev/null +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1331_base { + +class SSD1331 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + 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_(); + + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1331_base +} // namespace esphome diff --git a/esphome/components/ssd1331_spi/__init__.py b/esphome/components/ssd1331_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py new file mode 100644 index 0000000000..c32ac60578 --- /dev/null +++ b/esphome/components/ssd1331_spi/display.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1331_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@kbx81"] + +AUTO_LOAD = ["ssd1331_base"] +DEPENDENCIES = ["spi"] + +ssd1331_spi = cg.esphome_ns.namespace("ssd1331_spi") +SPISSD1331 = ssd1331_spi.class_("SPISSD1331", ssd1331_base.SSD1331, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + ssd1331_base.SSD1331_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPISSD1331), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ssd1331_base.setup_ssd1331(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.cpp b/esphome/components/ssd1331_spi/ssd1331_spi.cpp new file mode 100644 index 0000000000..ff42c74b9f --- /dev/null +++ b/esphome/components/ssd1331_spi/ssd1331_spi.cpp @@ -0,0 +1,58 @@ +#include "ssd1331_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1331_spi { + +static const char *const TAG = "ssd1331_spi"; + +void SPISSD1331::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1331..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1331::setup(); +} +void SPISSD1331::dump_config() { + LOG_DISPLAY("", "SPI SSD1331", this); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1331::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1331::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1331_spi +} // namespace esphome diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.h b/esphome/components/ssd1331_spi/ssd1331_spi.h new file mode 100644 index 0000000000..93b2e228b1 --- /dev/null +++ b/esphome/components/ssd1331_spi/ssd1331_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1331_base/ssd1331_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1331_spi { + +class SPISSD1331 : public ssd1331_base::SSD1331, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1331_spi +} // namespace esphome diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py new file mode 100644 index 0000000000..555d6c5e2e --- /dev/null +++ b/esphome/components/ssd1351_base/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN + +CODEOWNERS = ["@kbx81"] + +ssd1351_base_ns = cg.esphome_ns.namespace("ssd1351_base") +SSD1351 = ssd1351_base_ns.class_("SSD1351", cg.PollingComponent, display.DisplayBuffer) +SSD1351Model = ssd1351_base_ns.enum("SSD1351Model") + +MODELS = { + "SSD1351_128X96": SSD1351Model.SSD1351_MODEL_128_96, + "SSD1351_128X128": SSD1351Model.SSD1351_MODEL_128_128, +} + +SSD1351_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Required(CONF_MODEL): SSD1351_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_ssd1351(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp new file mode 100644 index 0000000000..f26cd7c697 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -0,0 +1,191 @@ +#include "ssd1351_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1351_base { + +static const char *const TAG = "ssd1351"; + +static const uint16_t SSD1351_COLORMASK = 0xffff; +static const uint8_t SSD1351_MAX_CONTRAST = 15; +static const uint8_t SSD1351_BYTESPERPIXEL = 2; +// SSD1351 commands +static const uint8_t SSD1351_SETCOLUMN = 0x15; +static const uint8_t SSD1351_SETROW = 0x75; +static const uint8_t SSD1351_SETREMAP = 0xA0; +static const uint8_t SSD1351_STARTLINE = 0xA1; +static const uint8_t SSD1351_DISPLAYOFFSET = 0xA2; +static const uint8_t SSD1351_DISPLAYOFF = 0xAE; +static const uint8_t SSD1351_DISPLAYON = 0xAF; +static const uint8_t SSD1351_PRECHARGE = 0xB1; +static const uint8_t SSD1351_CLOCKDIV = 0xB3; +static const uint8_t SSD1351_PRECHARGELEVEL = 0xBB; +static const uint8_t SSD1351_VCOMH = 0xBE; +// display controls +static const uint8_t SSD1351_DISPLAYALLOFF = 0xA4; +static const uint8_t SSD1351_DISPLAYALLON = 0xA5; +static const uint8_t SSD1351_NORMALDISPLAY = 0xA6; +static const uint8_t SSD1351_INVERTDISPLAY = 0xA7; +// contrast controls +static const uint8_t SSD1351_CONTRASTABC = 0xC1; +static const uint8_t SSD1351_CONTRASTMASTER = 0xC7; +// memory functions +static const uint8_t SSD1351_WRITERAM = 0x5C; +static const uint8_t SSD1351_READRAM = 0x5D; +// other functions +static const uint8_t SSD1351_FUNCTIONSELECT = 0xAB; +static const uint8_t SSD1351_DISPLAYENHANCE = 0xB2; +static const uint8_t SSD1351_SETVSL = 0xB4; +static const uint8_t SSD1351_SETGPIO = 0xB5; +static const uint8_t SSD1351_PRECHARGE2 = 0xB6; +static const uint8_t SSD1351_SETGRAY = 0xB8; +static const uint8_t SSD1351_USELUT = 0xB9; +static const uint8_t SSD1351_MUXRATIO = 0xCA; +static const uint8_t SSD1351_COMMANDLOCK = 0xFD; +static const uint8_t SSD1351_HORIZSCROLL = 0x96; +static const uint8_t SSD1351_STOPSCROLL = 0x9E; +static const uint8_t SSD1351_STARTSCROLL = 0x9F; + +void SSD1351::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1351_COMMANDLOCK); + this->data(0x12); + this->command(SSD1351_COMMANDLOCK); + this->data(0xB1); + this->command(SSD1351_DISPLAYOFF); + this->command(SSD1351_CLOCKDIV); + this->data(0xF1); // 7:4 = Oscillator Freq, 3:0 = CLK Div Ratio (A[3:0]+1 = 1..16) + this->command(SSD1351_MUXRATIO); + this->data(127); + this->command(SSD1351_DISPLAYOFFSET); + this->data(0x00); + this->command(SSD1351_SETGPIO); + this->data(0x00); + this->command(SSD1351_FUNCTIONSELECT); + this->data(0x01); // internal (diode drop) + this->command(SSD1351_PRECHARGE); + this->data(0x32); + this->command(SSD1351_VCOMH); + this->data(0x05); + this->command(SSD1351_NORMALDISPLAY); + this->command(SSD1351_SETVSL); + this->data(0xA0); + this->data(0xB5); + this->data(0x55); + this->command(SSD1351_PRECHARGE2); + this->data(0x01); + this->command(SSD1351_SETREMAP); + this->data(0x34); + this->command(SSD1351_STARTLINE); + this->data(0x00); + this->command(SSD1351_CONTRASTABC); + this->data(0xC8); + this->data(0x80); + this->data(0xC8); + 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 +} +void SSD1351::display() { + this->command(SSD1351_SETCOLUMN); // set column address + this->data(0x00); // set column start address + this->data(0x7F); // set column end address + this->command(SSD1351_SETROW); // set row address + this->data(0x00); // set row start address + this->data(0x7F); // set last row + this->command(SSD1351_WRITERAM); + this->write_display_data(); +} +void SSD1351::update() { + this->do_update_(); + this->display(); +} +void SSD1351::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1351_CONTRASTMASTER); + this->data(int(SSD1351_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1351::is_on() { return this->is_on_; } +void SSD1351::turn_on() { + this->command(SSD1351_DISPLAYON); + this->is_on_ = true; +} +void SSD1351::turn_off() { + this->command(SSD1351_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1351::get_height_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return 96; + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +int SSD1351::get_width_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +size_t SSD1351::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * size_t(SSD1351_BYTESPERPIXEL); +} +void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + const uint32_t color565 = display::ColorUtil::color_to_565(color); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x + y * this->get_width_internal()) * SSD1351_BYTESPERPIXEL; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} +void SSD1351::fill(Color color) { + const uint32_t color565 = display::ColorUtil::color_to_565(color); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + if (i & 1) { + this->buffer_[i] = color565 & 0xff; + } else { + this->buffer_[i] = (color565 >> 8) & 0xff; + } +} +void SSD1351::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1351::model_str_() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return "SSD1351 128x96"; + case SSD1351_MODEL_128_128: + return "SSD1351 128x128"; + default: + return "Unknown"; + } +} + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h new file mode 100644 index 0000000000..422e601f8b --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1351_base { + +enum SSD1351Model { + SSD1351_MODEL_128_96 = 0, + SSD1351_MODEL_128_128, +}; + +class SSD1351 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1351Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void data(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + 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_(); + const char *model_str_(); + + SSD1351Model model_{SSD1351_MODEL_128_96}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/__init__.py b/esphome/components/ssd1351_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py new file mode 100644 index 0000000000..3f3409226c --- /dev/null +++ b/esphome/components/ssd1351_spi/display.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1351_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@kbx81"] + +AUTO_LOAD = ["ssd1351_base"] +DEPENDENCIES = ["spi"] + +ssd1351_spi = cg.esphome_ns.namespace("ssd1351_spi") +SPISSD1351 = ssd1351_spi.class_("SPISSD1351", ssd1351_base.SSD1351, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + ssd1351_base.SSD1351_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPISSD1351), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ssd1351_base.setup_ssd1351(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp new file mode 100644 index 0000000000..9599c6e644 --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -0,0 +1,72 @@ +#include "ssd1351_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1351_spi { + +static const char *const TAG = "ssd1351_spi"; + +void SPISSD1351::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1351..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1351::setup(); +} +void SPISSD1351::dump_config() { + LOG_DISPLAY("", "SPI SSD1351", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1351::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void SPISSD1351::data(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1351::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.h b/esphome/components/ssd1351_spi/ssd1351_spi.h new file mode 100644 index 0000000000..b8f3310f5c --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1351_base/ssd1351_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1351_spi { + +class SPISSD1351 : public ssd1351_base::SSD1351, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + void data(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/st7735/__init__.py b/esphome/components/st7735/__init__.py new file mode 100644 index 0000000000..ba854bb0ae --- /dev/null +++ b/esphome/components/st7735/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +st7735_ns = cg.esphome_ns.namespace("st7735") diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py new file mode 100644 index 0000000000..ae31f604a5 --- /dev/null +++ b/esphome/components/st7735/display.py @@ -0,0 +1,101 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi +from esphome.components import display +from esphome.const import ( + CONF_DC_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_MODEL, + CONF_RESET_PIN, + CONF_PAGES, +) +from . import st7735_ns + +CODEOWNERS = ["@SenexCrenshaw"] + +DEPENDENCIES = ["spi"] + +CONF_DEVICE_WIDTH = "device_width" +CONF_DEVICE_HEIGHT = "device_height" +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 +) +ST7735Model = st7735_ns.enum("ST7735Model") + +MODELS = { + "INITR_GREENTAB": ST7735Model.ST7735_INITR_GREENTAB, + "INITR_REDTAB": ST7735Model.ST7735_INITR_REDTAB, + "INITR_BLACKTAB": ST7735Model.ST7735_INITR_BLACKTAB, + "INITR_MINI160X80": ST7735Model.ST7735_INITR_MINI_160X80, + "INITR_18BLACKTAB": ST7735Model.ST7735_INITR_18BLACKTAB, + "INITR_18REDTAB": ST7735Model.ST7735_INITR_18REDTAB, +} +ST7735_MODEL = cv.enum(MODELS, upper=True, space="_") + + +ST7735_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Required(CONF_MODEL): ST7735_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("1s")) + +CONFIG_SCHEMA = cv.All( + ST7735_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPIST7735), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DEVICE_WIDTH): cv.int_, + cv.Required(CONF_DEVICE_HEIGHT): cv.int_, + cv.Required(CONF_COL_START): cv.int_, + 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) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def setup_st7735(var, config): + await cg.register_component(var, config) + await display.register_display(var, config) + + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_MODEL], + config[CONF_DEVICE_WIDTH], + config[CONF_DEVICE_HEIGHT], + config[CONF_COL_START], + 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) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp new file mode 100644 index 0000000000..c5178986f3 --- /dev/null +++ b/esphome/components/st7735/st7735.cpp @@ -0,0 +1,490 @@ +#include "st7735.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace st7735 { + +static const uint8_t ST_CMD_DELAY = 0x80; // special signifier for command lists + +static const uint8_t ST77XX_NOP = 0x00; +static const uint8_t ST77XX_SWRESET = 0x01; +static const uint8_t ST77XX_RDDID = 0x04; +static const uint8_t ST77XX_RDDST = 0x09; + +static const uint8_t ST77XX_SLPIN = 0x10; +static const uint8_t ST77XX_SLPOUT = 0x11; +static const uint8_t ST77XX_PTLON = 0x12; +static const uint8_t ST77XX_NORON = 0x13; + +static const uint8_t ST77XX_INVOFF = 0x20; +static const uint8_t ST77XX_INVON = 0x21; +static const uint8_t ST77XX_DISPOFF = 0x28; +static const uint8_t ST77XX_DISPON = 0x29; +static const uint8_t ST77XX_CASET = 0x2A; +static const uint8_t ST77XX_RASET = 0x2B; +static const uint8_t ST77XX_RAMWR = 0x2C; +static const uint8_t ST77XX_RAMRD = 0x2E; + +static const uint8_t ST77XX_PTLAR = 0x30; +static const uint8_t ST77XX_TEOFF = 0x34; +static const uint8_t ST77XX_TEON = 0x35; +static const uint8_t ST77XX_MADCTL = 0x36; +static const uint8_t ST77XX_COLMOD = 0x3A; + +static const uint8_t ST77XX_MADCTL_MY = 0x80; +static const uint8_t ST77XX_MADCTL_MX = 0x40; +static const uint8_t ST77XX_MADCTL_MV = 0x20; +static const uint8_t ST77XX_MADCTL_ML = 0x10; +static const uint8_t ST77XX_MADCTL_RGB = 0x00; + +static const uint8_t ST77XX_RDID1 = 0xDA; +static const uint8_t ST77XX_RDID2 = 0xDB; +static const uint8_t ST77XX_RDID3 = 0xDC; +static const uint8_t ST77XX_RDID4 = 0xDD; + +// Some register settings +static const uint8_t ST7735_MADCTL_BGR = 0x08; + +static const uint8_t ST7735_MADCTL_MH = 0x04; + +static const uint8_t ST7735_FRMCTR1 = 0xB1; +static const uint8_t ST7735_FRMCTR2 = 0xB2; +static const uint8_t ST7735_FRMCTR3 = 0xB3; +static const uint8_t ST7735_INVCTR = 0xB4; +static const uint8_t ST7735_DISSET5 = 0xB6; + +static const uint8_t ST7735_PWCTR1 = 0xC0; +static const uint8_t ST7735_PWCTR2 = 0xC1; +static const uint8_t ST7735_PWCTR3 = 0xC2; +static const uint8_t ST7735_PWCTR4 = 0xC3; +static const uint8_t ST7735_PWCTR5 = 0xC4; +static const uint8_t ST7735_VMCTR1 = 0xC5; + +static const uint8_t ST7735_PWCTR6 = 0xFC; + +static const uint8_t ST7735_GMCTRP1 = 0xE0; +static const uint8_t ST7735_GMCTRN1 = 0xE1; + +// clang-format off +static const uint8_t PROGMEM + BCMD[] = { // Init commands for 7735B screens + 18, // 18 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay + 50, // 50 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay + 255, // 255 = max (500 ms) delay + ST77XX_COLMOD, 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: + 0x05, // 16-bit color + 10, // 10 ms delay + ST7735_FRMCTR1, 3+ST_CMD_DELAY, // 4: Frame rate control, 3 args + delay: + 0x00, // fastest refresh + 0x06, // 6 lines front porch + 0x03, // 3 lines back porch + 10, // 10 ms delay + ST77XX_MADCTL, 1, // 5: Mem access ctl (directions), 1 arg: + 0x08, // Row/col addr, bottom-top refresh + ST7735_DISSET5, 2, // 6: Display settings #5, 2 args: + 0x15, // 1 clk cycle nonoverlap, 2 cycle gate + // rise, 3 cycle osc equalize + 0x02, // Fix on VTL + ST7735_INVCTR, 1, // 7: Display inversion control, 1 arg: + 0x0, // Line inversion + ST7735_PWCTR1, 2+ST_CMD_DELAY, // 8: Power control, 2 args + delay: + 0x02, // GVDD = 4.7V + 0x70, // 1.0uA + 10, // 10 ms delay + ST7735_PWCTR2, 1, // 9: Power control, 1 arg, no delay: + 0x05, // VGH = 14.7V, VGL = -7.35V + ST7735_PWCTR3, 2, // 10: Power control, 2 args, no delay: + 0x01, // Opamp current small + 0x02, // Boost frequency + ST7735_VMCTR1, 2+ST_CMD_DELAY, // 11: Power control, 2 args + delay: + 0x3C, // VCOMH = 4V + 0x38, // VCOML = -1.1V + 10, // 10 ms delay + ST7735_PWCTR6, 2, // 12: Power control, 2 args, no delay: + 0x11, 0x15, + ST7735_GMCTRP1,16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x09, 0x16, 0x09, 0x20, // (Not entirely necessary, but provides + 0x21, 0x1B, 0x13, 0x19, // accurate colors) + 0x17, 0x15, 0x1E, 0x2B, + 0x04, 0x05, 0x02, 0x0E, + ST7735_GMCTRN1,16+ST_CMD_DELAY, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x0B, 0x14, 0x08, 0x1E, // (Not entirely necessary, but provides + 0x22, 0x1D, 0x18, 0x1E, // accurate colors) + 0x1B, 0x1A, 0x24, 0x2B, + 0x06, 0x06, 0x02, 0x0F, + 10, // 10 ms delay + ST77XX_CASET, 4, // 15: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 2 + 0x00, 0x81, // XEND = 129 + ST77XX_RASET, 4, // 16: Row addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 1 + 0x00, 0x81, // XEND = 160 + ST77XX_NORON, ST_CMD_DELAY, // 17: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 18: Main screen turn on, no args, delay + 255 }, // 255 = max (500 ms) delay + + RCMD1[] = { // 7735R init, part 1 (red or green tab) + 15, // 15 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + 150, // 150 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay + 255, // 500 ms delay + ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + 0x01, 0x2C, 0x2D, // Dot inversion mode + 0x01, 0x2C, 0x2D, // Line inversion mode + ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: + 0x07, // No inversion + ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: + 0xA2, + 0x02, // -4.6V + 0x84, // AUTO mode + ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: + 0x0A, // Opamp current small + 0x00, // Boost frequency + ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: + 0x8A, // BCLK/2, + 0x2A, // opamp current small & medium low + ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: + 0x8A, 0xEE, + ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: + 0x0E, + ST77XX_INVOFF, 0, // 13: Don't invert display, no args + ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: + 0xC8, // row/col addr, bottom-top refresh + ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: + 0x05 }, // 16-bit color + + RCMD2GREEN[] = { // 7735R init, part 2 (green tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 0 + 0x00, 0x7F+0x02, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x01, // XSTART = 0 + 0x00, 0x9F+0x01 }, // XEND = 159 + + RCMD2RED[] = { // 7735R init, part 2 (red tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + RCMD2GREEN144[] = { // 7735R init, part 2 (green 1.44 tab) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F }, // XEND = 127 + + RCMD2GREEN160X80[] = { // 7735R init, part 2 (mini 160x80) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x4F, // XEND = 79 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + RCMD3[] = { // 7735R init, part 3 (red or green tab) + 4, // 4 commands in list: + ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + 0x37, 0x32, 0x29, 0x2d, // accurate colors) + 0x29, 0x25, 0x2B, 0x39, + 0x00, 0x01, 0x03, 0x10, + ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + 0x2E, 0x2E, 0x37, 0x3F, + 0x00, 0x00, 0x02, 0x10, + ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay + 100 }; // 100 ms delay + +// clang-format on +static const char *const TAG = "st7735"; + +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..."); + this->spi_setup(); + + this->dc_pin_->setup(); // OUTPUT + this->cs_->setup(); // OUTPUT + + this->dc_pin_->digital_write(true); + this->cs_->digital_write(true); + + this->init_reset_(); + delay(100); // NOLINT + + ESP_LOGD(TAG, " START"); + dump_config(); + ESP_LOGD(TAG, " END"); + + display_init_(RCMD1); + + if (this->model_ == INITR_GREENTAB) { + display_init_(RCMD2GREEN); + colstart_ == 0 ? colstart_ = 2 : colstart_; + rowstart_ == 0 ? rowstart_ = 1 : rowstart_; + } else if ((this->model_ == INITR_144GREENTAB) || (this->model_ == INITR_HALLOWING)) { + height_ == 0 ? height_ = ST7735_TFTHEIGHT_128 : height_; + width_ == 0 ? width_ = ST7735_TFTWIDTH_128 : width_; + display_init_(RCMD2GREEN144); + colstart_ == 0 ? colstart_ = 2 : colstart_; + rowstart_ == 0 ? rowstart_ = 3 : rowstart_; + } else if (this->model_ == INITR_MINI_160X80) { + height_ == 0 ? height_ = ST7735_TFTHEIGHT_160 : height_; + width_ == 0 ? width_ = ST7735_TFTWIDTH_80 : width_; + display_init_(RCMD2GREEN160X80); + colstart_ = 24; + rowstart_ = 0; // For default rotation 0 + } else { + // colstart, rowstart left at default '0' values + display_init_(RCMD2RED); + } + display_init_(RCMD3); + + uint8_t data = 0; + if (this->model_ != INITR_HALLOWING) { + data = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY; + } + if (this->usebgr_) { + data = data | ST7735_MADCTL_BGR; + } else { + data = data | ST77XX_MADCTL_RGB; + } + 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()); +} + +void ST7735::update() { + this->do_update_(); + this->write_display_data_(); +} + +int ST7735::get_height_internal() { return height_; } + +int ST7735::get_width_internal() { return width_; } + +size_t ST7735::get_buffer_length() { + if (this->eightbitcolor_) { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()); + } + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; +} + +void HOT ST7735::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + if (this->eightbitcolor_) { + const uint32_t color332 = display::ColorUtil::color_to_332(color); + uint16_t pos = (x + y * this->get_width_internal()); + this->buffer_[pos] = color332; + } else { + const uint32_t color565 = display::ColorUtil::color_to_565(color); + uint16_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; + } +} + +void ST7735::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *ST7735::model_str_() { + switch (this->model_) { + case INITR_GREENTAB: + return "ST7735 GREENTAB"; + case INITR_REDTAB: + return "ST7735 REDTAB"; + case INITR_BLACKTAB: + return "ST7735 BLACKTAB"; + case INITR_MINI_160X80: + return "ST7735 MINI160x80"; + default: + return "Unknown"; + } +} + +void ST7735::display_init_(const uint8_t *addr) { + uint8_t num_commands, cmd, num_args; + uint16_t ms; + + 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 = progmem_read_byte(addr++); // Read post-command delay time (ms) + if (ms == 255) + ms = 500; // If 255, delay for 500 ms + delay(ms); + } + } +} + +void ST7735::dump_config() { + LOG_DISPLAY("", "ST7735", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGD(TAG, " Buffer Size: %zu", this->get_buffer_length()); + ESP_LOGD(TAG, " Height: %d", this->height_); + ESP_LOGD(TAG, " Width: %d", this->width_); + ESP_LOGD(TAG, " ColStart: %d", this->colstart_); + ESP_LOGD(TAG, " RowStart: %d", this->rowstart_); + LOG_UPDATE_INTERVAL(this); +} + +void HOT ST7735::writecommand_(uint8_t value) { + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + this->disable(); +} + +void HOT ST7735::writedata_(uint8_t value) { + this->dc_pin_->digital_write(true); + this->enable(); + this->write_byte(value); + this->disable(); +} + +void HOT ST7735::sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->writecommand_(cmd); + this->senddata_(data_bytes, num_data_bytes); +} + +void HOT ST7735::senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->dc_pin_->digital_write(true); // pull DC high to indicate data + this->cs_->digital_write(false); + this->enable(); + for (uint8_t i = 0; i < num_data_bytes; i++) { + this->write_byte(progmem_read_byte(data_bytes++)); // write byte - SPI library + } + this->cs_->digital_write(true); + this->disable(); +} + +void HOT ST7735::write_display_data_() { + uint16_t offsetx = colstart_; + uint16_t offsety = rowstart_; + + uint16_t x1 = offsetx; + uint16_t x2 = x1 + get_width_internal() - 1; + uint16_t y1 = offsety; + uint16_t y2 = y1 + get_height_internal() - 1; + + this->enable(); + + // set column(x) address + this->dc_pin_->digital_write(false); + this->write_byte(ST77XX_CASET); + this->dc_pin_->digital_write(true); + this->spi_master_write_addr_(x1, x2); + + // set Page(y) address + this->dc_pin_->digital_write(false); + this->write_byte(ST77XX_RASET); + this->dc_pin_->digital_write(true); + this->spi_master_write_addr_(y1, y2); + + // Memory Write + this->dc_pin_->digital_write(false); + this->write_byte(ST77XX_RAMWR); + this->dc_pin_->digital_write(true); + + if (this->eightbitcolor_) { + for (size_t line = 0; line < this->get_buffer_length(); line = line + this->get_width_internal()) { + for (int index = 0; index < this->get_width_internal(); ++index) { + auto color332 = display::ColorUtil::to_color(this->buffer_[index + line], display::ColorOrder::COLOR_ORDER_RGB, + display::ColorBitness::COLOR_BITNESS_332, true); + + auto color = display::ColorUtil::color_to_565(color332); + + this->write_byte((color >> 8) & 0xff); + this->write_byte(color & 0xff); + } + } + } else { + this->write_array(this->buffer_, this->get_buffer_length()); + } + this->disable(); +} + +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; + + this->dc_pin_->digital_write(true); + this->write_array(byte, 4); +} + +void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) { + static uint8_t byte[1024]; + int index = 0; + for (int i = 0; i < size; i++) { + byte[index++] = (color >> 8) & 0xFF; + byte[index++] = color & 0xFF; + } + + this->dc_pin_->digital_write(true); + return write_array(byte, size * 2); +} + +} // namespace st7735 +} // namespace esphome diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h new file mode 100644 index 0000000000..c049fb9e83 --- /dev/null +++ b/esphome/components/st7735/st7735.h @@ -0,0 +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, 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/__init__.py b/esphome/components/st7789v/__init__.py new file mode 100644 index 0000000000..3e64d09c57 --- /dev/null +++ b/esphome/components/st7789v/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +st7789v_ns = cg.esphome_ns.namespace("st7789v") diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py new file mode 100644 index 0000000000..7b38b1d2c5 --- /dev/null +++ b/esphome/components/st7789v/display.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import ( + CONF_BACKLIGHT_PIN, + CONF_BRIGHTNESS, + CONF_CS_PIN, + CONF_DC_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_RESET_PIN, +) +from . import st7789v_ns + +CODEOWNERS = ["@kbx81"] + +DEPENDENCIES = ["spi"] + +ST7789V = st7789v_ns.class_( + "ST7789V", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) +ST7789VRef = ST7789V.operator("ref") + +CONFIG_SCHEMA = ( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ST7789V), + 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.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + } + ) + .extend(cv.polling_component_schema("5s")) + .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) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + 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( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + await display.register_display(var, config) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp new file mode 100644 index 0000000000..471ad6664c --- /dev/null +++ b/esphome/components/st7789v/st7789v.cpp @@ -0,0 +1,274 @@ +#include "st7789v.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7789v { + +static const char *const TAG = "st7789v"; + +void ST7789V::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + + this->init_reset_(); + + this->write_command_(ST7789_SLPOUT); // Sleep out + delay(120); // NOLINT + + this->write_command_(ST7789_NORON); // Normal display mode on + + // *** display and color format setting *** + this->write_command_(ST7789_MADCTL); + this->write_data_(ST7789_MADCTL_COLOR_ORDER); + + // JLX240 display datasheet + this->write_command_(0xB6); + this->write_data_(0x0A); + this->write_data_(0x82); + + this->write_command_(ST7789_COLMOD); + this->write_data_(0x55); + delay(10); + + // *** ST7789V Frame rate setting *** + this->write_command_(ST7789_PORCTRL); + this->write_data_(0x0c); + this->write_data_(0x0c); + this->write_data_(0x00); + this->write_data_(0x33); + this->write_data_(0x33); + + this->write_command_(ST7789_GCTRL); // Voltages: VGH / VGL + this->write_data_(0x35); + + // *** ST7789V Power setting *** + this->write_command_(ST7789_VCOMS); + this->write_data_(0x28); // JLX240 display datasheet + + this->write_command_(ST7789_LCMCTRL); + this->write_data_(0x0C); + + this->write_command_(ST7789_VDVVRHEN); + this->write_data_(0x01); + this->write_data_(0xFF); + + this->write_command_(ST7789_VRHS); // voltage VRHS + this->write_data_(0x10); + + this->write_command_(ST7789_VDVS); + this->write_data_(0x20); + + this->write_command_(ST7789_FRCTRL2); + this->write_data_(0x0f); + + this->write_command_(ST7789_PWCTRL1); + this->write_data_(0xa4); + this->write_data_(0xa1); + + // *** ST7789V gamma setting *** + this->write_command_(ST7789_PVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x32); + this->write_data_(0x44); + this->write_data_(0x42); + this->write_data_(0x06); + this->write_data_(0x0e); + this->write_data_(0x12); + this->write_data_(0x14); + this->write_data_(0x17); + + this->write_command_(ST7789_NVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x31); + this->write_data_(0x54); + this->write_data_(0x47); + this->write_data_(0x0e); + this->write_data_(0x1c); + this->write_data_(0x17); + this->write_data_(0x1b); + this->write_data_(0x1e); + + this->write_command_(ST7789_INVON); + + // Clear display - ensures we do not see garbage at power-on + this->draw_filled_rect_(0, 0, 239, 319, 0x0000); + + delay(120); // NOLINT + + this->write_command_(ST7789_DISPON); // Display on + delay(120); // NOLINT + + backlight_(true); + + this->init_internal_(this->get_buffer_length_()); + memset(this->buffer_, 0x00, this->get_buffer_length_()); +} + +void ST7789V::dump_config() { + LOG_DISPLAY("", "SPI ST7789V", this); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" B/L Pin: ", this->backlight_pin_); + LOG_UPDATE_INTERVAL(this); +} + +float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7789V::update() { + this->do_update_(); + this->write_display_data(); +} + +void ST7789V::loop() {} + +void ST7789V::write_display_data() { + uint16_t x1 = 52; // _offsetx + uint16_t x2 = 186; // _offsetx + uint16_t y1 = 40; // _offsety + uint16_t y2 = 279; // _offsety + + this->enable(); + + // set column(x) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + // set page(y) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + // write display memory + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); + this->dc_pin_->digital_write(true); + + this->write_array(this->buffer_, this->get_buffer_length_()); + + this->disable(); +} + +void ST7789V::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +void ST7789V::backlight_(bool onoff) { + if (this->backlight_pin_ != nullptr) { + this->backlight_pin_->setup(); + this->backlight_pin_->digital_write(onoff); + } +} + +void ST7789V::write_command_(uint8_t value) { + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + this->disable(); +} + +void ST7789V::write_data_(uint8_t value) { + this->dc_pin_->digital_write(true); + this->enable(); + this->write_byte(value); + this->disable(); +} + +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; + + this->dc_pin_->digital_write(true); + this->write_array(byte, 4); +} + +void ST7789V::write_color_(uint16_t color, uint16_t size) { + static uint8_t byte[1024]; + int index = 0; + for (int i = 0; i < size; i++) { + byte[index++] = (color >> 8) & 0xFF; + byte[index++] = color & 0xFF; + } + + this->dc_pin_->digital_write(true); + return write_array(byte, size * 2); +} + +int ST7789V::get_height_internal() { + return 240; // 320; +} + +int ST7789V::get_width_internal() { + return 135; // 240; +} + +size_t ST7789V::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; +} + +// Draw a filled rectangle +// x1: Start X coordinate +// y1: Start Y coordinate +// x2: End X coordinate +// y2: End Y coordinate +// color: color +void ST7789V::draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { + // ESP_LOGD(TAG,"offset(x)=%d offset(y)=%d",dev->_offsetx,dev->_offsety); + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); // set column(x) address + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); // set Page(y) address + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); // begin a write to memory + this->dc_pin_->digital_write(true); + for (int i = x1; i <= x2; i++) { + uint16_t size = y2 - y1 + 1; + this->write_color_(color, size); + } + this->disable(); +} + +void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + auto color565 = display::ColorUtil::color_to_565(color); + + uint16_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h new file mode 100644 index 0000000000..2aef043ba0 --- /dev/null +++ b/esphome/components/st7789v/st7789v.h @@ -0,0 +1,151 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7789v { + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t ST7789_NOP = 0x00; // No Operation +static const uint8_t ST7789_SWRESET = 0x01; // Software Reset +static const uint8_t ST7789_RDDID = 0x04; // Read Display ID +static const uint8_t ST7789_RDDST = 0x09; // Read Display Status +static const uint8_t ST7789_RDDPM = 0x0A; // Read Display Power Mode +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 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 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 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 +static const uint8_t ST7789_CASET = 0x2A; // Column Address Set +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 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_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 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 +static const uint8_t ST7789_RDCTRLD = 0x54; // Read CTRL Value Display +static const uint8_t ST7789_WRCACE = 0x55; // Write Content Adaptive Brightness Control and Color Enhancement +static const uint8_t ST7789_RDCABC = 0x56; // Read Content Adaptive Brightness Control +static const uint8_t ST7789_WRCABCMB = 0x5E; // Write CABC Minimum Brightnes +static const uint8_t ST7789_RDCABCMB = 0x5F; // Read CABC Minimum Brightnes +static const uint8_t ST7789_RDABCSDR = 0x68; // Read Automatic Brightness Control Self-Diagnostic Result +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 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 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 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 +static const uint8_t ST7789_FRCTRL2 = 0xC6; // Frame Rate Control in Normal Mode +static const uint8_t ST7789_CABCCTRL = 0xC7; // CABC Control +static const uint8_t ST7789_REGSEL1 = 0xC8; // Register Value Selection 1 +static const uint8_t ST7789_REGSEL2 = 0xCA; // Register Value Selection +static const uint8_t ST7789_PWMFRSEL = 0xCC; // PWM Frequency Selection +static const uint8_t ST7789_PWCTRL1 = 0xD0; // Power Control 1 +static const uint8_t ST7789_VAPVANEN = 0xD2; // Enable VAP/VAN signal output +static const uint8_t ST7789_CMD2EN = 0xDF; // Command 2 Enable +static const uint8_t ST7789_PVGAMCTRL = 0xE0; // Positive Voltage Gamma Control +static const uint8_t ST7789_NVGAMCTRL = 0xE1; // Negative Voltage Gamma Control +static const uint8_t ST7789_DGMLUTR = 0xE2; // Digital Gamma Look-up Table for Red +static const uint8_t ST7789_DGMLUTB = 0xE3; // Digital Gamma Look-up Table for Blue +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 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 + +// Flags for ST7789_MADCTL +static const uint8_t ST7789_MADCTL_MY = 0x80; +static const uint8_t ST7789_MADCTL_MX = 0x40; +static const uint8_t ST7789_MADCTL_MV = 0x20; +static const uint8_t ST7789_MADCTL_ML = 0x10; +static const uint8_t ST7789_MADCTL_RGB = 0x00; +static const uint8_t ST7789_MADCTL_BGR = 0x08; +static const uint8_t ST7789_MADCTL_MH = 0x04; +static const uint8_t ST7789_MADCTL_SS = 0x02; +static const uint8_t ST7789_MADCTL_GS = 0x01; + +static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; + +class ST7789V : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } + + // ========== 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 loop() override; + + void write_display_data(); + + protected: + GPIOPin *dc_pin_; + GPIOPin *reset_pin_{nullptr}; + GPIOPin *backlight_pin_{nullptr}; + + void init_reset_(); + void backlight_(bool onoff); + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_addr_(uint16_t addr1, uint16_t addr2); + void write_color_(uint16_t color, uint16_t size); + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + void draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; +}; + +} // namespace st7789v +} // namespace esphome 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..63fa0ba72f --- /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 < (uint8_t)(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/binary_sensor.py b/esphome/components/status/binary_sensor.py index 9963461cef..9367706388 100644 --- a/esphome/components/status/binary_sensor.py +++ b/esphome/components/status/binary_sensor.py @@ -1,20 +1,33 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID, CONF_DEVICE_CLASS, DEVICE_CLASS_CONNECTIVITY +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_DEVICE_CLASS, + DEVICE_CLASS_CONNECTIVITY, + ENTITY_CATEGORY_DIAGNOSTIC, +) -status_ns = cg.esphome_ns.namespace('status') -StatusBinarySensor = status_ns.class_('StatusBinarySensor', binary_sensor.BinarySensor, - cg.Component) +status_ns = cg.esphome_ns.namespace("status") +StatusBinarySensor = status_ns.class_( + "StatusBinarySensor", binary_sensor.BinarySensor, cg.Component +) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(StatusBinarySensor), - - cv.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_CONNECTIVITY): binary_sensor.device_class, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(StatusBinarySensor), + cv.Optional( + CONF_DEVICE_CLASS, default=DEVICE_CLASS_CONNECTIVITY + ): binary_sensor.device_class, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC + ): cv.entity_category, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp index 90ac1faad7..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 @@ -13,10 +13,10 @@ namespace esphome { namespace status { -static const char *TAG = "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/__init__.py b/esphome/components/status_led/__init__.py index 26105b5e92..92869ee630 100644 --- a/esphome/components/status_led/__init__.py +++ b/esphome/components/status_led/__init__.py @@ -4,20 +4,22 @@ import esphome.codegen as cg from esphome.const import CONF_ID, CONF_PIN from esphome.core import coroutine_with_priority -status_led_ns = cg.esphome_ns.namespace('status_led') -StatusLED = status_led_ns.class_('StatusLED', cg.Component) +status_led_ns = cg.esphome_ns.namespace("status_led") +StatusLED = status_led_ns.class_("StatusLED", cg.Component) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(StatusLED), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(StatusLED), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) @coroutine_with_priority(80.0) -def to_code(config): - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) +async def to_code(config): + pin = await cg.gpio_pin_expression(config[CONF_PIN]) rhs = StatusLED.new(pin) var = cg.Pvariable(config[CONF_ID], rhs) - yield cg.register_component(var, config) + await cg.register_component(var, config) cg.add(var.pre_setup()) - cg.add_define('USE_STATUS_LED') + cg.add_define("USE_STATUS_LED") 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.cpp b/esphome/components/status_led/status_led.cpp index 472b3cd92f..d2d56cf05b 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -5,9 +5,9 @@ namespace esphome { namespace status_led { -static const char *TAG = "status_led"; +static const char *const TAG = "status_led"; -StatusLED *global_status_led = nullptr; +StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; } void StatusLED::pre_setup() { diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index 7da325e460..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 { @@ -20,7 +20,7 @@ class StatusLED : public Component { GPIOPin *pin_; }; -extern StatusLED *global_status_led; +extern StatusLED *global_status_led; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace status_led } // namespace esphome diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index e8d6acbd1c..54f6aa4205 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -1,33 +1,43 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ACCELERATION, CONF_DECELERATION, CONF_ID, CONF_MAX_SPEED, \ - CONF_POSITION, CONF_TARGET, CONF_SPEED -from esphome.core import CORE, coroutine, coroutine_with_priority +from esphome.const import ( + CONF_ACCELERATION, + CONF_DECELERATION, + CONF_ID, + CONF_MAX_SPEED, + CONF_POSITION, + CONF_TARGET, + CONF_SPEED, +) +from esphome.core import CORE, coroutine_with_priority IS_PLATFORM_COMPONENT = True # pylint: disable=invalid-name -stepper_ns = cg.esphome_ns.namespace('stepper') -Stepper = stepper_ns.class_('Stepper') +stepper_ns = cg.esphome_ns.namespace("stepper") +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) +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): value = cv.string(value) - for suffix in ('steps/s^2', 'steps/s*s', 'steps/s/s', 'steps/ss', 'steps/(s*s)'): + for suffix in ("steps/s^2", "steps/s*s", "steps/s/s", "steps/ss", "steps/(s*s)"): if value.endswith(suffix): - value = value[:-len(suffix)] + value = value[: -len(suffix)] - if value == 'inf': + if value == "inf": return 1e6 try: value = float(value) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid(f"Expected acceleration as floating point number, got {value}") if value <= 0: @@ -38,16 +48,17 @@ def validate_acceleration(value): def validate_speed(value): value = cv.string(value) - for suffix in ('steps/s', 'steps/s'): + for suffix in ("steps/s", "steps/s"): if value.endswith(suffix): - value = value[:-len(suffix)] + value = value[: -len(suffix)] - if value == 'inf': + if value == "inf": return 1e6 try: value = float(value) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid(f"Expected speed as floating point number, got {value}") if value <= 0: @@ -56,15 +67,16 @@ def validate_speed(value): return value -STEPPER_SCHEMA = cv.Schema({ - cv.Required(CONF_MAX_SPEED): validate_speed, - cv.Optional(CONF_ACCELERATION, default='inf'): validate_acceleration, - cv.Optional(CONF_DECELERATION, default='inf'): validate_acceleration, -}) +STEPPER_SCHEMA = cv.Schema( + { + cv.Required(CONF_MAX_SPEED): validate_speed, + cv.Optional(CONF_ACCELERATION, default="inf"): validate_acceleration, + cv.Optional(CONF_DECELERATION, default="inf"): validate_acceleration, + } +) -@coroutine -def setup_stepper_core_(stepper_var, config): +async def setup_stepper_core_(stepper_var, config): if CONF_ACCELERATION in config: cg.add(stepper_var.set_acceleration(config[CONF_ACCELERATION])) if CONF_DECELERATION in config: @@ -73,49 +85,102 @@ def setup_stepper_core_(stepper_var, config): cg.add(stepper_var.set_max_speed(config[CONF_MAX_SPEED])) -@coroutine -def register_stepper(var, config): +async def register_stepper(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - yield setup_stepper_core_(var, config) + await setup_stepper_core_(var, config) -@automation.register_action('stepper.set_target', SetTargetAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(Stepper), - cv.Required(CONF_TARGET): cv.templatable(cv.int_), -})) -def stepper_set_target_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "stepper.set_target", + SetTargetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_TARGET): cv.templatable(cv.int_), + } + ), +) +async def stepper_set_target_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_ = yield cg.templatable(config[CONF_TARGET], args, cg.int32) + template_ = await cg.templatable(config[CONF_TARGET], args, cg.int32) cg.add(var.set_target(template_)) - yield var + return var -@automation.register_action('stepper.report_position', ReportPositionAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(Stepper), - cv.Required(CONF_POSITION): cv.templatable(cv.int_), -})) -def stepper_report_position_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "stepper.report_position", + ReportPositionAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_POSITION): cv.templatable(cv.int_), + } + ), +) +async def stepper_report_position_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_ = yield cg.templatable(config[CONF_POSITION], args, cg.int32) + template_ = await cg.templatable(config[CONF_POSITION], args, cg.int32) cg.add(var.set_position(template_)) - yield var + return var -@automation.register_action('stepper.set_speed', SetSpeedAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(Stepper), - cv.Required(CONF_SPEED): cv.templatable(validate_speed), -})) -def stepper_set_speed_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "stepper.set_speed", + SetSpeedAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_SPEED): cv.templatable(validate_speed), + } + ), +) +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_ = yield cg.templatable(config[CONF_SPEED], args, cg.int32) + template_ = await cg.templatable(config[CONF_SPEED], args, cg.float_) cg.add(var.set_speed(template_)) - yield var + 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) -def to_code(config): +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 b966fb00cc..7926024204 100644 --- a/esphome/components/stepper/stepper.cpp +++ b/esphome/components/stepper/stepper.cpp @@ -1,10 +1,11 @@ #include "stepper.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace stepper { -static const char *TAG = "stepper"; +static const char *const TAG = "stepper"; void Stepper::calculate_speed_(uint32_t now) { // delta t since last calculation in seconds 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 f48deeeae5..b02c835ef8 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -1,22 +1,40 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -sts3x_ns = cg.esphome_ns.namespace('sts3x') +sts3x_ns = cg.esphome_ns.namespace("sts3x") -STS3XComponent = sts3x_ns.class_('STS3XComponent', sensor.Sensor, - cg.PollingComponent, i2c.I2CDevice) +STS3XComponent = sts3x_ns.class_( + "STS3XComponent", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.GenerateID(): cv.declare_id(STS3XComponent), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x4A)) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(STS3XComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x4A)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index 1a24a17caf..b1ecbc98f8 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sts3x { -static const char *TAG = "sts3x"; +static const char *const TAG = "sts3x"; static const uint16_t STS3X_COMMAND_READ_SERIAL_NUMBER = 0x3780; static const uint16_t STS3X_COMMAND_READ_STATUS = 0xF32D; @@ -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 292a7bf299..0cef417c15 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -3,45 +3,52 @@ import re import esphome.config_validation as cv from esphome import core +from esphome.const import CONF_SUBSTITUTIONS +from esphome.yaml_util import ESPHomeDataBase, make_data_base +CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) -CONF_SUBSTITUTIONS = 'substitutions' - -VALID_SUBSTITUTIONS_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' \ - '0123456789_' +VALID_SUBSTITUTIONS_CHARACTERS = ( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" +) def validate_substitution_key(value): value = cv.string(value) if not value: raise cv.Invalid("Substitution key must not be empty") - if value[0] == '$': + if value[0] == "$": value = value[1:] if value[0].isdigit(): raise cv.Invalid("First character in substitutions cannot be a digit.") 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 -CONFIG_SCHEMA = cv.Schema({ - validate_substitution_key: cv.string_strict, -}) +CONFIG_SCHEMA = cv.Schema( + { + validate_substitution_key: cv.string_strict, + } +) -def to_code(config): +async def to_code(config): pass -VARIABLE_PROG = re.compile('\\$([{0}]+|\\{{[{0}]*\\}})'.format(VALID_SUBSTITUTIONS_CHARACTERS)) +# pylint: disable=consider-using-f-string +VARIABLE_PROG = re.compile( + "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) +) def _expand_substitutions(substitutions, value, path): - if '$' not in value: + if "$" not in value: return value orig_value = value @@ -55,11 +62,16 @@ def _expand_substitutions(substitutions, value, path): i, j = m.span(0) name = m.group(1) - if name.startswith('{') and name.endswith('}'): + if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: - _LOGGER.warning("Found '%s' (see %s) which looks like a substitution, but '%s' was " - "not declared", orig_value, '->'.join(str(x) for x in path), name) + _LOGGER.warning( + "Found '%s' (see %s) which looks like a substitution, but '%s' was " + "not declared", + orig_value, + "->".join(str(x) for x in path), + name, + ) i = j continue @@ -68,6 +80,14 @@ def _expand_substitutions(substitutions, value, path): value = value[:i] + sub i = len(value) value += tail + + # orig_value can also already be a lambda with esp_range info, and only + # a plain string is sent in orig_value + if isinstance(orig_value, ESPHomeDataBase): + # even though string can get larger or smaller, the range should point + # to original document marks + return make_data_base(value, orig_value) + return value @@ -101,15 +121,20 @@ def _substitute_item(substitutions, item, path): return None -def do_substitution_pass(config): - if CONF_SUBSTITUTIONS not in config: +def do_substitution_pass(config, command_line_substitutions): + if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return substitutions = config[CONF_SUBSTITUTIONS] - with cv.prepend_path('substitutions'): + if substitutions is None: + substitutions = command_line_substitutions + elif command_line_substitutions: + substitutions = {**substitutions, **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))) + raise cv.Invalid( + f"Substitutions must be a key to value mapping, got {type(substitutions)}" + ) replace_keys = [] for key, value in substitutions.items(): @@ -123,4 +148,6 @@ def do_substitution_pass(config): del substitutions[old] config[CONF_SUBSTITUTIONS] = substitutions + # Move substitutions to the first place to replace substitutions in them correctly + config.move_to_end(CONF_SUBSTITUTIONS, False) _substitute_item(substitutions, config, []) diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index e4d2023a8e..d00a00661a 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -1,107 +1,181 @@ +import re + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import time -from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +from esphome.const import ( + CONF_TIME_ID, + CONF_ID, + CONF_TRIGGER_ID, + CONF_LATITUDE, + CONF_LONGITUDE, +) -sun_ns = cg.esphome_ns.namespace('sun') +CODEOWNERS = ["@OttoWinter"] +sun_ns = cg.esphome_ns.namespace("sun") -Sun = sun_ns.class_('Sun') -SunTrigger = sun_ns.class_('SunTrigger', cg.PollingComponent, automation.Trigger.template()) -SunCondition = sun_ns.class_('SunCondition', automation.Condition) +Sun = sun_ns.class_("Sun") +SunTrigger = sun_ns.class_( + "SunTrigger", cg.PollingComponent, automation.Trigger.template() +) +SunCondition = sun_ns.class_("SunCondition", automation.Condition) -CONF_SUN_ID = 'sun_id' -CONF_LATITUDE = 'latitude' -CONF_LONGITUDE = 'longitude' -CONF_ELEVATION = 'elevation' -CONF_ON_SUNRISE = 'on_sunrise' -CONF_ON_SUNSET = 'on_sunset' +CONF_SUN_ID = "sun_id" +CONF_ELEVATION = "elevation" +CONF_ON_SUNRISE = "on_sunrise" +CONF_ON_SUNSET = "on_sunset" # Default sun elevation is a bit below horizon because sunset # means time when the entire sun disk is below the horizon -DEFAULT_ELEVATION = -0.883 +DEFAULT_ELEVATION = -0.83333 ELEVATION_MAP = { - 'sunrise': 0.0, - 'sunset': 0.0, - 'civil': -6.0, - 'nautical': -12.0, - 'astronomical': -18.0, + "sunrise": 0.0, + "sunset": 0.0, + "civil": -6.0, + "nautical": -12.0, + "astronomical": -18.0, } def elevation(value): if isinstance(value, str): try: - value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)] + value = ELEVATION_MAP[ + cv.one_of(*ELEVATION_MAP, lower=True, space="_")(value) + ] except cv.Invalid: pass value = cv.angle(value) return cv.float_range(min=-180, max=180)(value) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(Sun), - cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_LATITUDE): cv.float_range(min=-90, max=90), - cv.Required(CONF_LONGITUDE): cv.float_range(min=-180, max=180), - - cv.Optional(CONF_ON_SUNRISE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), - cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, - }), - cv.Optional(CONF_ON_SUNSET): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), - cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, - }), -}) +# Parses sexagesimal values like 22°57′7″S +LAT_LON_REGEX = re.compile( + r"([+\-])?\s*" + r"(?:([0-9]+)\s*°)?\s*" + r"(?:([0-9]+)\s*[′\'])?\s*" + r'(?:([0-9]+)\s*[″"])?\s*' + r"([NESW])?" +) -def to_code(config): +def parse_latlon(value): + if isinstance(value, str) and value.endswith("°"): + # strip trailing degree character + value = value[:-1] + try: + return cv.float_(value) + except cv.Invalid: + pass + + value = cv.string_strict(value) + m = LAT_LON_REGEX.match(value) + + if m is None: + raise cv.Invalid("Invalid format for latitude/longitude") + sign = m.group(1) + deg = m.group(2) + minute = m.group(3) + second = m.group(4) + d = m.group(5) + + val = float(deg or 0) + float(minute or 0) / 60 + float(second or 0) / 3600 + if sign == "-": + val *= -1 + if d and d in "SW": + val *= -1 + return val + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sun), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_LATITUDE): cv.All( + parse_latlon, cv.float_range(min=-90, max=90) + ), + cv.Required(CONF_LONGITUDE): cv.All( + parse_latlon, cv.float_range(min=-180, max=180) + ), + cv.Optional(CONF_ON_SUNRISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, + } + ), + cv.Optional(CONF_ON_SUNSET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, + } + ), + } +) + + +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - time_ = yield cg.get_variable(config[CONF_TIME_ID]) + time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) cg.add(var.set_latitude(config[CONF_LATITUDE])) cg.add(var.set_longitude(config[CONF_LONGITUDE])) for conf in config.get(CONF_ON_SUNRISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - yield cg.register_component(trigger, conf) - yield cg.register_parented(trigger, var) + await cg.register_component(trigger, conf) + await cg.register_parented(trigger, var) cg.add(trigger.set_sunrise(True)) cg.add(trigger.set_elevation(conf[CONF_ELEVATION])) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_SUNSET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - yield cg.register_component(trigger, conf) - yield cg.register_parented(trigger, var) + await cg.register_component(trigger, conf) + await cg.register_parented(trigger, var) cg.add(trigger.set_sunrise(False)) cg.add(trigger.set_elevation(conf[CONF_ELEVATION])) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) -@automation.register_condition('sun.is_above_horizon', SunCondition, cv.Schema({ - cv.GenerateID(): cv.use_id(Sun), - cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable(elevation), -})) -def sun_above_horizon_to_code(config, condition_id, template_arg, args): +@automation.register_condition( + "sun.is_above_horizon", + SunCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(Sun), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable( + elevation + ), + } + ), +) +async def sun_above_horizon_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double) + await cg.register_parented(var, config[CONF_ID]) + templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double) cg.add(var.set_elevation(templ)) cg.add(var.set_above(True)) - yield var + return var -@automation.register_condition('sun.is_below_horizon', SunCondition, cv.Schema({ - cv.GenerateID(): cv.use_id(Sun), - cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable(elevation), -})) -def sun_below_horizon_to_code(config, condition_id, template_arg, args): +@automation.register_condition( + "sun.is_below_horizon", + SunCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(Sun), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable( + elevation + ), + } + ), +) +async def sun_below_horizon_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double) + await cg.register_parented(var, config[CONF_ID]) + templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double) cg.add(var.set_elevation(templ)) cg.add(var.set_above(False)) - yield var + return var diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index 5ca315888d..236acfadef 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -1,30 +1,47 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import UNIT_DEGREES, ICON_WEATHER_SUNSET, CONF_ID, CONF_TYPE +from esphome.const import ( + STATE_CLASS_NONE, + UNIT_DEGREES, + ICON_WEATHER_SUNSET, + CONF_ID, + CONF_TYPE, +) from .. import sun_ns, CONF_SUN_ID, Sun -DEPENDENCIES = ['sun'] +DEPENDENCIES = ["sun"] -SunSensor = sun_ns.class_('SunSensor', sensor.Sensor, cg.PollingComponent) -SensorType = sun_ns.enum('SensorType') +SunSensor = sun_ns.class_("SunSensor", sensor.Sensor, cg.PollingComponent) +SensorType = sun_ns.enum("SensorType") TYPES = { - 'elevation': SensorType.SUN_SENSOR_ELEVATION, - 'azimuth': SensorType.SUN_SENSOR_AZIMUTH, + "elevation": SensorType.SUN_SENSOR_ELEVATION, + "azimuth": SensorType.SUN_SENSOR_AZIMUTH, } -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DEGREES, ICON_WEATHER_SUNSET, 1).extend({ - cv.GenerateID(): cv.declare_id(SunSensor), - cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), - cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True), -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + icon=ICON_WEATHER_SUNSET, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(SunSensor), + cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), + cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) cg.add(var.set_type(config[CONF_TYPE])) - paren = yield cg.get_variable(config[CONF_SUN_ID]) + paren = await cg.get_variable(config[CONF_SUN_ID]) cg.add(var.set_parent(paren)) diff --git a/esphome/components/sun/sensor/sun_sensor.cpp b/esphome/components/sun/sensor/sun_sensor.cpp index 63b7715287..6c90722c29 100644 --- a/esphome/components/sun/sensor/sun_sensor.cpp +++ b/esphome/components/sun/sensor/sun_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sun { -static const char *TAG = "sun.sensor"; +static const char *const TAG = "sun.sensor"; void SunSensor::dump_config() { LOG_SENSOR("", "Sun Sensor", this); } diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 6744a418c5..113c14d431 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -1,176 +1,317 @@ #include "sun.h" #include "esphome/core/log.h" +/* +The formulas/algorithms in this module are based on the book +"Astronomical algorithms" by Jean Meeus (2nd edition) + +The target accuracy of this implementation is ~1min for sunrise/sunset calculations, +and 6 arcminutes for elevation/azimuth. As such, some of the advanced correction factors +like exact nutation are not included. But in some testing the accuracy appears to be within range +for random spots around the globe. +*/ + namespace esphome { namespace sun { -static const char *TAG = "sun"; +using namespace esphome::sun::internal; + +static const char *const TAG = "sun"; #undef PI +#undef degrees +#undef radians +#undef sq -/* Usually, ESPHome uses single-precision floating point values - * because those tend to be accurate enough and are more efficient. - * - * However, some of the data in this class has to be quite accurate, so double is - * used everywhere. - */ -static const double PI = 3.141592653589793; -static const double TAU = 6.283185307179586; -static const double TO_RADIANS = PI / 180.0; -static const double TO_DEGREES = 180.0 / PI; -static const double EARTH_TILT = 23.44 * TO_RADIANS; +static const num_t PI = 3.141592653589793; +inline num_t degrees(num_t rad) { return rad * 180 / PI; } +inline num_t radians(num_t deg) { return deg * PI / 180; } +inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } +inline num_t sq(num_t x) { return x * x; } +inline num_t cb(num_t x) { return x * x * x; } -optional Sun::sunrise(double elevation) { - auto time = this->time_->now(); - if (!time.is_valid()) - return {}; - double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true); - if (isnan(sun_time)) - return {}; - uint32_t epoch = this->calc_epoch_(time, sun_time); - return time::ESPTime::from_epoch_local(epoch); -} -optional Sun::sunset(double elevation) { - auto time = this->time_->now(); - if (!time.is_valid()) - return {}; - double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false); - if (isnan(sun_time)) - return {}; - uint32_t epoch = this->calc_epoch_(time, sun_time); - return time::ESPTime::from_epoch_local(epoch); -} -double Sun::elevation() { - auto time = this->current_sun_time_(); - if (isnan(time)) - return NAN; - return this->elevation_(time); -} -double Sun::azimuth() { - auto time = this->current_sun_time_(); - if (isnan(time)) - return NAN; - return this->azimuth_(time); -} -// like clamp, but with doubles -double clampd(double val, double min, double max) { - if (val < min) - return min; - if (val > max) - return max; - return val; -} -double Sun::sun_declination_(double sun_time) { - double n = sun_time - 1.0; - // maximum declination - const double tot = -sin(EARTH_TILT); +num_t GeoLocation::latitude_rad() const { return radians(latitude); } +num_t GeoLocation::longitude_rad() const { return radians(longitude); } +num_t EquatorialCoordinate::right_ascension_rad() const { return radians(right_ascension); } +num_t EquatorialCoordinate::declination_rad() const { return radians(declination); } +num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); } +num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); } - // eccentricity of the earth's orbit (ellipse) - double eccentricity = 0.0167; - - // days since perihelion (January 3rd) - double days_since_perihelion = n - 2; - // days since december solstice (december 22) - double days_since_december_solstice = n + 10; - const double c = TAU / 365.24; - double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion)); - // Make sure value is in range (double error may lead to results slightly larger than 1) - double x = clampd(tot * v, -1.0, 1.0); - return asin(x); +num_t julian_day(time::ESPTime moment) { + // p. 59 + // UT -> JD, TT -> JDE + int y = moment.year; + int m = moment.month; + num_t d = moment.day_of_month; + d += moment.hour / 24.0; + d += moment.minute / (24.0 * 60); + d += moment.second / (24.0 * 60 * 60); + if (m <= 2) { + y -= 1; + m += 12; + } + int a = y / 100; + int b = 2 - a + a / 4; + return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5; } -double Sun::elevation_ratio_(double sun_time) { - double decl = this->sun_declination_(sun_time); - double hangle = this->hour_angle_(sun_time); - double a = sin(this->latitude_rad_()) * sin(decl); - double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle); - double val = clampd(a + b, -1.0, 1.0); - return val; +num_t delta_t(time::ESPTime moment) { + // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html) + int t = moment.year - 2000; + return 62.92 + t * (0.32217 + t * 0.005589); } -double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; } -double Sun::hour_angle_(double sun_time) { - double time_of_day = fmod(sun_time, 1.0) * 24.0; - return -PI * (time_of_day - 12) / 12; +// Perform a fractional module operation where the result will always be positive (wrapping around) +num_t wmod(num_t x, num_t y) { + num_t res = fmod(x, y); + if (res < 0) + res += y; + return res; } -double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; } -double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); } -double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); } -double Sun::azimuth_rad_(double sun_time) { - double hangle = -this->hour_angle_(sun_time); - double decl = this->sun_declination_(sun_time); - double zen = this->zenith_rad_(sun_time); - double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl); - double denom = sin(zen) * cos(this->latitude_rad_()); - double v = clampd(nom / denom, -1.0, 1.0); - double az = PI - acos(v); - if (hangle > 0) - az = -az; - if (az < 0) - az += TAU; - return az; + +num_t internal::Moment::jd() const { return julian_day(dt); } + +num_t internal::Moment::jde() const { + // dt is in UT1, but JDE is based on TT + // so add deltaT factor + return jd() + delta_t(dt) / (60 * 60 * 24); } -double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; } -double Sun::calc_sun_time_(const time::ESPTime &time) { - // Time as seen at 0° longitude - if (!time.is_valid()) - return NAN; - double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0); - // Add longitude correction - double add = this->longitude_ / 360.0; - return base + add; -} -uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) { - sun_time -= this->longitude_ / 360.0; - base.day_of_year = uint32_t(floor(sun_time)); +struct SunAtTime { + num_t jde; + num_t t; - sun_time = (sun_time - base.day_of_year) * 24.0; - base.hour = uint32_t(floor(sun_time)); + // eq 25.1, p. 163; julian centuries from the epoch J2000.0 + SunAtTime(num_t jde) : jde(jde), t((jde - 2451545) / 36525.0) {} - sun_time = (sun_time - base.hour) * 60.0; - base.minute = uint32_t(floor(sun_time)); - - sun_time = (sun_time - base.minute) * 60.0; - base.second = uint32_t(floor(sun_time)); - - base.recalc_timestamp_utc(true); - return base.timestamp; -} -double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) { - // Use binary search, newton's method would be better but binary search already - // converges quite well (19 cycles) and much simpler. Function is guaranteed to be - // monotonous. - double lo, hi; - if (rising) { - lo = day_of_year + 0.0; - hi = day_of_year + 0.5; - } else { - lo = day_of_year + 1.0; - hi = day_of_year + 0.5; + num_t mean_obliquity() const { + // eq. 22.2, p. 147; mean obliquity of the ecliptic + num_t epsilon_0 = (+arcdeg(23, 26, 21.448) - arcdeg(0, 0, 46.8150) * t - arcdeg(0, 0, 0.00059) * sq(t) + + arcdeg(0, 0, 0.001813) * cb(t)); + return epsilon_0; } - double min_elevation = this->elevation_(lo); - double max_elevation = this->elevation_(hi); - if (elevation < min_elevation || elevation > max_elevation) - return NAN; - - // Accuracy: 0.1s - const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0); - - while (fabs(hi - lo) > accuracy) { - double mid = (lo + hi) / 2.0; - double value = this->elevation_(mid) - elevation; - if (value < 0) { - lo = mid; - } else if (value > 0) { - hi = mid; - } else { - lo = hi = mid; - break; - } + num_t omega() const { + // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic + // in degrees + num_t omega = 125.05 - 1934.136 * t; + return omega; } - return (lo + hi) / 2.0; + num_t true_obliquity() const { + // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic + num_t delta_epsilon = 0.00256 * cos(radians(omega())); + num_t epsilon = mean_obliquity() + delta_epsilon; + return epsilon; + } + + num_t mean_longitude() const { + // eq 25.2, p. 163; geometric mean longitude = mean equinox of the date in degrees + num_t l0 = 280.46646 + 36000.76983 * t + 0.0003032 * sq(t); + return wmod(l0, 360); + } + + num_t eccentricity() const { + // eq 25.4, p. 163; eccentricity of earth's orbit + num_t e = 0.016708634 - 0.000042037 * t - 0.0000001267 * sq(t); + return e; + } + + num_t mean_anomaly() const { + // eq 25.3, p. 163; mean anomaly of the sun in degrees + num_t m = 357.52911 + 35999.05029 * t - 0.0001537 * sq(t); + return wmod(m, 360); + } + + num_t equation_of_center() const { + // p. 164; sun's equation of the center c in degrees + num_t m_rad = radians(mean_anomaly()); + num_t c = ((1.914602 - 0.004817 * t - 0.000014 * sq(t)) * sin(m_rad) + (0.019993 - 0.000101 * t) * sin(2 * m_rad) + + 0.000289 * sin(3 * m_rad)); + return wmod(c, 360); + } + + num_t true_longitude() const { + // p. 164; sun's true longitude in degrees + num_t x = mean_longitude() + equation_of_center(); + return wmod(x, 360); + } + + num_t true_anomaly() const { + // p. 164; sun's true anomaly in degrees + num_t x = mean_anomaly() + equation_of_center(); + return wmod(x, 360); + } + + num_t apparent_longitude() const { + // p. 164; sun's apparent longitude = true equinox in degrees + num_t x = true_longitude() - 0.00569 - 0.00478 * sin(radians(omega())); + return wmod(x, 360); + } + + EquatorialCoordinate equatorial_coordinate() const { + num_t epsilon_rad = radians(true_obliquity()); + // eq. 25.6; p. 165; sun's right ascension alpha + num_t app_lon_rad = radians(apparent_longitude()); + num_t right_ascension_rad = atan2(cos(epsilon_rad) * sin(app_lon_rad), cos(app_lon_rad)); + num_t declination_rad = asin(sin(epsilon_rad) * sin(app_lon_rad)); + return EquatorialCoordinate{degrees(right_ascension_rad), degrees(declination_rad)}; + } + + num_t equation_of_time() const { + // chapter 28, p. 185 + num_t epsilon_half = radians(true_obliquity() / 2); + num_t y = sq(tan(epsilon_half)); + num_t l2 = 2 * mean_longitude(); + num_t l2_rad = radians(l2); + num_t e = eccentricity(); + num_t m = mean_anomaly(); + num_t m_rad = radians(m); + num_t sin_m = sin(m_rad); + num_t eot = (y * sin(l2_rad) - 2 * e * sin_m + 4 * e * y * sin_m * cos(l2_rad) - 1 / 2.0 * sq(y) * sin(2 * l2_rad) - + 5 / 4.0 * sq(e) * sin(2 * m_rad)); + return degrees(eot); + } + + void debug() const { + // debug output like in example 25.a, p. 165 + ESP_LOGV(TAG, "jde: %f", jde); + ESP_LOGV(TAG, "T: %f", t); + ESP_LOGV(TAG, "L_0: %f", mean_longitude()); + ESP_LOGV(TAG, "M: %f", mean_anomaly()); + ESP_LOGV(TAG, "e: %f", eccentricity()); + ESP_LOGV(TAG, "C: %f", equation_of_center()); + ESP_LOGV(TAG, "Odot: %f", true_longitude()); + ESP_LOGV(TAG, "Omega: %f", omega()); + ESP_LOGV(TAG, "lambda: %f", apparent_longitude()); + ESP_LOGV(TAG, "epsilon_0: %f", mean_obliquity()); + ESP_LOGV(TAG, "epsilon: %f", true_obliquity()); + ESP_LOGV(TAG, "v: %f", true_anomaly()); + auto eq = equatorial_coordinate(); + ESP_LOGV(TAG, "right_ascension: %f", eq.right_ascension); + ESP_LOGV(TAG, "declination: %f", eq.declination); + } +}; + +struct SunAtLocation { + GeoLocation location; + + num_t greenwich_sidereal_time(Moment moment) const { + // Return the greenwich mean sidereal time for this instant in degrees + // see chapter 12, p. 87 + num_t jd = moment.jd(); + // eq 12.1, p.87; jd for 0h UT of this date + time::ESPTime moment_0h = moment.dt; + moment_0h.hour = moment_0h.minute = moment_0h.second = 0; + num_t jd0 = Moment{moment_0h}.jd(); + num_t t = (jd0 - 2451545) / 36525; + // eq. 12.4, p.88 + num_t gmst = (+280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * sq(t) - (1 / 38710000.0) * cb(t)); + return wmod(gmst, 360); + } + + HorizontalCoordinate true_coordinate(Moment moment) const { + auto eq = SunAtTime(moment.jde()).equatorial_coordinate(); + num_t gmst = greenwich_sidereal_time(moment); + // do not apply any nutation correction (not important for our target accuracy) + num_t nutation_corr = 0; + + num_t ra = eq.right_ascension; + num_t alpha = gmst + nutation_corr + location.longitude - ra; + alpha = wmod(alpha, 360); + num_t alpha_rad = radians(alpha); + + num_t sin_lat = sin(location.latitude_rad()); + num_t cos_lat = cos(location.latitude_rad()); + num_t sin_elevation = (+sin_lat * sin(eq.declination_rad()) + cos_lat * cos(eq.declination_rad()) * cos(alpha_rad)); + num_t elevation_rad = asin(sin_elevation); + num_t azimuth_rad = atan2(sin(alpha_rad), cos(alpha_rad) * sin_lat - tan(eq.declination_rad()) * cos_lat); + return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180}; + } + + optional sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); } + optional sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); } + optional event(bool rise, time::ESPTime date, num_t zenith) const { + // couldn't get the method described in chapter 15 to work, + // so instead this is based on the algorithm in time4j + // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java + auto m = local_event_(date, 12); // noon + num_t jde = julian_day(m); + num_t new_h = 0, old_h; + do { + old_h = new_h; + auto x = local_hour_angle_(jde + old_h / 86400, rise, zenith); + if (!x.has_value()) + return {}; + new_h = *x; + } while (std::abs(new_h - old_h) >= 15); + time_t new_timestamp = m.timestamp + (time_t) new_h; + return time::ESPTime::from_epoch_local(new_timestamp); + } + + protected: + optional local_hour_angle_(num_t jde, bool rise, num_t zenith) const { + auto pos = SunAtTime(jde).equatorial_coordinate(); + num_t dec_rad = pos.declination_rad(); + num_t lat_rad = location.latitude_rad(); + num_t num = cos(radians(zenith)) - (sin(dec_rad) * sin(lat_rad)); + num_t denom = cos(dec_rad) * cos(lat_rad); + num_t cos_h = num / denom; + if (cos_h > 1 || cos_h < -1) + return {}; + num_t hour_angle = degrees(acos(cos_h)) * 240; + if (rise) + hour_angle *= -1; + return hour_angle; + } + + time::ESPTime local_event_(time::ESPTime date, int hour) const { + // input date should be in UTC, and hour/minute/second fields 0 + num_t added_d = hour / 24.0 - location.longitude / 360; + num_t jd = julian_day(date) + added_d; + + num_t eot = SunAtTime(jd).equation_of_time() * 240; + time_t new_timestamp = (time_t)(date.timestamp + added_d * 86400 - eot); + return time::ESPTime::from_epoch_utc(new_timestamp); + } +}; + +HorizontalCoordinate Sun::calc_coords_() { + SunAtLocation sun{location_}; + Moment m{time_->utcnow()}; + if (!m.dt.is_valid()) + return HorizontalCoordinate{NAN, NAN}; + + // uncomment to print some debug output + /* + SunAtTime st{m.jde()}; + st.debug(); + */ + return sun.true_coordinate(m); } +optional Sun::calc_event_(bool rising, double zenith) { + SunAtLocation sun{location_}; + auto now = this->time_->utcnow(); + if (!now.is_valid()) + return {}; + // Calculate UT1 timestamp at 0h + auto today = now; + today.hour = today.minute = today.second = 0; + today.recalc_timestamp_utc(); + + auto it = sun.event(rising, today, zenith); + if (it.has_value() && it->timestamp < now.timestamp) { + // We're calculating *next* sunrise/sunset, but calculated event + // is today, so try again tomorrow + time_t new_timestamp = today.timestamp + 24 * 60 * 60; + today = time::ESPTime::from_epoch_utc(new_timestamp); + it = sun.event(rising, today, zenith); + } + return it; +} + +optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } +optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +double Sun::elevation() { return this->calc_coords_().elevation; } +double Sun::azimuth() { return this->calc_coords_().azimuth; } } // namespace sun } // namespace esphome diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 501d122da0..0a2e6bcf97 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -8,92 +8,79 @@ namespace esphome { namespace sun { +namespace internal { + +/* Usually, ESPHome uses single-precision floating point values + * because those tend to be accurate enough and are more efficient. + * + * However, some of the data in this class has to be quite accurate, so double is + * used everywhere. + */ +using num_t = double; +struct GeoLocation { + num_t latitude; + num_t longitude; + + num_t latitude_rad() const; + num_t longitude_rad() const; +}; + +struct Moment { + time::ESPTime dt; + + num_t jd() const; + num_t jde() const; +}; + +struct EquatorialCoordinate { + num_t right_ascension; + num_t declination; + + num_t right_ascension_rad() const; + num_t declination_rad() const; +}; + +struct HorizontalCoordinate { + num_t elevation; + num_t azimuth; + + num_t elevation_rad() const; + num_t azimuth_rad() const; +}; + +} // namespace internal + class Sun { public: void set_time(time::RealTimeClock *time) { time_ = time; } time::RealTimeClock *get_time() const { return time_; } - void set_latitude(double latitude) { latitude_ = latitude; } - void set_longitude(double longitude) { longitude_ = longitude; } + void set_latitude(double latitude) { location_.latitude = latitude; } + void set_longitude(double longitude) { location_.longitude = longitude; } - optional sunrise(double elevation = 0.0); - optional sunset(double elevation = 0.0); + optional sunrise(double elevation); + optional sunset(double elevation); double elevation(); double azimuth(); protected: - double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); } - - /** Calculate the declination of the sun in rad. - * - * See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth - * - * Accuracy: ±0.2° - * - * @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_. - * @return Sun declination in degrees - */ - double sun_declination_(double sun_time); - - double elevation_ratio_(double sun_time); - - /** Calculate the hour angle based on the sun time of day in hours. - * - * Positive in morning, 0 at noon, negative in afternoon. - * - * @param sun_time Sun time, see calc_sun_time_. - * @return Hour angle in rad. - */ - double hour_angle_(double sun_time); - - double elevation_(double sun_time); - - double elevation_rad_(double sun_time); - - double zenith_rad_(double sun_time); - - double azimuth_rad_(double sun_time); - - double azimuth_(double sun_time); - - /** Return the sun time given by the time_ object. - * - * Sun time is defined as doubleing point day of year. - * Integer part encodes the day of the year (1=January 1st) - * Decimal part encodes time of day (1/24 = 1 hour) - */ - double calc_sun_time_(const time::ESPTime &time); - - uint32_t calc_epoch_(time::ESPTime base, double sun_time); - - /** Calculate the sun time of day - * - * @param day_of_year - * @param elevation - * @param rising - * @return - */ - double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising); - - double latitude_rad_(); + internal::HorizontalCoordinate calc_coords_(); + optional calc_event_(bool rising, double zenith); time::RealTimeClock *time_; - /// Latitude in degrees, range: -90 to 90. - double latitude_; - /// Longitude in degrees, range: -180 to 180. - double longitude_; + internal::GeoLocation location_; }; class SunTrigger : public Trigger<>, public PollingComponent, public Parented { public: - SunTrigger() : PollingComponent(1000) {} + SunTrigger() : PollingComponent(60000) {} void set_sunrise(bool sunrise) { sunrise_ = sunrise; } void set_elevation(double elevation) { elevation_ = elevation; } void update() override { double current = this->parent_->elevation(); - if (isnan(current)) + if (std::isnan(current)) return; bool crossed; @@ -103,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/sun/text_sensor/__init__.py b/esphome/components/sun/text_sensor/__init__.py index 984250f9f7..ac1ce223d1 100644 --- a/esphome/components/sun/text_sensor/__init__.py +++ b/esphome/components/sun/text_sensor/__init__.py @@ -1,16 +1,24 @@ from esphome.components import text_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ICON, ICON_WEATHER_SUNSET_DOWN, ICON_WEATHER_SUNSET_UP, CONF_TYPE, \ - CONF_ID, CONF_FORMAT +from esphome.const import ( + CONF_ICON, + ICON_WEATHER_SUNSET_DOWN, + ICON_WEATHER_SUNSET_UP, + CONF_TYPE, + CONF_ID, + CONF_FORMAT, +) from .. import sun_ns, CONF_SUN_ID, Sun, CONF_ELEVATION, elevation, DEFAULT_ELEVATION -DEPENDENCIES = ['sun'] +DEPENDENCIES = ["sun"] -SunTextSensor = sun_ns.class_('SunTextSensor', text_sensor.TextSensor, cg.PollingComponent) +SunTextSensor = sun_ns.class_( + "SunTextSensor", text_sensor.TextSensor, cg.PollingComponent +) SUN_TYPES = { - 'sunset': False, - 'sunrise': True, + "sunset": False, + "sunrise": True, } @@ -18,27 +26,32 @@ def validate_optional_icon(config): if CONF_ICON not in config: config = config.copy() config[CONF_ICON] = { - 'sunset': ICON_WEATHER_SUNSET_DOWN, - 'sunrise': ICON_WEATHER_SUNSET_UP, + "sunset": ICON_WEATHER_SUNSET_DOWN, + "sunrise": ICON_WEATHER_SUNSET_UP, }[config[CONF_TYPE]] return config -CONFIG_SCHEMA = cv.All(text_sensor.TEXT_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SunTextSensor), - cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), - cv.Required(CONF_TYPE): cv.one_of(*SUN_TYPES, lower=True), - cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, - cv.Optional(CONF_FORMAT, default='%X'): cv.string_strict, -}).extend(cv.polling_component_schema('60s')), validate_optional_icon) +CONFIG_SCHEMA = cv.All( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SunTextSensor), + cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), + cv.Required(CONF_TYPE): cv.one_of(*SUN_TYPES, lower=True), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, + cv.Optional(CONF_FORMAT, default="%X"): cv.string_strict, + } + ).extend(cv.polling_component_schema("60s")), + validate_optional_icon, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) - paren = yield cg.get_variable(config[CONF_SUN_ID]) + paren = await cg.get_variable(config[CONF_SUN_ID]) cg.add(var.set_parent(paren)) cg.add(var.set_sunrise(SUN_TYPES[config[CONF_TYPE]])) cg.add(var.set_elevation(config[CONF_ELEVATION])) diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.cpp b/esphome/components/sun/text_sensor/sun_text_sensor.cpp index ee949584cc..c047b87fdd 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.cpp +++ b/esphome/components/sun/text_sensor/sun_text_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace sun { -static const char *TAG = "sun.text_sensor"; +static const char *const TAG = "sun.text_sensor"; void SunTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sun Text Sensor", this); } diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 3870631e13..08cbccbe35 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -3,96 +3,110 @@ import esphome.config_validation as cv 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, coroutine_with_priority +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, + 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 -switch_ns = cg.esphome_ns.namespace('switch_') -Switch = switch_ns.class_('Switch', cg.Nameable) -SwitchPtr = Switch.operator('ptr') +switch_ns = cg.esphome_ns.namespace("switch_") +Switch = switch_ns.class_("Switch", cg.EntityBase) +SwitchPtr = Switch.operator("ptr") -ToggleAction = switch_ns.class_('ToggleAction', automation.Action) -TurnOffAction = switch_ns.class_('TurnOffAction', automation.Action) -TurnOnAction = switch_ns.class_('TurnOnAction', automation.Action) -SwitchPublishAction = switch_ns.class_('SwitchPublishAction', automation.Action) +ToggleAction = switch_ns.class_("ToggleAction", automation.Action) +TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action) +TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) +SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) -SwitchCondition = switch_ns.class_('SwitchCondition', Condition) -SwitchTurnOnTrigger = switch_ns.class_('SwitchTurnOnTrigger', automation.Trigger.template()) -SwitchTurnOffTrigger = switch_ns.class_('SwitchTurnOffTrigger', automation.Trigger.template()) +SwitchCondition = switch_ns.class_("SwitchCondition", Condition) +SwitchTurnOnTrigger = switch_ns.class_( + "SwitchTurnOnTrigger", automation.Trigger.template() +) +SwitchTurnOffTrigger = switch_ns.class_( + "SwitchTurnOffTrigger", automation.Trigger.template() +) icon = cv.icon -SWITCH_SCHEMA = 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({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), - }), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), - }), -}) +SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), + cv.Optional(CONF_INVERTED): cv.boolean, + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), + } + ), + } +) -@coroutine -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])) +async def setup_switch_core_(var, config): + await setup_entity(var, config) + if CONF_INVERTED in config: cg.add(var.set_inverted(config[CONF_INVERTED])) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [], conf) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) - yield mqtt.register_mqtt_component(mqtt_, config) + await mqtt.register_mqtt_component(mqtt_, config) -@coroutine -def register_switch(var, config): +async def register_switch(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_switch(var)) - yield setup_switch_core_(var, config) + await setup_switch_core_(var, config) -SWITCH_ACTION_SCHEMA = maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(Switch), -}) +SWITCH_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Switch), + } +) -@automation.register_action('switch.toggle', ToggleAction, SWITCH_ACTION_SCHEMA) -@automation.register_action('switch.turn_off', TurnOffAction, SWITCH_ACTION_SCHEMA) -@automation.register_action('switch.turn_on', TurnOnAction, SWITCH_ACTION_SCHEMA) -def switch_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) +@automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) +@automation.register_action("switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA) +@automation.register_action("switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA) +async def switch_toggle_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('switch.is_on', SwitchCondition, SWITCH_ACTION_SCHEMA) -def switch_is_on_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren, True) +@automation.register_condition("switch.is_on", SwitchCondition, SWITCH_ACTION_SCHEMA) +async def switch_is_on_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, True) -@automation.register_condition('switch.is_off', SwitchCondition, SWITCH_ACTION_SCHEMA) -def switch_is_off_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren, False) +@automation.register_condition("switch.is_off", SwitchCondition, SWITCH_ACTION_SCHEMA) +async def switch_is_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, False) @coroutine_with_priority(100.0) -def to_code(config): +async def to_code(config): cg.add_global(switch_ns.using) - cg.add_define('USE_SWITCH') + cg.add_define("USE_SWITCH") diff --git a/esphome/components/switch/automation.cpp b/esphome/components/switch/automation.cpp index ba9bb7e05a..5989ae9ce3 100644 --- a/esphome/components/switch/automation.cpp +++ b/esphome/components/switch/automation.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace switch_ { -static const char *TAG = "switch.automation"; +static const char *const TAG = "switch.automation"; } // namespace switch_ } // namespace esphome diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 90bdabf0f4..579daf4d24 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -73,6 +73,7 @@ template class SwitchPublishAction : public Action { public: SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index a6a25dd9dc..e4d20719e1 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -4,19 +4,11 @@ namespace esphome { namespace switch_ { -static const char *TAG = "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 cd6cec429f..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" @@ -8,15 +9,15 @@ namespace esphome { namespace switch_ { #define LOG_SWITCH(prefix, type, obj) \ - if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ - if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ - if (obj->assumed_state()) { \ + if ((obj)->assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (obj->is_inverted()) { \ + if ((obj)->is_inverted()) { \ ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ } \ } @@ -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 11fcfe3955..879ced2fb3 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -2,76 +2,108 @@ 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' -CONF_KEY_COLUMNS = 'key_columns' -CONF_SLEEP_TIME = 'sleep_time' -CONF_SCAN_TIME = 'scan_time' -CONF_DEBOUNCE_TIME = 'debounce_time' +CONF_KEYPAD = "keypad" +CONF_KEY_ROWS = "key_rows" +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'] +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 -} +sx1509_ns = cg.esphome_ns.namespace("sx1509") -SX1509Component = sx1509_ns.class_('SX1509Component', cg.Component, i2c.I2CDevice) -SX1509GPIOPin = sx1509_ns.class_('SX1509GPIOPin', cg.GPIOPin) +SX1509Component = sx1509_ns.class_("SX1509Component", cg.Component, i2c.I2CDevice) +SX1509GPIOPin = sx1509_ns.class_("SX1509GPIOPin", cg.GPIOPin) -KEYPAD_SCHEMA = cv.Schema({ - cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8), - cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8), - cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192), - cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128), - cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64), -}) +KEYPAD_SCHEMA = cv.Schema( + { + cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8), + cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8), + cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192), + cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128), + cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64), + } +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SX1509Component), - cv.Optional(CONF_KEYPAD): cv.Schema(KEYPAD_SCHEMA), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x3E)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SX1509Component), + cv.Optional(CONF_KEYPAD): cv.Schema(KEYPAD_SCHEMA), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x3E)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) if CONF_KEYPAD in config: keypad = config[CONF_KEYPAD] cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS])) - if CONF_SLEEP_TIME in keypad and CONF_SCAN_TIME in keypad and CONF_DEBOUNCE_TIME in keypad: + if ( + CONF_SLEEP_TIME in keypad + and CONF_SCAN_TIME in keypad + and CONF_DEBOUNCE_TIME in keypad + ): cg.add(var.set_sleep_time(keypad[CONF_SLEEP_TIME])) cg.add(var.set_scan_time(keypad[CONF_SCAN_TIME])) cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME])) -CONF_SX1509 = 'sx1509' -CONF_SX1509_ID = 'sx1509_id' - -SX1509_OUTPUT_PIN_SCHEMA = cv.Schema({ - 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.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, -}) +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 -@pins.PIN_SCHEMA_REGISTRY.register(CONF_SX1509, - (SX1509_OUTPUT_PIN_SCHEMA, SX1509_INPUT_PIN_SCHEMA)) -def sx1509_pin_to_code(config): - parent = yield cg.get_variable(config[CONF_SX1509]) - yield SX1509GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], - config[CONF_INVERTED]) +CONF_SX1509 = "sx1509" +SX1509_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(SX1509GPIOPin), + cv.Required(CONF_SX1509): cv.use_id(SX1509Component), + 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, + } +) + + +@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]) + 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/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py index 9a65524383..1560af8e99 100644 --- a/esphome/components/sx1509/binary_sensor/__init__.py +++ b/esphome/components/sx1509/binary_sensor/__init__.py @@ -4,25 +4,27 @@ from esphome.components import binary_sensor from esphome.const import CONF_ID from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID -CONF_ROW = 'row' -CONF_COL = 'col' +CONF_ROW = "row" +CONF_COL = "col" -DEPENDENCIES = ['sx1509'] +DEPENDENCIES = ["sx1509"] -SX1509BinarySensor = sx1509_ns.class_('SX1509BinarySensor', binary_sensor.BinarySensor) +SX1509BinarySensor = sx1509_ns.class_("SX1509BinarySensor", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SX1509BinarySensor), - cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), - cv.Required(CONF_ROW): cv.int_range(min=0, max=4), - cv.Required(CONF_COL): cv.int_range(min=0, max=4), -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SX1509BinarySensor), + cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), + cv.Required(CONF_ROW): cv.int_range(min=0, max=4), + cv.Required(CONF_COL): cv.int_range(min=0, max=4), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) - hub = yield cg.get_variable(config[CONF_SX1509_ID]) + await binary_sensor.register_binary_sensor(var, config) + hub = await cg.get_variable(config[CONF_SX1509_ID]) cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL])) cg.add(hub.register_keypad_binary_sensor(var)) diff --git a/esphome/components/sx1509/output/__init__.py b/esphome/components/sx1509/output/__init__.py index 80aec0afd4..7afea0fbf8 100644 --- a/esphome/components/sx1509/output/__init__.py +++ b/esphome/components/sx1509/output/__init__.py @@ -4,22 +4,25 @@ from esphome.components import output from esphome.const import CONF_PIN, CONF_ID from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID -DEPENDENCIES = ['sx1509'] +DEPENDENCIES = ["sx1509"] -SX1509FloatOutputChannel = sx1509_ns.class_('SX1509FloatOutputChannel', - output.FloatOutput, cg.Component) +SX1509FloatOutputChannel = sx1509_ns.class_( + "SX1509FloatOutputChannel", output.FloatOutput, cg.Component +) -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(SX1509FloatOutputChannel), - cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), - cv.Required(CONF_PIN): cv.int_range(min=0, max=15), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(SX1509FloatOutputChannel), + cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), + cv.Required(CONF_PIN): cv.int_range(min=0, max=15), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): - parent = yield cg.get_variable(config[CONF_SX1509_ID]) +async def to_code(config): + parent = await cg.get_variable(config[CONF_SX1509_ID]) var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield output.register_output(var, config) + await cg.register_component(var, config) + await output.register_output(var, config) cg.add(var.set_pin(config[CONF_PIN])) cg.add(var.set_parent(parent)) diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp index 7ff1bbb61b..e9c401eeed 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.cpp +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace sx1509 { -static const char *TAG = "sx1509_float_channel"; +static const char *const TAG = "sx1509_float_channel"; void SX1509FloatOutputChannel::write_state(float state) { const uint16_t max_duty = 255; @@ -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 2806a1cac2..9095dfeffa 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace sx1509 { -static const char *TAG = "sx1509"; +static const char *const TAG = "sx1509"; void SX1509Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SX1509Component..."); @@ -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 - byte reg_clock = osc_source | osc_pin_function | osc_freq_out; + 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 1d1c87b4e6..2c6e0b0c32 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -5,16 +5,17 @@ namespace esphome { namespace sx1509 { -static const char *TAG = "sx1509_gpio_pin"; +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 new file mode 100644 index 0000000000..0f222b8fc7 --- /dev/null +++ b/esphome/components/tca9548a/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +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.Component, i2c.I2CDevice) +TCA9548AChannel = tca9548a_ns.class_("TCA9548AChannel", i2c.I2CBus) + +MULTI_CONF = True + +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]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + 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 new file mode 100644 index 0000000000..f3f8685287 --- /dev/null +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -0,0 +1,52 @@ +#include "tca9548a.h" +#include "esphome/core/log.h" + +namespace esphome { +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(&status, 1) != i2c::ERROR_OK) { + ESP_LOGI(TAG, "TCA9548A failed"); + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Channels currently open: %d", status); +} +void TCA9548AComponent::dump_config() { + ESP_LOGCONFIG(TAG, "TCA9548A:"); + LOG_I2C_DEVICE(this); +} + +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 +} // namespace esphome diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h new file mode 100644 index 0000000000..39d07c2eb4 --- /dev/null +++ b/esphome/components/tca9548a/tca9548a.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tca9548a { + +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 setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } + void update(); + + i2c::ErrorCode switch_to_channel(uint8_t channel); + + protected: + friend class TCA9548AChannel; + uint8_t current_channel_ = 255; +}; +} // namespace tca9548a +} // namespace esphome diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 3c94f4a243..9facd6b8db 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -3,16 +3,19 @@ import esphome.config_validation as cv from esphome.components import climate_ir from esphome.const import CONF_ID -AUTO_LOAD = ['climate_ir'] +AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@glmnet"] -tcl112_ns = cg.esphome_ns.namespace('tcl112') -Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) +tcl112_ns = cg.esphome_ns.namespace("tcl112") +Tcl112Climate = tcl112_ns.class_("Tcl112Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(Tcl112Climate), -}) +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Tcl112Climate), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield climate_ir.register_climate_ir(var, config) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index 3e7eb7ec9a..5b938ba0c3 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace tcl112 { -static const char *TAG = "tcl112.climate"; +static const char *const TAG = "tcl112.climate"; const uint16_t TCL112_STATE_LENGTH = 14; const uint16_t TCL112_BITS = TCL112_STATE_LENGTH * 8; @@ -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; @@ -88,7 +88,7 @@ void Tcl112Climate::transmit_state() { // Set fan uint8_t selected_fan; - switch (this->fan_mode) { + switch (this->fan_mode.value()) { case climate::CLIMATE_FAN_HIGH: selected_fan = TCL112_FAN_HIGH; 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 287b2e441c..fcc56e395f 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -1,79 +1,131 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_COLOR_TEMPERATURE, CONF_GAIN, CONF_ID, \ - CONF_ILLUMINANCE, CONF_INTEGRATION_TIME, ICON_LIGHTBULB, \ - UNIT_PERCENT, ICON_THERMOMETER, UNIT_KELVIN, ICON_BRIGHTNESS_5, UNIT_LUX +from esphome.const import ( + CONF_COLOR_TEMPERATURE, + CONF_GAIN, + CONF_ID, + CONF_ILLUMINANCE, + CONF_GLASS_ATTENUATION_FACTOR, + CONF_INTEGRATION_TIME, + DEVICE_CLASS_ILLUMINANCE, + ICON_LIGHTBULB, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + ICON_THERMOMETER, + UNIT_KELVIN, + UNIT_LUX, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -CONF_RED_CHANNEL = 'red_channel' -CONF_GREEN_CHANNEL = 'green_channel' -CONF_BLUE_CHANNEL = 'blue_channel' -CONF_CLEAR_CHANNEL = 'clear_channel' +CONF_RED_CHANNEL = "red_channel" +CONF_GREEN_CHANNEL = "green_channel" +CONF_BLUE_CHANNEL = "blue_channel" +CONF_CLEAR_CHANNEL = "clear_channel" -tcs34725_ns = cg.esphome_ns.namespace('tcs34725') -TCS34725Component = tcs34725_ns.class_('TCS34725Component', cg.PollingComponent, i2c.I2CDevice) +tcs34725_ns = cg.esphome_ns.namespace("tcs34725") +TCS34725Component = tcs34725_ns.class_( + "TCS34725Component", cg.PollingComponent, i2c.I2CDevice +) -TCS34725IntegrationTime = tcs34725_ns.enum('TCS34725IntegrationTime') +TCS34725IntegrationTime = tcs34725_ns.enum("TCS34725IntegrationTime") TCS34725_INTEGRATION_TIMES = { - '2.4ms': TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS, - '24ms': TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS, - '50ms': TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS, - '101ms': TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_101MS, - '154ms': TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_154MS, - '700ms': TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_700MS, + "2.4ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS, + "24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS, + "50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS, + "101ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_101MS, + "120ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_120MS, + "154ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_154MS, + "180ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_180MS, + "199ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_199MS, + "240ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_240MS, + "300ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_300MS, + "360ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_360MS, + "401ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_401MS, + "420ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_420MS, + "480ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_480MS, + "499ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_499MS, + "540ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_540MS, + "600ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_600MS, + "614ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_614MS, } -TCS34725Gain = tcs34725_ns.enum('TCS34725Gain') +TCS34725Gain = tcs34725_ns.enum("TCS34725Gain") TCS34725_GAINS = { - '1X': TCS34725Gain.TCS34725_GAIN_1X, - '4X': TCS34725Gain.TCS34725_GAIN_4X, - '16X': TCS34725Gain.TCS34725_GAIN_16X, - '60X': TCS34725Gain.TCS34725_GAIN_60X, + "1X": TCS34725Gain.TCS34725_GAIN_1X, + "4X": TCS34725Gain.TCS34725_GAIN_4X, + "16X": TCS34725Gain.TCS34725_GAIN_16X, + "60X": TCS34725Gain.TCS34725_GAIN_60X, } -color_channel_schema = sensor.sensor_schema(UNIT_PERCENT, ICON_LIGHTBULB, 1) -color_temperature_schema = sensor.sensor_schema(UNIT_KELVIN, ICON_THERMOMETER, 1) -illuminance_schema = sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 1) +color_channel_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, +) +color_temperature_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_KELVIN, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, +) +illuminance_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, +) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(TCS34725Component), - cv.Optional(CONF_RED_CHANNEL): color_channel_schema, - cv.Optional(CONF_GREEN_CHANNEL): color_channel_schema, - cv.Optional(CONF_BLUE_CHANNEL): color_channel_schema, - cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema, - cv.Optional(CONF_ILLUMINANCE): illuminance_schema, - cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema, - cv.Optional(CONF_INTEGRATION_TIME, default='2.4ms'): - cv.enum(TCS34725_INTEGRATION_TIMES, lower=True), - cv.Optional(CONF_GAIN, default='1X'): cv.enum(TCS34725_GAINS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TCS34725Component), + cv.Optional(CONF_RED_CHANNEL): color_channel_schema, + cv.Optional(CONF_GREEN_CHANNEL): color_channel_schema, + cv.Optional(CONF_BLUE_CHANNEL): color_channel_schema, + cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema, + cv.Optional(CONF_ILLUMINANCE): illuminance_schema, + cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema, + cv.Optional(CONF_INTEGRATION_TIME, default="2.4ms"): cv.enum( + TCS34725_INTEGRATION_TIMES, lower=True + ), + cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True), + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( + min=1.0 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x29)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) if CONF_RED_CHANNEL in config: - sens = yield sensor.new_sensor(config[CONF_RED_CHANNEL]) + sens = await sensor.new_sensor(config[CONF_RED_CHANNEL]) cg.add(var.set_red_sensor(sens)) if CONF_GREEN_CHANNEL in config: - sens = yield sensor.new_sensor(config[CONF_GREEN_CHANNEL]) + sens = await sensor.new_sensor(config[CONF_GREEN_CHANNEL]) cg.add(var.set_green_sensor(sens)) if CONF_BLUE_CHANNEL in config: - sens = yield sensor.new_sensor(config[CONF_BLUE_CHANNEL]) + sens = await sensor.new_sensor(config[CONF_BLUE_CHANNEL]) cg.add(var.set_blue_sensor(sens)) if CONF_CLEAR_CHANNEL in config: - sens = yield sensor.new_sensor(config[CONF_CLEAR_CHANNEL]) + sens = await sensor.new_sensor(config[CONF_CLEAR_CHANNEL]) cg.add(var.set_clear_sensor(sens)) if CONF_ILLUMINANCE in config: - sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + sens = await sensor.new_sensor(config[CONF_ILLUMINANCE]) cg.add(var.set_illuminance_sensor(sens)) if CONF_COLOR_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_COLOR_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_COLOR_TEMPERATURE]) cg.add(var.set_color_temperature_sensor(sens)) diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 49aff5b162..f7ffe2a97d 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -1,10 +1,11 @@ #include "tcs34725.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace tcs34725 { -static const char *TAG = "tcs34725"; +static const char *const TAG = "tcs34725"; static const uint8_t TCS34725_ADDRESS = 0x29; static const uint8_t TCS34725_COMMAND_BIT = 0x80; @@ -25,10 +26,8 @@ void TCS34725Component::setup() { return; } - uint8_t integration_reg = this->integration_time_; - uint8_t gain_reg = this->gain_; - if (!this->write_byte(TCS34725_REGISTER_ATIME, integration_reg) || - !this->write_byte(TCS34725_REGISTER_CONTROL, gain_reg)) { + if (!this->write_byte(TCS34725_REGISTER_ATIME, this->integration_reg_) || + !this->write_byte(TCS34725_REGISTER_CONTROL, this->gain_reg_)) { this->mark_failed(); return; } @@ -60,6 +59,114 @@ void TCS34725Component::dump_config() { LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_); } float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; } + +/*! + * @brief Converts the raw R/G/B values to color temperature in degrees + * Kelvin using the algorithm described in DN40 from Taos (now AMS). + * @param r + * Red value + * @param g + * Green value + * @param b + * Blue value + * @param c + * Clear channel value + * @return Color temperature in degrees Kelvin + */ +void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c) { + float r2, g2, b2; /* RGB values minus IR component */ + float sat; /* Digital saturation level */ + float ir; /* Inferred IR content */ + + this->illuminance_ = 0; // Assign 0 value before calculation + this->color_temperature_ = 0; + + const float ga = this->glass_attenuation_; // Glass Attenuation Factor + static const float DF = 310.f; // Device Factor + static const float R_COEF = 0.136f; // + static const float G_COEF = 1.f; // used in lux computation + static const float B_COEF = -0.444f; // + static const float CT_COEF = 3810.f; // Color Temperature Coefficient + static const float CT_OFFSET = 1391.f; // Color Temperatuer Offset + + if (c == 0) { + return; + } + + /* Analog/Digital saturation: + * + * (a) As light becomes brighter, the clear channel will tend to + * saturate first since R+G+B is approximately equal to C. + * (b) The TCS34725 accumulates 1024 counts per 2.4ms of integration + * time, up to a maximum values of 65535. This means analog + * saturation can occur up to an integration time of 153.6ms + * (64*2.4ms=153.6ms). + * (c) If the integration time is > 153.6ms, digital saturation will + * occur before analog saturation. Digital saturation occurs when + * the count reaches 65535. + */ + if ((256 - this->integration_reg_) > 63) { + /* Track digital saturation */ + sat = 65535.f; + } else { + /* Track analog saturation */ + sat = 1024.f * (256.f - this->integration_reg_); + } + + /* Ripple rejection: + * + * (a) An integration time of 50ms or multiples of 50ms are required to + * reject both 50Hz and 60Hz ripple. + * (b) If an integration time faster than 50ms is required, you may need + * to average a number of samples over a 50ms period to reject ripple + * from fluorescent and incandescent light sources. + * + * Ripple saturation notes: + * + * (a) If there is ripple in the received signal, the value read from C + * will be less than the max, but still have some effects of being + * saturated. This means that you can be below the 'sat' value, but + * still be saturating. At integration times >150ms this can be + * ignored, but <= 150ms you should calculate the 75% saturation + * level to avoid this problem. + */ + if (this->integration_time_ < 150) { + /* Adjust sat to 75% to avoid analog saturation if atime < 153.6ms */ + sat -= sat / 4.f; + } + + /* Check for saturation and mark the sample as invalid if true */ + if (c >= sat) { + return; + } + + /* AMS RGB sensors have no IR channel, so the IR content must be */ + /* calculated indirectly. */ + ir = ((r + g + b) > c) ? (r + g + b - c) / 2 : 0; + + /* Remove the IR component from the raw RGB values */ + r2 = r - ir; + g2 = g - ir; + b2 = b - ir; + + if (r2 == 0) { + return; + } + + // Lux Calculation (DN40 3.2) + + float g1 = R_COEF * r2 + G_COEF * g2 + B_COEF * b2; + float cpl = (this->integration_time_ * this->gain_) / (ga * DF); + this->illuminance_ = g1 / cpl; + + // Color Temperature Calculation (DN40) + /* A simple method of measuring color temp is to use the ratio of blue */ + /* to red light, taking IR cancellation into account. */ + this->color_temperature_ = (CT_COEF * b2) / /** Color temp coefficient. */ + r2 + + CT_OFFSET; /** Color temp offset. */ +} + void TCS34725Component::update() { uint16_t raw_c; uint16_t raw_r; @@ -73,6 +180,12 @@ void TCS34725Component::update() { return; } + // May need to fix endianness as the data read over I2C is big-endian, but most ESP platforms are little-endian + raw_c = i2c::i2ctohs(raw_c); + raw_r = i2c::i2ctohs(raw_r); + raw_g = i2c::i2ctohs(raw_g); + raw_b = i2c::i2ctohs(raw_b); + const float channel_c = raw_c / 655.35f; const float channel_r = raw_r / 655.35f; const float channel_g = raw_g / 655.35f; @@ -86,38 +199,54 @@ void TCS34725Component::update() { if (this->blue_sensor_ != nullptr) this->blue_sensor_->publish_state(channel_b); - // Formulae taken from Adafruit TCS35725 library - float illuminance = (-0.32466f * channel_r) + (1.57837f * channel_g) + (-0.73191f * channel_b); + if (this->illuminance_sensor_ || this->color_temperature_sensor_) { + calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c); + } + if (this->illuminance_sensor_ != nullptr) - this->illuminance_sensor_->publish_state(illuminance); + this->illuminance_sensor_->publish_state(this->illuminance_); - // Color temperature - // 1. Convert RGB to XYZ color space - const float x = (-0.14282f * raw_r) + (1.54924f * raw_g) + (-0.95641f * raw_b); - const float y = (-0.32466f * raw_r) + (1.57837f * raw_g) + (-0.73191f * raw_b); - const float z = (-0.68202f * raw_r) + (0.77073f * raw_g) + (0.56332f * raw_b); - - // 2. Calculate chromacity coordinates - const float xc = (x) / (x + y + z); - const float yc = (y) / (x + y + z); - - // 3. Use McCamy's formula to determine the color temperature - const float n = (xc - 0.3320f) / (0.1858f - yc); - - // 4. final color temperature in Kelvin. - const float color_temperature = (449.0f * powf(n, 3.0f)) + (3525.0f * powf(n, 2.0f)) + (6823.3f * n) + 5520.33f; if (this->color_temperature_sensor_ != nullptr) - this->color_temperature_sensor_->publish_state(color_temperature); + this->color_temperature_sensor_->publish_state(this->color_temperature_); ESP_LOGD(TAG, "Got R=%.1f%%,G=%.1f%%,B=%.1f%%,C=%.1f%% Illuminance=%.1flx Color Temperature=%.1fK", channel_r, - channel_g, channel_b, channel_c, illuminance, color_temperature); + channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_); this->status_clear_warning(); } void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) { - this->integration_time_ = integration_time; + this->integration_reg_ = integration_time; + this->integration_time_ = (256.f - integration_time) * 2.4f; +} +void TCS34725Component::set_gain(TCS34725Gain gain) { + this->gain_reg_ = gain; + switch (gain) { + case TCS34725Gain::TCS34725_GAIN_1X: + this->gain_ = 1.f; + break; + case TCS34725Gain::TCS34725_GAIN_4X: + this->gain_ = 4.f; + break; + case TCS34725Gain::TCS34725_GAIN_16X: + this->gain_ = 16.f; + break; + case TCS34725Gain::TCS34725_GAIN_60X: + this->gain_ = 60.f; + break; + default: + this->gain_ = 1.f; + break; + } +} + +void TCS34725Component::set_glass_attenuation_factor(float ga) { + // The Glass Attenuation (FA) factor used to compensate for lower light + // levels at the device due to the possible presence of glass. The GA is + // the inverse of the glass transmissivity (T), so GA = 1/T. A transmissivity + // of 50% gives GA = 1 / 0.50 = 2. If no glass is present, use GA = 1. + // See Application Note: DN40-Rev 1.0 + this->glass_attenuation_ = ga; } -void TCS34725Component::set_gain(TCS34725Gain gain) { this->gain_ = gain; } } // namespace tcs34725 } // namespace esphome diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index b914db0eb0..47ed2959c6 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -12,8 +12,20 @@ enum TCS34725IntegrationTime { TCS34725_INTEGRATION_TIME_24MS = 0xF6, TCS34725_INTEGRATION_TIME_50MS = 0xEB, TCS34725_INTEGRATION_TIME_101MS = 0xD5, + TCS34725_INTEGRATION_TIME_120MS = 0xCE, TCS34725_INTEGRATION_TIME_154MS = 0xC0, - TCS34725_INTEGRATION_TIME_700MS = 0x00, + TCS34725_INTEGRATION_TIME_180MS = 0xB5, + TCS34725_INTEGRATION_TIME_199MS = 0xAD, + TCS34725_INTEGRATION_TIME_240MS = 0x9C, + TCS34725_INTEGRATION_TIME_300MS = 0x83, + TCS34725_INTEGRATION_TIME_360MS = 0x6A, + TCS34725_INTEGRATION_TIME_401MS = 0x59, + TCS34725_INTEGRATION_TIME_420MS = 0x51, + TCS34725_INTEGRATION_TIME_480MS = 0x38, + TCS34725_INTEGRATION_TIME_499MS = 0x30, + TCS34725_INTEGRATION_TIME_540MS = 0x1F, + TCS34725_INTEGRATION_TIME_600MS = 0x06, + TCS34725_INTEGRATION_TIME_614MS = 0x00, }; enum TCS34725Gain { @@ -27,6 +39,7 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { public: void set_integration_time(TCS34725IntegrationTime integration_time); void set_gain(TCS34725Gain gain); + void set_glass_attenuation_factor(float ga); void set_clear_sensor(sensor::Sensor *clear_sensor) { clear_sensor_ = clear_sensor; } void set_red_sensor(sensor::Sensor *red_sensor) { red_sensor_ = red_sensor; } @@ -49,8 +62,16 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *blue_sensor_{nullptr}; sensor::Sensor *illuminance_sensor_{nullptr}; sensor::Sensor *color_temperature_sensor_{nullptr}; - TCS34725IntegrationTime integration_time_{TCS34725_INTEGRATION_TIME_2_4MS}; - TCS34725Gain gain_{TCS34725_GAIN_1X}; + float integration_time_{2.4}; + float gain_{1.0}; + float glass_attenuation_{1.0}; + float illuminance_; + float color_temperature_; + + private: + void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c); + uint8_t integration_reg_{TCS34725_INTEGRATION_TIME_2_4MS}; + uint8_t gain_reg_{TCS34725_GAIN_1X}; }; } // namespace tcs34725 diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py new file mode 100644 index 0000000000..9a5712e10f --- /dev/null +++ b/esphome/components/teleinfo/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@0hax"] + +teleinfo_ns = cg.esphome_ns.namespace("teleinfo") +TeleInfo = teleinfo_ns.class_("TeleInfo", cg.PollingComponent, uart.UARTDevice) +CONF_TELEINFO_ID = "teleinfo_id" + +CONF_HISTORICAL_MODE = "historical_mode" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TeleInfo), + cv.Optional(CONF_HISTORICAL_MODE, default=False): cv.boolean, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) + 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 new file mode 100644 index 0000000000..e7cc2fcb1b --- /dev/null +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, ICON_FLASH, UNIT_WATT_HOURS + +from .. import teleinfo_ns, TeleInfo, CONF_TELEINFO_ID + +CONF_TAG_NAME = "tag_name" + +TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component) + +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), + cv.Required(CONF_TAG_NAME): cv.string, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) + 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/sensor/teleinfo_sensor.cpp b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp new file mode 100644 index 0000000000..ad9c6dae00 --- /dev/null +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp @@ -0,0 +1,14 @@ +#include "esphome/core/log.h" +#include "teleinfo_sensor.h" +namespace esphome { +namespace teleinfo { + +static const char *const TAG = "teleinfo_sensor"; +TeleInfoSensor::TeleInfoSensor(const char *tag) { this->tag = std::string(tag); } +void TeleInfoSensor::publish_val(const std::string &val) { + auto newval = parse_number(val).value_or(0.0f); + publish_state(newval); +} +void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", "Teleinfo Sensor", this); } +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.h b/esphome/components/teleinfo/sensor/teleinfo_sensor.h new file mode 100644 index 0000000000..56781166ab --- /dev/null +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.h @@ -0,0 +1,16 @@ +#pragma once +#include "esphome/components/teleinfo/teleinfo.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace teleinfo { + +class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component { + public: + TeleInfoSensor(const char *tag); + void publish_val(const std::string &val) override; + void dump_config() override; +}; + +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp new file mode 100644 index 0000000000..d9f80134f4 --- /dev/null +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -0,0 +1,209 @@ +#include "teleinfo.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace teleinfo { + +static const char *const TAG = "teleinfo"; + +/* Helpers */ +static int get_field(char *dest, char *buf_start, char *buf_end, int sep, int max_len) { + char *field_end; + int len; + + field_end = static_cast(memchr(buf_start, sep, buf_end - buf_start)); + if (!field_end) + return 0; + len = field_end - buf_start; + if (len >= max_len) + return len; + strncpy(dest, buf_start, len); + dest[len] = '\0'; + + return len; +} +/* TeleInfo methods */ +bool TeleInfo::check_crc_(const char *grp, const char *grp_end) { + int grp_len = grp_end - grp; + uint8_t raw_crc = grp[grp_len - 1]; + uint8_t crc_tmp = 0; + int i; + + for (i = 0; i < grp_len - checksum_area_end_; i++) + crc_tmp += grp[i]; + + crc_tmp &= 0x3F; + crc_tmp += 0x20; + if (raw_crc != crc_tmp) { + ESP_LOGE(TAG, "bad crc: got %d except %d", raw_crc, crc_tmp); + return false; + } + + return true; +} +bool TeleInfo::read_chars_until_(bool drop, uint8_t c) { + uint8_t received; + int j = 0; + + while (available() > 0 && j < 128) { + j++; + received = read(); + if (received == c) + return true; + if (drop) + continue; + /* + * Internal buffer is full, switch to OFF mode. + * Data will be retrieved on next update. + */ + if (buf_index_ >= (MAX_BUF_SIZE - 1)) { + ESP_LOGW(TAG, "Internal buffer full"); + state_ = OFF; + return false; + } + buf_[buf_index_++] = received; + } + + return false; +} +void TeleInfo::setup() { state_ = OFF; } +void TeleInfo::update() { + if (state_ == OFF) { + buf_index_ = 0; + state_ = ON; + } +} +void TeleInfo::loop() { + switch (state_) { + case OFF: + break; + case ON: + /* Dequeue chars until start frame (0x2) */ + if (read_chars_until_(true, 0x2)) + state_ = START_FRAME_RECEIVED; + break; + case START_FRAME_RECEIVED: + /* Dequeue chars until end frame (0x3) */ + if (read_chars_until_(false, 0x3)) + state_ = END_FRAME_RECEIVED; + break; + case END_FRAME_RECEIVED: + char *buf_finger; + char *grp_end; + char *buf_end; + int field_len; + + buf_finger = buf_; + buf_end = buf_ + buf_index_; + + /* Each frame is composed of multiple groups starting by 0xa(Line Feed) and ending by + * 0xd ('\r'). + * + * Historical mode: each group contains tag, data and a CRC separated by 0x20 (Space) + * 0xa | Tag | 0x20 | Data | 0x20 | CRC | 0xd + * ^^^^^^^^^^^^^^^^^^^^ + * Checksum is computed on the above in historical mode. + * + * Standard mode: each group contains tag, data and a CRC separated by 0x9 (\t) + * 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_)) { // NOLINT(clang-diagnostic-sign-compare) + /* + * 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; + + /* Group len */ + grp_end = static_cast(memchr(buf_finger, 0xd, buf_end - buf_finger)); + if (!grp_end) { + ESP_LOGE(TAG, "No group found"); + break; + } + + if (!check_crc_(buf_finger, grp_end)) + continue; + + /* Get tag */ + 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."); + continue; + } + + /* Advance buf_finger to after the tag and the separator. */ + buf_finger += field_len + 1; + + /* + * If there is two separators and the tag is not equal to "DATE" or + * historical mode is not in use (separator_ != 0x20), it means there is a + * timestamp to read first. + */ + if (std::count(buf_finger, grp_end, separator_) == 2 && strcmp(tag_, "DATE") != 0 && separator_ != 0x20) { + 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 for tag %s", timestamp_); + continue; + } + + /* 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 for tag %s", tag_); + continue; + } + + /* Advance buf_finger to end of group */ + buf_finger += field_len + 1 + 1 + 1; + + publish_value_(std::string(tag_), std::string(val_)); + } + state_ = OFF; + break; + } +} +void TeleInfo::publish_value_(const std::string &tag, const std::string &val) { + for (auto element : teleinfo_listeners_) { + if (tag != element->tag) + continue; + element->publish_val(val); + } +} +void TeleInfo::dump_config() { + ESP_LOGCONFIG(TAG, "TeleInfo:"); + this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7); +} +TeleInfo::TeleInfo(bool historical_mode) { + if (historical_mode) { + /* + * Historical mode doesn't contain last separator between checksum and data. + */ + checksum_area_end_ = 2; + separator_ = 0x20; + baud_rate_ = 1200; + } else { + checksum_area_end_ = 1; + separator_ = 0x9; + baud_rate_ = 9600; + } +} +void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); } + +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h new file mode 100644 index 0000000000..2be34cfb78 --- /dev/null +++ b/esphome/components/teleinfo/teleinfo.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace teleinfo { +/* + * 198 bytes should be enough to contain a full session in historical mode with + * three phases. But go with 1024 just to be sure. + */ +static const uint8_t MAX_TAG_SIZE = 64; +static const uint16_t MAX_VAL_SIZE = 256; +static const uint16_t MAX_BUF_SIZE = 2048; +static const uint16_t MAX_TIMESTAMP_SIZE = 14; + +class TeleInfoListener { + public: + std::string tag; + virtual void publish_val(const std::string &val){}; +}; +class TeleInfo : public PollingComponent, public uart::UARTDevice { + public: + TeleInfo(bool historical_mode); + void register_teleinfo_listener(TeleInfoListener *listener); + void loop() override; + void setup() override; + void update() override; + void dump_config() override; + std::vector teleinfo_listeners_{}; + + protected: + uint32_t baud_rate_; + int checksum_area_end_; + int separator_; + char buf_[MAX_BUF_SIZE]; + uint32_t buf_index_{0}; + char tag_[MAX_TAG_SIZE]; + char val_[MAX_VAL_SIZE]; + char timestamp_[MAX_TIMESTAMP_SIZE]; + enum State { + OFF, + ON, + START_FRAME_RECEIVED, + END_FRAME_RECEIVED, + } state_{OFF}; + bool read_chars_until_(bool drop, uint8_t c); + bool check_crc_(const char *grp, const char *grp_end); + void publish_value_(const std::string &tag, const std::string &val); +}; +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py new file mode 100644 index 0000000000..3bd73ff272 --- /dev/null +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -0,0 +1,28 @@ +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 teleinfo_ns, TeleInfo, CONF_TELEINFO_ID + +CONF_TAG_NAME = "tag_name" + +TeleInfoTextSensor = teleinfo_ns.class_( + "TeleInfoTextSensor", text_sensor.TextSensor, cg.Component +) + +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TeleInfoTextSensor), + cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), + cv.Required(CONF_TAG_NAME): cv.string, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) + 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/teleinfo/text_sensor/teleinfo_text_sensor.cpp b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp new file mode 100644 index 0000000000..87cf0dea17 --- /dev/null +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp @@ -0,0 +1,11 @@ +#include "esphome/core/log.h" +#include "teleinfo_text_sensor.h" +namespace esphome { +namespace teleinfo { + +static const char *const TAG = "teleinfo_text_sensor"; +TeleInfoTextSensor::TeleInfoTextSensor(const char *tag) { this->tag = std::string(tag); } +void TeleInfoTextSensor::publish_val(const std::string &val) { publish_state(val); } +void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Teleinfo Text Sensor", this); } +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h new file mode 100644 index 0000000000..5a7dc9d1a7 --- /dev/null +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h @@ -0,0 +1,13 @@ +#pragma once +#include "esphome/components/teleinfo/teleinfo.h" +#include "esphome/components/text_sensor/text_sensor.h" +namespace esphome { +namespace teleinfo { +class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component { + public: + TeleInfoTextSensor(const char *tag); + void publish_val(const std::string &val) override; + void dump_config() override; +}; +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/template/__init__.py b/esphome/components/template/__init__.py index 44c59260d3..6253af9090 100644 --- a/esphome/components/template/__init__.py +++ b/esphome/components/template/__init__.py @@ -1,3 +1,3 @@ import esphome.codegen as cg -template_ns = cg.esphome_ns.namespace('template_') +template_ns = cg.esphome_ns.namespace("template_") diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 14f9f23ec2..8f551e3215 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -5,35 +5,43 @@ from esphome.components import binary_sensor from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE from .. import template_ns -TemplateBinarySensor = template_ns.class_('TemplateBinarySensor', binary_sensor.BinarySensor, - cg.Component) +TemplateBinarySensor = template_ns.class_( + "TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component +) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TemplateBinarySensor), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateBinarySensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) if CONF_LAMBDA in config: - template_ = yield cg.process_lambda(config[CONF_LAMBDA], [], - return_type=cg.optional.template(bool)) + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(bool) + ) cg.add(var.set_template(template_)) -@automation.register_action('binary_sensor.template.publish', - binary_sensor.BinarySensorPublishAction, - cv.Schema({ - cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), - cv.Required(CONF_STATE): cv.templatable(cv.boolean), - })) -def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "binary_sensor.template.publish", + binary_sensor.BinarySensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } + ), +) +async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) - yield var + return var diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index a4199faa9a..66ff4be4c4 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace template_ { -static const char *TAG = "template.binary_sensor"; +static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::loop() { if (!this->f_.has_value()) diff --git a/esphome/components/template/button/__init__.py b/esphome/components/template/button/__init__.py new file mode 100644 index 0000000000..aa192d118e --- /dev/null +++ b/esphome/components/template/button/__init__.py @@ -0,0 +1,13 @@ +import esphome.config_validation as cv +from esphome.components import button + + +CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(button.Button), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + await button.new_button(config) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 607d9d0064..a628da70d2 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -2,92 +2,130 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import cover -from esphome.const import CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, CONF_ID, \ - CONF_LAMBDA, CONF_OPEN_ACTION, CONF_OPTIMISTIC, CONF_POSITION, CONF_RESTORE_MODE, \ - CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA, \ - CONF_POSITION_ACTION +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_CLOSE_ACTION, + CONF_CURRENT_OPERATION, + CONF_ID, + CONF_LAMBDA, + CONF_OPEN_ACTION, + CONF_OPTIMISTIC, + CONF_POSITION, + CONF_RESTORE_MODE, + CONF_STATE, + CONF_STOP_ACTION, + CONF_TILT, + CONF_TILT_ACTION, + CONF_TILT_LAMBDA, + CONF_POSITION_ACTION, +) from .. import template_ns -TemplateCover = template_ns.class_('TemplateCover', cover.Cover, cg.Component) +TemplateCover = template_ns.class_("TemplateCover", cover.Cover, cg.Component) -TemplateCoverRestoreMode = template_ns.enum('TemplateCoverRestoreMode') +TemplateCoverRestoreMode = template_ns.enum("TemplateCoverRestoreMode") RESTORE_MODES = { - 'NO_RESTORE': TemplateCoverRestoreMode.COVER_NO_RESTORE, - 'RESTORE': TemplateCoverRestoreMode.COVER_RESTORE, - 'RESTORE_AND_CALL': TemplateCoverRestoreMode.COVER_RESTORE_AND_CALL, + "NO_RESTORE": TemplateCoverRestoreMode.COVER_NO_RESTORE, + "RESTORE": TemplateCoverRestoreMode.COVER_RESTORE, + "RESTORE_AND_CALL": TemplateCoverRestoreMode.COVER_RESTORE_AND_CALL, } -CONF_HAS_POSITION = 'has_position' +CONF_HAS_POSITION = "has_position" -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TemplateCover), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, - cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, - cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_MODE, default='RESTORE'): cv.enum(RESTORE_MODES, upper=True), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateCover), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, + cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield cover.register_cover(var, config) + await cg.register_component(var, config) + await cover.register_cover(var, config) if CONF_LAMBDA in config: - template_ = yield cg.process_lambda(config[CONF_LAMBDA], [], - return_type=cg.optional.template(float)) + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) cg.add(var.set_state_lambda(template_)) if CONF_OPEN_ACTION in config: - yield automation.build_automation(var.get_open_trigger(), [], config[CONF_OPEN_ACTION]) + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) if CONF_CLOSE_ACTION in config: - yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) + await automation.build_automation( + var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] + ) if CONF_STOP_ACTION in config: - yield automation.build_automation(var.get_stop_trigger(), [], config[CONF_STOP_ACTION]) + await automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) if CONF_TILT_ACTION in config: - yield automation.build_automation(var.get_tilt_trigger(), [(float, 'tilt')], - config[CONF_TILT_ACTION]) + await automation.build_automation( + var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] + ) cg.add(var.set_has_tilt(True)) if CONF_TILT_LAMBDA in config: - tilt_template_ = yield cg.process_lambda(config[CONF_TILT_LAMBDA], [], - return_type=cg.optional.template(float)) + tilt_template_ = await cg.process_lambda( + config[CONF_TILT_LAMBDA], [], return_type=cg.optional.template(float) + ) cg.add(var.set_tilt_lambda(tilt_template_)) if CONF_POSITION_ACTION in config: - yield automation.build_automation(var.get_position_trigger(), [(float, 'pos')], - config[CONF_POSITION_ACTION]) + await automation.build_automation( + var.get_position_trigger(), [(float, "pos")], config[CONF_POSITION_ACTION] + ) cg.add(var.set_has_position(True)) else: cg.add(var.set_has_position(config[CONF_HAS_POSITION])) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) -@automation.register_action('cover.template.publish', cover.CoverPublishAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(cover.Cover), - cv.Exclusive(CONF_STATE, 'pos'): cv.templatable(cover.validate_cover_state), - cv.Exclusive(CONF_POSITION, 'pos'): cv.templatable(cv.zero_to_one_float), - cv.Optional(CONF_CURRENT_OPERATION): cv.templatable(cover.validate_cover_operation), - cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), -})) -def cover_template_publish_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "cover.template.publish", + cover.CoverPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(cover.Cover), + cv.Exclusive(CONF_STATE, "pos"): cv.templatable(cover.validate_cover_state), + cv.Exclusive(CONF_POSITION, "pos"): cv.templatable(cv.zero_to_one_float), + cv.Optional(CONF_CURRENT_OPERATION): cv.templatable( + cover.validate_cover_operation + ), + cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), + } + ), +) +async def cover_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_STATE in config: - template_ = yield cg.templatable(config[CONF_STATE], args, float) + template_ = await cg.templatable(config[CONF_STATE], args, float) cg.add(var.set_position(template_)) if CONF_POSITION in config: - template_ = yield cg.templatable(config[CONF_POSITION], args, float) + template_ = await cg.templatable(config[CONF_POSITION], args, float) cg.add(var.set_position(template_)) if CONF_TILT in config: - template_ = yield cg.templatable(config[CONF_TILT], args, float) + template_ = await cg.templatable(config[CONF_TILT], args, float) cg.add(var.set_tilt(template_)) if CONF_CURRENT_OPERATION in config: - template_ = yield cg.templatable(config[CONF_CURRENT_OPERATION], args, cover.CoverOperation) + template_ = await cg.templatable( + config[CONF_CURRENT_OPERATION], args, cover.CoverOperation + ) cg.add(var.set_current_operation(template_)) - yield var + return var diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 887f282007..47c651e643 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -6,7 +6,7 @@ namespace template_ { using namespace esphome::cover; -static const char *TAG = "template.cover"; +static const char *const TAG = "template.cover"; TemplateCover::TemplateCover() : open_trigger_(new Trigger<>()), @@ -120,7 +120,7 @@ void TemplateCover::set_has_position(bool has_position) { this->has_position_ = void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py new file mode 100644 index 0000000000..3dec7066d3 --- /dev/null +++ b/esphome/components/template/number/__init__.py @@ -0,0 +1,93 @@ +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") + elif CONF_INITIAL_VALUE not in config: + config[CONF_INITIAL_VALUE] = config[CONF_MIN_VALUE] + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the 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])) + 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/output/__init__.py b/esphome/components/template/output/__init__.py index cc85a9da68..b42a4be166 100644 --- a/esphome/components/template/output/__init__.py +++ b/esphome/components/template/output/__init__.py @@ -5,30 +5,43 @@ from esphome.components import output from esphome.const import CONF_ID, CONF_TYPE, CONF_BINARY from .. import template_ns -TemplateBinaryOutput = template_ns.class_('TemplateBinaryOutput', output.BinaryOutput) -TemplateFloatOutput = template_ns.class_('TemplateFloatOutput', output.FloatOutput) +TemplateBinaryOutput = template_ns.class_("TemplateBinaryOutput", output.BinaryOutput) +TemplateFloatOutput = template_ns.class_("TemplateFloatOutput", output.FloatOutput) -CONF_FLOAT = 'float' -CONF_WRITE_ACTION = 'write_action' +CONF_FLOAT = "float" +CONF_WRITE_ACTION = "write_action" -CONFIG_SCHEMA = cv.typed_schema({ - CONF_BINARY: output.BINARY_OUTPUT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TemplateBinaryOutput), - cv.Required(CONF_WRITE_ACTION): automation.validate_automation(single=True), - }), - CONF_FLOAT: output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TemplateFloatOutput), - cv.Required(CONF_WRITE_ACTION): automation.validate_automation(single=True), - }), -}, lower=True) +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_BINARY: output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateBinaryOutput), + cv.Required(CONF_WRITE_ACTION): automation.validate_automation( + single=True + ), + } + ), + CONF_FLOAT: output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateFloatOutput), + cv.Required(CONF_WRITE_ACTION): automation.validate_automation( + single=True + ), + } + ), + }, + lower=True, +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) if config[CONF_TYPE] == CONF_BINARY: - yield automation.build_automation(var.get_trigger(), [(bool, 'state')], - config[CONF_WRITE_ACTION]) + await automation.build_automation( + var.get_trigger(), [(bool, "state")], config[CONF_WRITE_ACTION] + ) else: - yield automation.build_automation(var.get_trigger(), [(float, 'state')], - config[CONF_WRITE_ACTION]) - yield output.register_output(var, config) + await automation.build_automation( + var.get_trigger(), [(float, "state")], config[CONF_WRITE_ACTION] + ) + await output.register_output(var, config) 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 3fc71cf9de..75fb505d91 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -2,36 +2,58 @@ 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_LAMBDA, CONF_STATE, UNIT_EMPTY, ICON_EMPTY +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_STATE, + STATE_CLASS_NONE, +) from .. import template_ns -TemplateSensor = template_ns.class_('TemplateSensor', sensor.Sensor, cg.PollingComponent) +TemplateSensor = template_ns.class_( + "TemplateSensor", sensor.Sensor, cg.PollingComponent +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1).extend({ - cv.GenerateID(): cv.declare_id(TemplateSensor), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TemplateSensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) if CONF_LAMBDA in config: - template_ = yield cg.process_lambda(config[CONF_LAMBDA], [], - return_type=cg.optional.template(float)) + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) cg.add(var.set_template(template_)) -@automation.register_action('sensor.template.publish', sensor.SensorPublishAction, - cv.Schema({ - cv.Required(CONF_ID): cv.use_id(sensor.Sensor), - cv.Required(CONF_STATE): cv.templatable(cv.float_), - })) -def sensor_template_publish_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "sensor.template.publish", + sensor.SensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_STATE): cv.templatable(cv.float_), + } + ), +) +async def sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_STATE], args, float) + template_ = await cg.templatable(config[CONF_STATE], args, float) cg.add(var.set_state(template_)) - yield var + return var diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index e2d36f13a0..b28eb3fed2 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -1,18 +1,20 @@ #include "template_sensor.h" #include "esphome/core/log.h" +#include namespace esphome { namespace template_ { -static const char *TAG = "template.sensor"; +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 783f5a1922..6095a7c561 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -2,50 +2,90 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import switch -from esphome.const import CONF_ASSUMED_STATE, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC, \ - CONF_RESTORE_STATE, CONF_STATE, CONF_TURN_OFF_ACTION, CONF_TURN_ON_ACTION +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_ID, + CONF_LAMBDA, + CONF_OPTIMISTIC, + CONF_RESTORE_STATE, + CONF_STATE, + CONF_TURN_OFF_ACTION, + CONF_TURN_ON_ACTION, +) 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) +TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component) -def to_code(config): +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): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield switch.register_switch(var, config) + await cg.register_component(var, config) + await switch.register_switch(var, config) if CONF_LAMBDA in config: - template_ = yield cg.process_lambda(config[CONF_LAMBDA], [], - return_type=cg.optional.template(bool)) + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(bool) + ) cg.add(var.set_state_lambda(template_)) if CONF_TURN_OFF_ACTION in config: - yield automation.build_automation(var.get_turn_off_trigger(), [], - config[CONF_TURN_OFF_ACTION]) + await automation.build_automation( + var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] + ) if CONF_TURN_ON_ACTION in config: - yield automation.build_automation(var.get_turn_on_trigger(), [], - config[CONF_TURN_ON_ACTION]) + await automation.build_automation( + var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] + ) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_state(config[CONF_RESTORE_STATE])) -@automation.register_action('switch.template.publish', switch.SwitchPublishAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(switch.Switch), - cv.Required(CONF_STATE): cv.templatable(cv.boolean), -})) -def switch_template_publish_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "switch.template.publish", + switch.SwitchPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(switch.Switch), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } + ), +) +async def switch_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) - yield var + return var diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5868b30996..b3e545d3e9 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace template_ { -static const char *TAG = "template.switch"; +static const char *const TAG = "template.switch"; TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} @@ -19,7 +19,7 @@ void TemplateSwitch::loop() { } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); } if (state) { diff --git a/esphome/components/template/text_sensor/__init__.py b/esphome/components/template/text_sensor/__init__.py index dae99dc9bc..2e098a77c2 100644 --- a/esphome/components/template/text_sensor/__init__.py +++ b/esphome/components/template/text_sensor/__init__.py @@ -6,33 +6,43 @@ from esphome.components.text_sensor import TextSensorPublishAction from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE from .. import template_ns -TemplateTextSensor = template_ns.class_('TemplateTextSensor', text_sensor.TextSensor, - cg.PollingComponent) +TemplateTextSensor = template_ns.class_( + "TemplateTextSensor", text_sensor.TextSensor, cg.PollingComponent +) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TemplateTextSensor), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateTextSensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } +).extend(cv.polling_component_schema("60s")) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) if CONF_LAMBDA in config: - template_ = yield cg.process_lambda(config[CONF_LAMBDA], [], - return_type=cg.optional.template(cg.std_string)) + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) + ) cg.add(var.set_template(template_)) -@automation.register_action('text_sensor.template.publish', TextSensorPublishAction, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(text_sensor.TextSensor), - cv.Required(CONF_STATE): cv.templatable(cv.string_strict), -})) -def text_sensor_template_publish_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_action( + "text_sensor.template.publish", + TextSensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(text_sensor.TextSensor), + cv.Required(CONF_STATE): cv.templatable(cv.string_strict), + } + ), +) +async def text_sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_STATE], args, cg.std_string) + template_ = await cg.templatable(config[CONF_STATE], args, cg.std_string) cg.add(var.set_state(template_)) - yield var + return var diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index e06c70d95e..83bebb5bcf 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -4,15 +4,16 @@ namespace esphome { namespace template_ { -static const char *TAG = "template.text_sensor"; +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 f138f38d2f..e0fc6af19c 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -2,71 +2,187 @@ 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_ICON, CONF_ID, CONF_INTERNAL, CONF_ON_VALUE, \ - CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME, CONF_STATE -from esphome.core import CORE, coroutine, coroutine_with_priority +from esphome.const import ( + CONF_FILTERS, + CONF_ID, + CONF_ON_VALUE, + CONF_ON_RAW_VALUE, + CONF_TRIGGER_ID, + CONF_MQTT_ID, + 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) -TextSensorPtr = TextSensor.operator('ptr') +text_sensor_ns = cg.esphome_ns.namespace("text_sensor") +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 +) +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) +MapFilter = text_sensor_ns.class_("MapFilter", 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_mapping(value): + if not isinstance(value, dict): + value = cv.string(value) + if "->" not in value: + raise cv.Invalid("Mapping must contain '->'") + a, b = value.split("->", 1) + value = {CONF_FROM: a.strip(), CONF_TO: b.strip()} + + return cv.Schema( + {cv.Required(CONF_FROM): cv.string, cv.Required(CONF_TO): cv.string} + )(value) + + +@FILTER_REGISTRY.register( + "substitute", SubstituteFilter, cv.ensure_list(validate_mapping) +) +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) + + +@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) +async def map_filter_to_code(config, filter_id): + map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string) + return cg.new_Pvariable( + filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config]) + ) -TextSensorStateTrigger = text_sensor_ns.class_('TextSensorStateTrigger', - automation.Trigger.template(cg.std_string)) -TextSensorPublishAction = text_sensor_ns.class_('TextSensorPublishAction', automation.Action) -TextSensorStateCondition = text_sensor_ns.class_('TextSensorStateCondition', automation.Condition) icon = cv.icon -TEXT_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ - cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTTextSensor), - cv.Optional(CONF_ICON): icon, - cv.Optional(CONF_ON_VALUE): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TextSensorStateTrigger), - }), -}) + +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_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 + ), + } + ), + } +) -@coroutine -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])) +async def build_filters(config): + return await cg.build_registry_list(FILTER_REGISTRY, config) + + +async def setup_text_sensor_core_(var, config): + 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) - yield automation.build_automation(trigger, [(cg.std_string, 'x')], conf) + 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) - yield mqtt.register_mqtt_component(mqtt_, config) + await mqtt.register_mqtt_component(mqtt_, config) -@coroutine -def register_text_sensor(var, config): +async def register_text_sensor(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_text_sensor(var)) - yield setup_text_sensor_core_(var, config) + await setup_text_sensor_core_(var, config) @coroutine_with_priority(100.0) -def to_code(config): - cg.add_define('USE_TEXT_SENSOR') +async def to_code(config): + cg.add_define("USE_TEXT_SENSOR") cg.add_global(text_sensor_ns.using) -@automation.register_condition('text_sensor.state', TextSensorStateCondition, cv.Schema({ - cv.Required(CONF_ID): cv.use_id(TextSensor), - cv.Required(CONF_STATE): cv.templatable(cv.string_strict), -})) -def text_sensor_state_to_code(config, condition_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +@automation.register_condition( + "text_sensor.state", + TextSensorStateCondition, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(TextSensor), + cv.Required(CONF_STATE): cv.templatable(cv.string_strict), + } + ), +) +async def text_sensor_state_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) - templ = yield cg.templatable(config[CONF_STATE], args, cg.std_string) + templ = await cg.templatable(config[CONF_STATE], args, cg.std_string) cg.add(var.set_state(templ)) - yield var + return var diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 496efb1cc3..d7286845e0 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/components/text_sensor/text_sensor.h" @@ -10,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(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); }); } }; @@ -30,6 +39,7 @@ template class TextSensorPublishAction : public Action { public: TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp new file mode 100644 index 0000000000..c6cbcd7c1e --- /dev/null +++ b/esphome/components/text_sensor/filter.cpp @@ -0,0 +1,80 @@ +#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 (size_t 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; +} + +// Map +optional MapFilter::new_value(std::string value) { + auto item = mappings_.find(value); + return item == mappings_.end() ? value : item->second; +} + +} // 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..38f35e6172 --- /dev/null +++ b/esphome/components/text_sensor/filter.h @@ -0,0 +1,123 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include +#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_; +}; + +/// A filter that maps values from one set to another +class MapFilter : public Filter { + public: + MapFilter(std::map mappings) : mappings_(std::move(mappings)) {} + optional new_value(std::string value) override; + + protected: + std::map mappings_; +}; + +} // namespace text_sensor +} // namespace esphome diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index d4f5ef434a..5d47e7465a 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -4,27 +4,70 @@ namespace esphome { namespace text_sensor { -static const char *TAG = "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(std::string state) { +void TextSensor::publish_state(const std::string &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 = 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 85c2b644a0..4bd77131d7 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -1,50 +1,71 @@ #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()); \ - if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ - if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ + if (!(obj)->unique_id().empty()) { \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, (obj)->unique_id().c_str()); \ } \ } -class TextSensor : public Nameable { +class TextSensor : public EntityBase { public: explicit TextSensor(); explicit TextSensor(const std::string &name); - void publish_state(std::string state); + /// Getter-syntax for .state. + std::string get_state() const; + /// Getter-syntax for .raw_state + std::string get_raw_state() const; - void set_icon(const std::string &icon); + void publish_state(const std::string &state); + + /// 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/__init__.py b/esphome/components/thermostat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py new file mode 100644 index 0000000000..20565e811c --- /dev/null +++ b/esphome/components/thermostat/climate.py @@ -0,0 +1,697 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import climate, sensor +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, + CONF_DRY_MODE, + 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, + 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_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 +) +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 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, + ], + 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] + 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( + 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 + + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ThermostatClimate), + 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), + cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation( + single=True + ), + cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation( + single=True + ), + 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_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, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_at_least_one_key( + CONF_COOL_ACTION, CONF_DRY_ACTION, CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION + ), + validate_thermostat, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, 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 (config[CONF_FAN_ONLY_COOLING] and 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_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)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + 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 heat_cool_mode_available is True: + cg.add(var.set_supports_heat_cool(True)) + else: + 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] + ) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + await automation.build_automation( + var.get_fan_only_action_trigger(), [], config[CONF_FAN_ONLY_ACTION] + ) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_ACTION in config: + await automation.build_automation( + 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] + ) + if CONF_COOL_MODE in config: + await automation.build_automation( + var.get_cool_mode_trigger(), [], config[CONF_COOL_MODE] + ) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + await automation.build_automation( + var.get_dry_mode_trigger(), [], config[CONF_DRY_MODE] + ) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + await automation.build_automation( + var.get_fan_only_mode_trigger(), [], config[CONF_FAN_ONLY_MODE] + ) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + await automation.build_automation( + var.get_heat_mode_trigger(), [], config[CONF_HEAT_MODE] + ) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + await automation.build_automation( + var.get_off_mode_trigger(), [], config[CONF_OFF_MODE] + ) + if CONF_FAN_MODE_ON_ACTION in config: + await automation.build_automation( + var.get_fan_mode_on_trigger(), [], config[CONF_FAN_MODE_ON_ACTION] + ) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + await automation.build_automation( + var.get_fan_mode_off_trigger(), [], config[CONF_FAN_MODE_OFF_ACTION] + ) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + await automation.build_automation( + var.get_fan_mode_auto_trigger(), [], config[CONF_FAN_MODE_AUTO_ACTION] + ) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + await automation.build_automation( + var.get_fan_mode_low_trigger(), [], config[CONF_FAN_MODE_LOW_ACTION] + ) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + await automation.build_automation( + var.get_fan_mode_medium_trigger(), [], config[CONF_FAN_MODE_MEDIUM_ACTION] + ) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + await automation.build_automation( + var.get_fan_mode_high_trigger(), [], config[CONF_FAN_MODE_HIGH_ACTION] + ) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + await automation.build_automation( + var.get_fan_mode_middle_trigger(), [], config[CONF_FAN_MODE_MIDDLE_ACTION] + ) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + await automation.build_automation( + var.get_fan_mode_focus_trigger(), [], config[CONF_FAN_MODE_FOCUS_ACTION] + ) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + await automation.build_automation( + var.get_fan_mode_diffuse_trigger(), [], config[CONF_FAN_MODE_DIFFUSE_ACTION] + ) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + await automation.build_automation( + var.get_swing_mode_both_trigger(), [], config[CONF_SWING_BOTH_ACTION] + ) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + await automation.build_automation( + var.get_swing_mode_horizontal_trigger(), + [], + config[CONF_SWING_HORIZONTAL_ACTION], + ) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + await automation.build_automation( + var.get_swing_mode_off_trigger(), [], config[CONF_SWING_OFF_ACTION] + ) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + await automation.build_automation( + var.get_swing_mode_vertical_trigger(), + [], + 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] + + if two_points_available is True: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_away_config(away_config)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp new file mode 100644 index 0000000000..ce15c53bbe --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -0,0 +1,1215 @@ +#include "thermostat_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +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, 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(); + }); + this->current_temperature = this->sensor_->state; + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles temps for us + this->mode = this->default_mode_; + this->change_away_(false); + } + // 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::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, 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 (this->supports_two_points_) { + 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 (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); + 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_action(true); + return traits; +} + +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; + } + + return target_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 + return; + + 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 -- 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; + } + + 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: + 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: + 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: + 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: + 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: + 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) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; + } + + 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_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; + + this->fan_mode = fan_mode; + 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, 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 + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop_action(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + 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_HEAT_COOL; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + 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, 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 + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop_action(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + 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_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; + } else { + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; + } + 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<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + 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; +} +void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +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_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +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_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); + } + 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); + } + 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_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + if (this->supports_away_) { + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", + this->away_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature); + } + } +} + +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) {} + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h new file mode 100644 index 0000000000..8d3e926752 --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.h @@ -0,0 +1,431 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" + +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(); + ThermostatClimateTargetTempConfig(float default_temperature); + ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + + float default_temperature{NAN}; + float default_temperature_low{NAN}; + float default_temperature_high{NAN}; + float cool_deadband_{NAN}; + float cool_overrun_{NAN}; + float heat_deadband_{NAN}; + float heat_overrun_{NAN}; +}; + +class ThermostatClimate : public climate::Climate, public Component { + public: + ThermostatClimate(); + void setup() override; + void dump_config() override; + + 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); + 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); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + + void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); + 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; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + 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. + void control(const climate::ClimateCall &call) override; + + /// Change the away setting, will reset target temperatures to defaults. + void change_away_(bool away); + + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the required action of this climate controller. + 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, 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, bool publish_state = true); + + /// Switch the climate device to the given climate 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, 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}; + + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// 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. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + 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}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// 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. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/mode. + /// + /// A null value for this attribute means that the controller has no heating action + /// 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. + /// + /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// 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 + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// 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_{}; + + /// 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 +} // namespace esphome diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6283392103..2d73d0aef9 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,133 +1,84 @@ -import bisect -import datetime import logging -import math -import string +from importlib import resources +from typing import Optional -import pytz import tzlocal import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF_HOURS, \ - CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, \ - CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE -from esphome.core import coroutine, coroutine_with_priority +from esphome.const import ( + CONF_ID, + CONF_CRON, + CONF_DAYS_OF_MONTH, + CONF_DAYS_OF_WEEK, + CONF_HOURS, + CONF_MINUTES, + CONF_MONTHS, + CONF_ON_TIME, + CONF_ON_TIME_SYNC, + CONF_SECONDS, + CONF_TIMEZONE, + CONF_TRIGGER_ID, + CONF_AT, + CONF_SECOND, + CONF_HOUR, + CONF_MINUTE, +) +from esphome.core import coroutine_with_priority +from esphome.automation import Condition _LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@OttoWinter"] IS_PLATFORM_COMPONENT = True -time_ns = cg.esphome_ns.namespace('time') -RealTimeClock = time_ns.class_('RealTimeClock', cg.Component) -CronTrigger = time_ns.class_('CronTrigger', automation.Trigger.template(), cg.Component) -ESPTime = time_ns.struct('ESPTime') +time_ns = cg.esphome_ns.namespace("time") +RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent) +CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component) +SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) +ESPTime = time_ns.struct("ESPTime") +TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) -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 savings 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: + iana_key = tzlocal.get_localzone_name() + if iana_key is None: + raise cv.Invalid( + "Could not automatically determine timezone, please set timezone manually." + ) + _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): @@ -137,45 +88,60 @@ def _parse_cron_int(value, special_mapping, message): try: return int(value) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid(message.format(value)) def _parse_cron_part(part, min_value, max_value, special_mapping): - if part in ('*', '?'): + if part in ("*", "?"): return set(range(min_value, max_value + 1)) - if '/' in part: - data = part.split('/') + if "/" in part: + data = part.split("/") if len(data) > 2: - raise cv.Invalid("Can't have more than two '/' in one time expression, got {}" - .format(part)) + raise cv.Invalid( + f"Can't have more than two '/' in one time expression, got {part}" + ) offset, repeat = data offset_n = 0 if offset: - offset_n = _parse_cron_int(offset, special_mapping, - "Offset for '/' time expression must be an integer, got {}") + offset_n = _parse_cron_int( + offset, + special_mapping, + "Offset for '/' time expression must be an integer, got {}", + ) try: repeat_n = int(repeat) except ValueError: - raise cv.Invalid("Repeat for '/' time expression must be an integer, got {}" - .format(repeat)) + # pylint: disable=raise-missing-from + raise cv.Invalid( + 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 "-" in part: + data = part.split("-") if len(data) > 2: - raise cv.Invalid("Can't have more than two '-' in range time expression '{}'" - .format(part)) + raise cv.Invalid( + f"Can't have more than two '-' in range time expression '{part}'" + ) begin, end = data - begin_n = _parse_cron_int(begin, special_mapping, "Number for time range must be integer, " - "got {}") - end_n = _parse_cron_int(end, special_mapping, "Number for time range must be integer, " - "got {}") + begin_n = _parse_cron_int( + begin, special_mapping, "Number for time range must be integer, " "got {}" + ) + end_n = _parse_cron_int( + end, special_mapping, "Number for time range must be integer, " "got {}" + ) if end_n < begin_n: return set(range(end_n, max_value + 1)) | set(range(min_value, begin_n + 1)) return set(range(begin_n, end_n + 1)) - return {_parse_cron_int(part, special_mapping, "Number for time expression must be an " - "integer, got {}")} + return { + _parse_cron_int( + part, + special_mapping, + "Number for time expression must be an " "integer, got {}", + ) + } def cron_expression_validator(name, min_value, max_value, special_mapping=None): @@ -184,42 +150,68 @@ 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) values = set() - for part in value.split(','): + for part in value.split(","): values |= _parse_cron_part(part, min_value, max_value, special_mapping) return validator(list(values)) return validator -validate_cron_seconds = cron_expression_validator('seconds', 0, 60) -validate_cron_minutes = cron_expression_validator('minutes', 0, 59) -validate_cron_hours = cron_expression_validator('hours', 0, 23) -validate_cron_days_of_month = cron_expression_validator('days of month', 1, 31) -validate_cron_months = cron_expression_validator('months', 1, 12, { - 'JAN': 1, 'FEB': 2, 'MAR': 3, 'APR': 4, 'MAY': 5, 'JUN': 6, 'JUL': 7, 'AUG': 8, - 'SEP': 9, 'OCT': 10, 'NOV': 11, 'DEC': 12 -}) -validate_cron_days_of_week = cron_expression_validator('days of week', 1, 7, { - 'SUN': 1, 'MON': 2, 'TUE': 3, 'WED': 4, 'THU': 5, 'FRI': 6, 'SAT': 7 -}) -CRON_KEYS = [CONF_SECONDS, CONF_MINUTES, CONF_HOURS, CONF_DAYS_OF_MONTH, CONF_MONTHS, - CONF_DAYS_OF_WEEK] +validate_cron_seconds = cron_expression_validator("seconds", 0, 60) +validate_cron_minutes = cron_expression_validator("minutes", 0, 59) +validate_cron_hours = cron_expression_validator("hours", 0, 23) +validate_cron_days_of_month = cron_expression_validator("days of month", 1, 31) +validate_cron_months = cron_expression_validator( + "months", + 1, + 12, + { + "JAN": 1, + "FEB": 2, + "MAR": 3, + "APR": 4, + "MAY": 5, + "JUN": 6, + "JUL": 7, + "AUG": 8, + "SEP": 9, + "OCT": 10, + "NOV": 11, + "DEC": 12, + }, +) +validate_cron_days_of_week = cron_expression_validator( + "days of week", + 1, + 7, + {"SUN": 1, "MON": 2, "TUE": 3, "WED": 4, "THU": 5, "FRI": 6, "SAT": 7}, +) +CRON_KEYS = [ + CONF_SECONDS, + CONF_MINUTES, + CONF_HOURS, + CONF_DAYS_OF_MONTH, + CONF_MONTHS, + CONF_DAYS_OF_WEEK, +] def validate_cron_raw(value): value = cv.string(value) - value = value.split(' ') + value = value.split(" ") if len(value) != 6: - raise cv.Invalid("Cron expression must consist of exactly 6 space-separated parts, " - "not {}".format(len(value))) + raise cv.Invalid( + 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 { CONF_SECONDS: validate_cron_seconds(seconds), @@ -237,9 +229,9 @@ def validate_time_at(value): CONF_HOURS: [value[CONF_HOUR]], CONF_MINUTES: [value[CONF_MINUTE]], CONF_SECONDS: [value[CONF_SECOND]], - CONF_DAYS_OF_MONTH: validate_cron_days_of_month('*'), - CONF_MONTHS: validate_cron_months('*'), - CONF_DAYS_OF_WEEK: validate_cron_days_of_week('*'), + CONF_DAYS_OF_MONTH: validate_cron_days_of_month("*"), + CONF_MONTHS: validate_cron_months("*"), + CONF_DAYS_OF_WEEK: validate_cron_days_of_week("*"), } @@ -265,35 +257,44 @@ 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({ - cv.Optional(CONF_TIMEZONE, default=detect_tz): validate_tz, - cv.Optional(CONF_ON_TIME): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CronTrigger), - cv.Optional(CONF_SECONDS): validate_cron_seconds, - cv.Optional(CONF_MINUTES): validate_cron_minutes, - cv.Optional(CONF_HOURS): validate_cron_hours, - cv.Optional(CONF_DAYS_OF_MONTH): validate_cron_days_of_month, - cv.Optional(CONF_MONTHS): validate_cron_months, - cv.Optional(CONF_DAYS_OF_WEEK): validate_cron_days_of_week, - cv.Optional(CONF_CRON): validate_cron_raw, - cv.Optional(CONF_AT): validate_time_at, - }, validate_cron_keys), -}) +TIME_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TIMEZONE, default=detect_tz): validate_tz, + cv.Optional(CONF_ON_TIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CronTrigger), + cv.Optional(CONF_SECONDS): validate_cron_seconds, + cv.Optional(CONF_MINUTES): validate_cron_minutes, + cv.Optional(CONF_HOURS): validate_cron_hours, + cv.Optional(CONF_DAYS_OF_MONTH): validate_cron_days_of_month, + cv.Optional(CONF_MONTHS): validate_cron_months, + cv.Optional(CONF_DAYS_OF_WEEK): validate_cron_days_of_week, + cv.Optional(CONF_CRON): validate_cron_raw, + cv.Optional(CONF_AT): validate_time_at, + }, + validate_cron_keys, + ), + cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncTrigger), + } + ), + } +).extend(cv.polling_component_schema("15min")) -@coroutine -def setup_time_core_(time_var, config): +async def setup_time_core_(time_var, config): cg.add(time_var.set_timezone(config[CONF_TIMEZONE])) for conf in config.get(CONF_ON_TIME, []): @@ -312,16 +313,35 @@ def setup_time_core_(time_var, config): days_of_week = conf.get(CONF_DAYS_OF_WEEK, list(range(1, 8))) cg.add(trigger.add_days_of_week(days_of_week)) - yield cg.register_component(trigger, conf) - yield automation.build_automation(trigger, [], conf) + await cg.register_component(trigger, conf) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_TIME_SYNC, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) + + await cg.register_component(trigger, conf) + await automation.build_automation(trigger, [], conf) -@coroutine -def register_time(time_var, config): - yield setup_time_core_(time_var, config) +async def register_time(time_var, config): + await setup_time_core_(time_var, config) @coroutine_with_priority(100.0) -def to_code(config): - cg.add_define('USE_TIME') +async def to_code(config): + cg.add_define("USE_TIME") cg.add_global(time_ns.using) + + +@automation.register_condition( + "time.has_time", + TimeHasTimeCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(RealTimeClock), + } + ), +) +async def time_has_time_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) diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 1232e6f834..7e16d7141f 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -1,10 +1,11 @@ #include "automation.h" #include "esphome/core/log.h" +#include namespace esphome { namespace time { -static const char *TAG = "automation"; +static const char *const TAG = "automation"; void CronTrigger::add_second(uint8_t second) { this->seconds_[second] = true; } void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; } @@ -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)) @@ -75,5 +79,9 @@ void CronTrigger::add_days_of_week(const std::vector &days_of_week) { } float CronTrigger::get_setup_priority() const { return setup_priority::HARDWARE; } +SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) { + rtc->add_on_time_sync_callback([this]() { this->trigger(); }); +} + } // namespace time } // namespace esphome diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index 978d25fbd4..6167aac4f7 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -37,5 +37,12 @@ class CronTrigger : public Trigger<>, public Component { optional last_check_; }; +class SyncTrigger : public Trigger<>, public Component { + public: + explicit SyncTrigger(RealTimeClock *rtc); + + protected: + RealTimeClock *rtc_; +}; } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index cb66dc3ce6..6f6739d293 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -1,32 +1,44 @@ #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 namespace esphome { namespace time { -static const char *TAG = "time"; +static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { setenv("TZ", this->timezone_.c_str(), 1); tzset(); - this->setup(); + PollingComponent::call_setup(); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; + ESP_LOGVV(TAG, "Got epoch %u", epoch); timezone tz = {0, 0}; - settimeofday(&timev, &tz); + int ret = settimeofday(&timev, &tz); + if (ret == EINVAL) { + // Some ESP8266 frameworks abort when timezone parameter is not NULL + // while ESP32 expects it not to be NULL + ret = settimeofday(&timev, nullptr); + } + + if (ret != 0) { + ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); + } 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: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); + + this->time_sync_callback_.call(); } size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 9f40fdc5b5..0c6fa6f3a0 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -1,10 +1,11 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include -#include #include +#include +#include namespace esphome { namespace time { @@ -29,13 +30,10 @@ struct ESPTime { uint8_t month; /// year uint16_t year; - /// daylight savings time flag + /// 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. @@ -105,7 +103,7 @@ struct ESPTime { /// The C library (newlib) available on ESPs only supports TZ strings that specify an offset and DST info; /// you cannot specify zone names or paths to zoneinfo files. /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html -class RealTimeClock : public Component { +class RealTimeClock : public PollingComponent { public: explicit RealTimeClock(); @@ -126,11 +124,26 @@ class RealTimeClock : public Component { void call_setup() override; + void add_on_time_sync_callback(std::function callback) { + this->time_sync_callback_.add(std::move(callback)); + }; + protected: /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); std::string timezone_{}; + + CallbackManager time_sync_callback_; +}; + +template class TimeHasTimeCondition : public Condition { + public: + TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {} + bool check(Ts... x) override { return this->parent_->now().is_valid(); } + + protected: + RealTimeClock *parent_; }; } // namespace time diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index dcb8d9505b..9625781c96 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -2,41 +2,53 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import cover -from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_ID, CONF_OPEN_ACTION, \ - CONF_OPEN_DURATION, CONF_STOP_ACTION, CONF_ASSUMED_STATE +from esphome.const import ( + CONF_CLOSE_ACTION, + CONF_CLOSE_DURATION, + CONF_ID, + CONF_OPEN_ACTION, + CONF_OPEN_DURATION, + CONF_STOP_ACTION, + CONF_ASSUMED_STATE, +) -time_based_ns = cg.esphome_ns.namespace('time_based') -TimeBasedCover = time_based_ns.class_('TimeBasedCover', cover.Cover, cg.Component) +time_based_ns = cg.esphome_ns.namespace("time_based") +TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Component) -CONF_HAS_BUILT_IN_ENDSTOP = 'has_built_in_endstop' +CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TimeBasedCover), - cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), - - cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, - - cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, - - cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TimeBasedCover), + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield cover.register_cover(var, config) + await cg.register_component(var, config) + await cover.register_cover(var, config) - yield automation.build_automation(var.get_stop_trigger(), [], config[CONF_STOP_ACTION]) + await automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) - yield automation.build_automation(var.get_open_trigger(), [], config[CONF_OPEN_ACTION]) + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) - yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) + await automation.build_automation( + var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] + ) cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 6d1de144f5..522252e907 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -1,10 +1,11 @@ #include "time_based_cover.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace time_based { -static const char *TAG = "time_based.cover"; +static const char *const TAG = "time_based.cover"; using namespace esphome::cover; @@ -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) { @@ -78,7 +94,7 @@ void TimeBasedCover::control(const CoverCall &call) { } void TimeBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } @@ -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/__init__.py b/esphome/components/tlc59208f/__init__.py index 4666b63b46..419fa397b7 100644 --- a/esphome/components/tlc59208f/__init__.py +++ b/esphome/components/tlc59208f/__init__.py @@ -3,18 +3,24 @@ import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] MULTI_CONF = True -tlc59208f_ns = cg.esphome_ns.namespace('tlc59208f') -TLC59208FOutput = tlc59208f_ns.class_('TLC59208FOutput', cg.Component, i2c.I2CDevice) +tlc59208f_ns = cg.esphome_ns.namespace("tlc59208f") +TLC59208FOutput = tlc59208f_ns.class_("TLC59208FOutput", cg.Component, i2c.I2CDevice) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(TLC59208FOutput), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TLC59208FOutput), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/tlc59208f/output.py b/esphome/components/tlc59208f/output.py index f61f7729e7..ac45909673 100644 --- a/esphome/components/tlc59208f/output.py +++ b/esphome/components/tlc59208f/output.py @@ -4,21 +4,23 @@ from esphome.components import output from esphome.const import CONF_CHANNEL, CONF_ID from . import TLC59208FOutput, tlc59208f_ns -DEPENDENCIES = ['tlc59208f'] +DEPENDENCIES = ["tlc59208f"] -TLC59208FChannel = tlc59208f_ns.class_('TLC59208FChannel', output.FloatOutput) -CONF_TLC59208F_ID = 'tlc59208f_id' +TLC59208FChannel = tlc59208f_ns.class_("TLC59208FChannel", output.FloatOutput) +CONF_TLC59208F_ID = "tlc59208f_id" -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel), - cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput), - - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), -}) +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel), + cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +) -def to_code(config): - paren = yield cg.get_variable(config[CONF_TLC59208F_ID]) - rhs = paren.create_channel(config[CONF_CHANNEL]) - var = cg.Pvariable(config[CONF_ID], rhs) - yield output.register_output(var, config) +async def to_code(config): + paren = await cg.get_variable(config[CONF_TLC59208F_ID]) + 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 6e65ff4e76..bd62f8de6d 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -1,11 +1,12 @@ #include "tlc59208f_output.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace tlc59208f { -static const char *TAG = "tlc59208f"; +static const char *const TAG = "tlc59208f"; // * marks register defaults // 0*: Register auto increment disabled, 1: Register auto increment enabled @@ -75,7 +76,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 +138,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/display.py b/esphome/components/tm1637/display.py index 211d78a9d9..609c62fd10 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -2,34 +2,52 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_CLK_PIN, CONF_DIO_PIN, CONF_ID, CONF_LAMBDA, CONF_INTENSITY +from esphome.const import ( + CONF_CLK_PIN, + CONF_DIO_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_INTENSITY, + CONF_INVERTED, + CONF_LENGTH, +) -tm1637_ns = cg.esphome_ns.namespace('tm1637') -TM1637Display = tm1637_ns.class_('TM1637Display', cg.PollingComponent) -TM1637DisplayRef = TM1637Display.operator('ref') +CODEOWNERS = ["@glmnet"] -CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TM1637Display), +tm1637_ns = cg.esphome_ns.namespace("tm1637") +TM1637Display = tm1637_ns.class_("TM1637Display", cg.PollingComponent) +TM1637DisplayRef = TM1637Display.operator("ref") - cv.Optional(CONF_INTENSITY, default=7): cv.All(cv.uint8_t, cv.Range(min=0, max=7)), - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, -}).extend(cv.polling_component_schema('1s')) +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1637Display), + cv.Optional(CONF_INTENSITY, default=7): cv.All( + cv.uint8_t, cv.Range(min=0, max=7) + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + cv.Optional(CONF_LENGTH, default=6): cv.All(cv.uint8_t, cv.Range(min=1, max=6)), + cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("1s")) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield display.register_display(var, config) + await cg.register_component(var, config) + await display.register_display(var, config) - clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_clk_pin(clk)) - dio = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + dio = await cg.gpio_pin_expression(config[CONF_DIO_PIN]) cg.add(var.set_dio_pin(dio)) cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_length(config[CONF_LENGTH])) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(TM1637DisplayRef, 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(TM1637DisplayRef, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index ebf2fff9d6..a21d2d438d 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -1,11 +1,12 @@ #include "tm1637.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace tm1637 { -const char* TAG = "display.tm1637"; +static const char *const TAG = "display.tm1637"; const uint8_t TM1637_I2C_COMM1 = 0x40; const uint8_t TM1637_I2C_COMM2 = 0xC0; const uint8_t TM1637_I2C_COMM3 = 0x80; @@ -20,7 +21,7 @@ const uint8_t TM1637_UNKNOWN_CHAR = 0b11111111; // --- // D X // XABCDEFG -const uint8_t TM1637_ASCII_TO_RAW[94] PROGMEM = { +const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 @@ -46,7 +47,7 @@ const uint8_t TM1637_ASCII_TO_RAW[94] PROGMEM = { 0b01011111, // '6', ord 0x36 0b01110000, // '7', ord 0x37 0b01111111, // '8', ord 0x38 - 0b01110011, // '9', ord 0x39 + 0b01111011, // '9', ord 0x39 0b01001000, // ':', ord 0x3A 0b01011000, // ';', ord 0x3B TM1637_UNKNOWN_CHAR, // '<', ord 0x3C @@ -115,6 +116,7 @@ const uint8_t TM1637_ASCII_TO_RAW[94] PROGMEM = { 0b00110001, // '{', ord 0x7B 0b00000110, // '|', ord 0x7C 0b00000111, // '}', ord 0x7D + 0b01100011, // '~', ord 0x7E (degree symbol) }; void TM1637Display::setup() { ESP_LOGCONFIG(TAG, "Setting up TM1637..."); @@ -128,14 +130,16 @@ void TM1637Display::setup() { } void TM1637Display::dump_config() { ESP_LOGCONFIG(TAG, "TM1637:"); - ESP_LOGCONFIG(TAG, " INTENSITY: %d", this->intensity_); + ESP_LOGCONFIG(TAG, " Intensity: %d", this->intensity_); + ESP_LOGCONFIG(TAG, " Inverted: %d", this->inverted_); + ESP_LOGCONFIG(TAG, " Length: %d", this->length_); LOG_PIN(" CLK Pin: ", this->clk_pin_); LOG_PIN(" DIO Pin: ", this->dio_pin_); LOG_UPDATE_INTERVAL(this); } void TM1637Display::update() { - for (uint8_t& i : this->buffer_) + for (uint8_t &i : this->buffer_) i = 0; if (this->writer_.has_value()) (*this->writer_)(*this); @@ -145,16 +149,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_(); } @@ -171,8 +175,14 @@ void TM1637Display::display() { this->send_byte_(TM1637_I2C_COMM2); // Write the data bytes - for (auto b : this->buffer_) { - this->send_byte_(b); + if (this->inverted_) { + for (int8_t i = this->length_ - 1; i >= 0; i--) { + this->send_byte_(this->buffer_[i]); + } + } else { + for (auto b : this->buffer_) { + this->send_byte_(b); + } } this->stop_(); @@ -188,71 +198,84 @@ 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; } -uint8_t TM1637Display::print(uint8_t start_pos, const char* str) { +uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); uint8_t pos = start_pos; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; - if (*str >= ' ' && *str <= '}') - data = pgm_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); + if (*str >= ' ' && *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); } // Remap segments, for compatibility with MAX7219 segment definition which is // XABCDEFG, but TM1637 is // XGFEDCBA - data = ((data & 0x80) ? 0x80 : 0) | // no move X - ((data & 0x40) ? 0x1 : 0) | // A - ((data & 0x20) ? 0x2 : 0) | // B - ((data & 0x10) ? 0x4 : 0) | // C - ((data & 0x8) ? 0x8 : 0) | // D - ((data & 0x4) ? 0x10 : 0) | // E - ((data & 0x2) ? 0x20 : 0) | // F - ((data & 0x1) ? 0x40 : 0); // G + if (this->inverted_) { + // XABCDEFG > XGCBAFED + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x8 : 0) | // A + ((data & 0x20) ? 0x10 : 0) | // B + ((data & 0x10) ? 0x20 : 0) | // C + ((data & 0x8) ? 0x1 : 0) | // D + ((data & 0x4) ? 0x2 : 0) | // E + ((data & 0x2) ? 0x4 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + } else { + // XABCDEFG > XGFEDCBA + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x1 : 0) | // A + ((data & 0x20) ? 0x2 : 0) | // B + ((data & 0x10) ? 0x4 : 0) | // C + ((data & 0x8) ? 0x8 : 0) | // D + ((data & 0x4) ? 0x10 : 0) | // E + ((data & 0x2) ? 0x20 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + } if (*str == '.') { if (pos != start_pos) pos--; this->buffer_[pos] |= 0b10000000; } else { - if (pos >= 4) { + if (pos >= 6) { ESP_LOGE(TAG, "String is too long for the display!"); break; } @@ -262,8 +285,8 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char* str) { } return pos - start_pos; } -uint8_t TM1637Display::print(const char* str) { return this->print(0, str); } -uint8_t TM1637Display::printf(uint8_t pos, const char* format, ...) { +uint8_t TM1637Display::print(const char *str) { return this->print(0, str); } +uint8_t TM1637Display::printf(uint8_t pos, const char *format, ...) { va_list arg; va_start(arg, format); char buffer[64]; @@ -273,7 +296,7 @@ uint8_t TM1637Display::printf(uint8_t pos, const char* format, ...) { return this->print(pos, buffer); return 0; } -uint8_t TM1637Display::printf(const char* format, ...) { +uint8_t TM1637Display::printf(const char *format, ...) { va_list arg; va_start(arg, format); char buffer[64]; @@ -285,14 +308,14 @@ uint8_t TM1637Display::printf(const char* format, ...) { } #ifdef USE_TIME -uint8_t TM1637Display::strftime(uint8_t pos, const char* format, time::ESPTime time) { +uint8_t TM1637Display::strftime(uint8_t pos, const char *format, time::ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1637Display::strftime(const char* format, time::ESPTime time) { return this->strftime(0, format, time); } +uint8_t TM1637Display::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } #endif } // namespace tm1637 diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 91e8ba66c0..9b2f014ff9 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" @@ -41,6 +41,8 @@ class TM1637Display : public PollingComponent { uint8_t print(const char *str); void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_length(uint8_t length) { this->length_ = length; } void display(); @@ -62,8 +64,10 @@ class TM1637Display : public PollingComponent { GPIOPin *dio_pin_; GPIOPin *clk_pin_; uint8_t intensity_; + uint8_t length_; + bool inverted_; optional writer_{}; - uint8_t buffer_[4] = {0}; + uint8_t buffer_[6] = {0}; }; } // namespace tm1637 diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index aa972552f4..9d2b17afdc 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -2,108 +2,135 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_DIO_PIN, CONF_LEVEL, CONF_BRIGHTNESS +from esphome.const import ( + CONF_ID, + CONF_CLK_PIN, + CONF_DIO_PIN, + CONF_LEVEL, + CONF_BRIGHTNESS, +) -tm1651_ns = cg.esphome_ns.namespace('tm1651') -TM1651Display = tm1651_ns.class_('TM1651Display', cg.Component) +tm1651_ns = cg.esphome_ns.namespace("tm1651") +TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) -SetLevelPercentAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) -SetLevelAction = tm1651_ns.class_('SetLevelAction', automation.Action) -SetBrightnessAction = tm1651_ns.class_('SetBrightnessAction', automation.Action) -TurnOnAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) -TurnOffAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) +SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) +SetLevelAction = tm1651_ns.class_("SetLevelAction", automation.Action) +SetBrightnessAction = tm1651_ns.class_("SetBrightnessAction", automation.Action) +TurnOnAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) +TurnOffAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) -CONF_LEVEL_PERCENT = 'level_percent' +CONF_LEVEL_PERCENT = "level_percent" TM1651_BRIGHTNESS_OPTIONS = { 1: TM1651Display.TM1651_BRIGHTNESS_LOW, 2: TM1651Display.TM1651_BRIGHTNESS_MEDIUM, - 3: TM1651Display.TM1651_BRIGHTNESS_HIGH + 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)) validate_level = cv.All(cv.int_range(min=0, max=7)) validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - clk_pin = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + clk_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_clk_pin(clk_pin)) - dio_pin = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + dio_pin = await cg.gpio_pin_expression(config[CONF_DIO_PIN]) 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({ - cv.Required(CONF_ID): cv.use_id(TM1651Display), -}) +BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(TM1651Display), + } +) -@automation.register_action('tm1651.turn_on', TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) -def output_turn_on_to_code(config, action_id, template_arg, args): +@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +async def output_turn_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var - - -@automation.register_action('tm1651.turn_off', TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA) -def output_turn_off_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - yield var + await cg.register_parented(var, config[CONF_ID]) + return var @automation.register_action( - 'tm1651.set_level_percent', + "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA +) +async def output_turn_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "tm1651.set_level_percent", SetLevelPercentAction, - cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), - }, key=CONF_LEVEL_PERCENT)) -def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), + }, + key=CONF_LEVEL_PERCENT, + ), +) +async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) cg.add(var.set_level_percent(template_)) - yield var + return var @automation.register_action( - 'tm1651.set_level', + "tm1651.set_level", SetLevelAction, - cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_LEVEL): cv.templatable(validate_level), - }, key=CONF_LEVEL)) -def tm1651_set_level_to_code(config, action_id, template_arg, args): + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL): cv.templatable(validate_level), + }, + key=CONF_LEVEL, + ), +) +async def tm1651_set_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_LEVEL], args, cg.uint8) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8) cg.add(var.set_level(template_)) - yield var + return var @automation.register_action( - 'tm1651.set_brightness', + "tm1651.set_brightness", SetBrightnessAction, - cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), - }, key=CONF_BRIGHTNESS)) -def tm1651_set_brightness_to_code(config, action_id, template_arg, args): + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), + }, + key=CONF_BRIGHTNESS, + ), +) +async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) - template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) cg.add(var.set_brightness(template_)) - yield var + return var diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index 0417706327..c6bb1bc025 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -1,10 +1,13 @@ +#ifdef USE_ARDUINO + #include "tm1651.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace tm1651 { -static const char *TAG = "tm1651.display"; +static const char *const TAG = "tm1651.display"; static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; static const uint8_t TM1651_MAX_LEVEL = 7; @@ -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/__init__.py b/esphome/components/tmp102/__init__.py new file mode 100644 index 0000000000..3e32a230f2 --- /dev/null +++ b/esphome/components/tmp102/__init__.py @@ -0,0 +1,9 @@ +""" +The TMP102 is a two-wire, serial output temperature +sensor available in a tiny SOT563 package. Requiring +no external components, the TMP102 is capable of +reading temperatures to a resolution of 0.0625°C. + +https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf + +""" diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py new file mode 100644 index 0000000000..c5ffbb8df5 --- /dev/null +++ b/esphome/components/tmp102/sensor.py @@ -0,0 +1,49 @@ +""" +The TMP102 is a two-wire, serial output temperature +sensor available in a tiny SOT563 package. Requiring +no external components, the TMP102 is capable of +reading temperatures to a resolution of 0.0625°C. + +https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf + +""" +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@timsavage"] +DEPENDENCIES = ["i2c"] + +tmp102_ns = cg.esphome_ns.namespace("tmp102") +TMP102Component = tmp102_ns.class_( + "TMP102Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TMP102Component), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +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/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp new file mode 100644 index 0000000000..f6bb9a05c0 --- /dev/null +++ b/esphome/components/tmp102/tmp102.cpp @@ -0,0 +1,54 @@ +#include "tmp102.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tmp102 { + +static const char *const TAG = "tmp102"; + +static const uint8_t TMP102_ADDRESS = 0x48; +static const uint8_t TMP102_REGISTER_TEMPERATURE = 0x00; +static const uint8_t TMP102_REGISTER_CONFIGURATION = 0x01; +static const uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02; +static const uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03; + +static const float TMP102_CONVERSION_FACTOR = 0.0625; + +void TMP102Component::setup() { ESP_LOGCONFIG(TAG, "Setting up TMP102..."); } + +void TMP102Component::dump_config() { + ESP_LOGCONFIG(TAG, "TMP102:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TMP102 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} + +void TMP102Component::update() { + uint16_t raw_temperature; + if (this->write(&TMP102_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + delay(50); // NOLINT + if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_temperature = i2c::i2ctohs(raw_temperature); + + raw_temperature = raw_temperature >> 4; + float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; + ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); + + this->publish_state(temperature); + this->status_clear_warning(); +} + +float TMP102Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace tmp102 +} // namespace esphome diff --git a/esphome/components/tmp102/tmp102.h b/esphome/components/tmp102/tmp102.h new file mode 100644 index 0000000000..1bbb2d5ae3 --- /dev/null +++ b/esphome/components/tmp102/tmp102.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tmp102 { + +class TMP102Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + /// Setup (reset) the sensor and check connection. + void setup() override; + void dump_config() override; + /// Update the sensor values (temperature) + void update() override; + + float get_setup_priority() const override; +}; + +} // namespace tmp102 +} // namespace esphome diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index ddca3eeb64..054864dd83 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -1,18 +1,37 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, \ - UNIT_CELSIUS, ICON_THERMOMETER +from esphome.const import ( + CONF_ID, + CONF_UPDATE_INTERVAL, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] -tmp117_ns = cg.esphome_ns.namespace('tmp117') -TMP117Component = tmp117_ns.class_('TMP117Component', - cg.PollingComponent, i2c.I2CDevice, sensor.Sensor) +tmp117_ns = cg.esphome_ns.namespace("tmp117") +TMP117Component = tmp117_ns.class_( + "TMP117Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) -CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ - cv.GenerateID(): cv.declare_id(TMP117Component), -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x48))) +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TMP117Component), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) def determine_config_register(polling_period): @@ -57,11 +76,11 @@ def determine_config_register(polling_period): return 0x0000 -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await sensor.register_sensor(var, config) update_period = config[CONF_UPDATE_INTERVAL].total_seconds cg.add(var.set_config(determine_config_register(update_period))) diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index 9040d9bfed..f719ea93b6 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace tmp117 { -static const char *TAG = "tmp117"; +static const char *const TAG = "tmp117"; void TMP117Component::update() { int16_t data; diff --git a/esphome/components/tof10120/__init__.py b/esphome/components/tof10120/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py new file mode 100644 index 0000000000..2d3add2399 --- /dev/null +++ b/esphome/components/tof10120/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, +) + +CODEOWNERS = ["@wstrzalka"] +DEPENDENCIES = ["i2c"] + +tof10120_ns = cg.esphome_ns.namespace("tof10120") +TOF10120Sensor = tof10120_ns.class_( + "TOF10120Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +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(TOF10120Sensor)}) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x52)) +) + + +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 i2c.register_i2c_device(var, config) diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp new file mode 100644 index 0000000000..5cd086938e --- /dev/null +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -0,0 +1,59 @@ +#include "tof10120_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +// Very basic support for TOF10120 distance sensor + +namespace esphome { +namespace tof10120 { + +static const char *const TAG = "tof10120"; +static const uint8_t TOF10120_READ_DISTANCE_CMD[] = {0x00}; +static const uint8_t TOF10120_DEFAULT_DELAY = 30; + +static const uint8_t TOF10120_DIR_SEND_REGISTER = 0x0e; +static const uint8_t TOF10120_DISTANCE_REGISTER = 0x00; + +static const uint16_t TOF10120_OUT_OF_RANGE_VALUE = 2000; + +void TOF10120Sensor::dump_config() { + LOG_SENSOR("", "TOF10120", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} + +void TOF10120Sensor::setup() {} + +void TOF10120Sensor::update() { + if (!this->write_bytes(TOF10120_DISTANCE_REGISTER, TOF10120_READ_DISTANCE_CMD, sizeof(TOF10120_READ_DISTANCE_CMD))) { + ESP_LOGE(TAG, "Communication with TOF10120 failed on write"); + this->status_set_warning(); + return; + } + + uint8_t data[2]; + 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; + } + + uint32_t distance_mm = (data[0] << 8) | data[1]; + ESP_LOGI(TAG, "Data read: %dmm", distance_mm); + + if (distance_mm == TOF10120_OUT_OF_RANGE_VALUE) { + ESP_LOGW(TAG, "Distance measurement out of range"); + this->publish_state(NAN); + } else { + this->publish_state(distance_mm / 1000.0f); + } + this->status_clear_warning(); +} + +} // namespace tof10120 +} // namespace esphome diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h new file mode 100644 index 0000000000..90bad8ed07 --- /dev/null +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tof10120 { + +class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; +}; +} // namespace tof10120 +} // namespace esphome diff --git a/esphome/components/toshiba/__init__.py b/esphome/components/toshiba/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py new file mode 100644 index 0000000000..3f2c644c87 --- /dev/null +++ b/esphome/components/toshiba/climate.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +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), + } +) + + +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 new file mode 100644 index 0000000000..975a149b52 --- /dev/null +++ b/esphome/components/toshiba/toshiba.cpp @@ -0,0 +1,713 @@ +#include "toshiba.h" + +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; +const uint8_t TOSHIBA_COMMAND_MOTION = 0x02; + +const uint8_t TOSHIBA_MODE_AUTO = 0x00; +const uint8_t TOSHIBA_MODE_COOL = 0x01; +const uint8_t TOSHIBA_MODE_DRY = 0x02; +const uint8_t TOSHIBA_MODE_HEAT = 0x03; +const uint8_t TOSHIBA_MODE_FAN_ONLY = 0x04; +const uint8_t TOSHIBA_MODE_OFF = 0x07; + +const uint8_t TOSHIBA_FAN_SPEED_AUTO = 0x00; +const uint8_t TOSHIBA_FAN_SPEED_QUIET = 0x20; +const uint8_t TOSHIBA_FAN_SPEED_1 = 0x40; +const uint8_t TOSHIBA_FAN_SPEED_2 = 0x60; +const uint8_t TOSHIBA_FAN_SPEED_3 = 0x80; +const uint8_t TOSHIBA_FAN_SPEED_4 = 0xa0; +const uint8_t TOSHIBA_FAN_SPEED_5 = 0xc0; + +const uint8_t TOSHIBA_POWER_HIGH = 0x01; +const uint8_t TOSHIBA_POWER_ECO = 0x03; + +const uint8_t TOSHIBA_MOTION_SWING = 0x04; +const uint8_t TOSHIBA_MOTION_FIX = 0x00; + +// 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 + message[0] = 0xf2; + message[1] = 0x0d; + + // Message length + message[2] = message_length - 6; + + // First checksum + message[3] = message[0] ^ message[1] ^ message[2]; + + // Command + message[4] = TOSHIBA_COMMAND_DEFAULT; + + // 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 + uint8_t mode; + switch (this->mode) { + case climate::CLIMATE_MODE_OFF: + mode = TOSHIBA_MODE_OFF; + break; + + case climate::CLIMATE_MODE_HEAT: + mode = TOSHIBA_MODE_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + mode = TOSHIBA_MODE_COOL; + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + default: + mode = TOSHIBA_MODE_AUTO; + } + + message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; + + // Zero + message[7] = 0x00; + + // 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 + + // The last byte is the xor of all bytes from [4] + for (uint8_t i = 4; i < 8; i++) { + message[8] ^= message[i]; + } + + // Transmit + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + encode_(data, message, message_length, 1); + + 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 (size_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 (size_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))) { + data->space(TOSHIBA_ONE_SPACE); + } else { + data->space(TOSHIBA_ZERO_SPACE); + } + } + } + data->item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE); + } +} + +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)) { + message[byte] |= 1 << (7 - bit); + } else if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { + message[byte] &= static_cast(~(1 << (7 - bit))); + } else { + return false; + } + } + } + return true; +} + +} // namespace toshiba +} // namespace esphome diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h new file mode 100644 index 0000000000..36e8760169 --- /dev/null +++ b/esphome/components/toshiba/toshiba.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace toshiba { + +// 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_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 diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 7fde9f5582..0c20ccd27c 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -1,28 +1,77 @@ 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_RESTORE, + CONF_TIME_ID, + DEVICE_CLASS_ENERGY, + CONF_METHOD, + STATE_CLASS_TOTAL_INCREASING, +) +from esphome.core.entity_helpers import inherit_property_from -DEPENDENCIES = ['time'] +DEPENDENCIES = ["time"] -CONF_POWER_ID = 'power_id' -total_daily_energy_ns = cg.esphome_ns.namespace('total_daily_energy') -TotalDailyEnergy = total_daily_energy_ns.class_('TotalDailyEnergy', sensor.Sensor, cg.Component) +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_RESTORE, default=True): cv.boolean, + 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), +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - sens = yield cg.get_variable(config[CONF_POWER_ID]) + sens = await cg.get_variable(config[CONF_POWER_ID]) cg.add(var.set_parent(sens)) - time_ = yield cg.get_variable(config[CONF_TIME_ID]) + time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) + cg.add(var.set_restore(config[CONF_RESTORE])) + 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 1816c46844..3746301715 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -4,22 +4,25 @@ namespace esphome { namespace total_daily_energy { -static const char *TAG = "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()); + float initial_value = 0; - float recovered; - if (this->pref_.load(&recovered)) { - this->publish_state_and_save(recovered); - } else { - this->publish_state_and_save(0); + if (this->restore_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_.load(&initial_value); } + this->publish_state_and_save(initial_value); + 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..498f65891e 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -2,21 +2,30 @@ #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_restore(bool restore) { restore_ = restore; } + 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 +37,14 @@ 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}; + bool restore_; 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 d51ece09d5..cf3837cb4d 100644 --- a/esphome/components/tsl2561/sensor.py +++ b/esphome/components/tsl2561/sensor.py @@ -1,25 +1,32 @@ 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_INTEGRATION_TIME, UNIT_LUX, ICON_BRIGHTNESS_5 +from esphome.const import ( + CONF_GAIN, + CONF_ID, + CONF_INTEGRATION_TIME, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -tsl2561_ns = cg.esphome_ns.namespace('tsl2561') -TSL2561IntegrationTime = tsl2561_ns.enum('TSL2561IntegrationTime') +tsl2561_ns = cg.esphome_ns.namespace("tsl2561") +TSL2561IntegrationTime = tsl2561_ns.enum("TSL2561IntegrationTime") INTEGRATION_TIMES = { 14: TSL2561IntegrationTime.TSL2561_INTEGRATION_14MS, 101: TSL2561IntegrationTime.TSL2561_INTEGRATION_101MS, 402: TSL2561IntegrationTime.TSL2561_INTEGRATION_402MS, } -TSL2561Gain = tsl2561_ns.enum('TSL2561Gain') +TSL2561Gain = tsl2561_ns.enum("TSL2561Gain") GAINS = { - '1X': TSL2561Gain.TSL2561_GAIN_1X, - '16X': TSL2561Gain.TSL2561_GAIN_16X, + "1X": TSL2561Gain.TSL2561_GAIN_1X, + "16X": TSL2561Gain.TSL2561_GAIN_16X, } -CONF_IS_CS_PACKAGE = 'is_cs_package' +CONF_IS_CS_PACKAGE = "is_cs_package" def validate_integration_time(value): @@ -27,22 +34,37 @@ def validate_integration_time(value): return cv.enum(INTEGRATION_TIMES, int=True)(value) -TSL2561Sensor = tsl2561_ns.class_('TSL2561Sensor', sensor.Sensor, cg.PollingComponent, - i2c.I2CDevice) +TSL2561Sensor = tsl2561_ns.class_( + "TSL2561Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 1).extend({ - cv.GenerateID(): cv.declare_id(TSL2561Sensor), - cv.Optional(CONF_INTEGRATION_TIME, default='402ms'): validate_integration_time, - cv.Optional(CONF_GAIN, default='1X'): cv.enum(GAINS, upper=True), - cv.Optional(CONF_IS_CS_PACKAGE, default=False): cv.boolean, -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x39)) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TSL2561Sensor), + cv.Optional( + CONF_INTEGRATION_TIME, default="402ms" + ): validate_integration_time, + cv.Optional(CONF_GAIN, default="1X"): cv.enum(GAINS, upper=True), + cv.Optional(CONF_IS_CS_PACKAGE, default=False): cv.boolean, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x39)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await sensor.register_sensor(var, config) cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) cg.add(var.set_gain(config[CONF_GAIN])) diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index ffd39a54b0..e80e9220d8 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace tsl2561 { -static const char *TAG = "tsl2561"; +static const char *const TAG = "tsl2561"; static const uint8_t TSL2561_COMMAND_BIT = 0x80; static const uint8_t TSL2561_WORD_BIT = 0x20; 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/__init__.py b/esphome/components/ttp229_bsf/__init__.py index 5ec182d46b..f1f86c929e 100644 --- a/esphome/components/ttp229_bsf/__init__.py +++ b/esphome/components/ttp229_bsf/__init__.py @@ -3,27 +3,29 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_ID, CONF_SDO_PIN, CONF_SCL_PIN -DEPENDENCIES = ['i2c'] -AUTO_LOAD = ['binary_sensor'] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["binary_sensor"] -CONF_TTP229_ID = 'ttp229_id' -ttp229_bsf_ns = cg.esphome_ns.namespace('ttp229_bsf') +CONF_TTP229_ID = "ttp229_id" +ttp229_bsf_ns = cg.esphome_ns.namespace("ttp229_bsf") -TTP229BSFComponent = ttp229_bsf_ns.class_('TTP229BSFComponent', cg.Component) +TTP229BSFComponent = ttp229_bsf_ns.class_("TTP229BSFComponent", cg.Component) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(TTP229BSFComponent), - cv.Required(CONF_SDO_PIN): pins.gpio_input_pullup_pin_schema, - cv.Required(CONF_SCL_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TTP229BSFComponent), + cv.Required(CONF_SDO_PIN): pins.gpio_input_pullup_pin_schema, + cv.Required(CONF_SCL_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) - sdo = yield cg.gpio_pin_expression(config[CONF_SDO_PIN]) + sdo = await cg.gpio_pin_expression(config[CONF_SDO_PIN]) cg.add(var.set_sdo_pin(sdo)) - scl = yield cg.gpio_pin_expression(config[CONF_SCL_PIN]) + scl = await cg.gpio_pin_expression(config[CONF_SCL_PIN]) cg.add(var.set_scl_pin(scl)) diff --git a/esphome/components/ttp229_bsf/binary_sensor.py b/esphome/components/ttp229_bsf/binary_sensor.py index 7a1ab3dc9f..75540fe0e8 100644 --- a/esphome/components/ttp229_bsf/binary_sensor.py +++ b/esphome/components/ttp229_bsf/binary_sensor.py @@ -4,20 +4,22 @@ from esphome.components import binary_sensor from esphome.const import CONF_CHANNEL, CONF_ID from . import ttp229_bsf_ns, TTP229BSFComponent, CONF_TTP229_ID -DEPENDENCIES = ['ttp229_bsf'] -TTP229BSFChannel = ttp229_bsf_ns.class_('TTP229BSFChannel', binary_sensor.BinarySensor) +DEPENDENCIES = ["ttp229_bsf"] +TTP229BSFChannel = ttp229_bsf_ns.class_("TTP229BSFChannel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TTP229BSFChannel), - cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229BSFComponent), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TTP229BSFChannel), + cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229BSFComponent), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) + await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_channel(config[CONF_CHANNEL])) - hub = yield cg.get_variable(config[CONF_TTP229_ID]) + hub = await cg.get_variable(config[CONF_TTP229_ID]) cg.add(hub.register_channel(var)) diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.cpp b/esphome/components/ttp229_bsf/ttp229_bsf.cpp index 9b5c3c67de..93c947d28a 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.cpp +++ b/esphome/components/ttp229_bsf/ttp229_bsf.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace ttp229_bsf { -static const char *TAG = "ttp229_bsf"; +static const char *const TAG = "ttp229_bsf"; void TTP229BSFComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up ttp229_bsf... "); 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/__init__.py b/esphome/components/ttp229_lsf/__init__.py index 6faca970f0..cba41a7938 100644 --- a/esphome/components/ttp229_lsf/__init__.py +++ b/esphome/components/ttp229_lsf/__init__.py @@ -3,21 +3,29 @@ import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID -DEPENDENCIES = ['i2c'] -AUTO_LOAD = ['binary_sensor'] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["binary_sensor"] -CONF_TTP229_ID = 'ttp229_id' -ttp229_lsf_ns = cg.esphome_ns.namespace('ttp229_lsf') +CONF_TTP229_ID = "ttp229_id" +ttp229_lsf_ns = cg.esphome_ns.namespace("ttp229_lsf") -TTP229LSFComponent = ttp229_lsf_ns.class_('TTP229LSFComponent', cg.Component, i2c.I2CDevice) +TTP229LSFComponent = ttp229_lsf_ns.class_( + "TTP229LSFComponent", cg.Component, i2c.I2CDevice +) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(TTP229LSFComponent), -}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x57)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TTP229LSFComponent), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x57)) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ttp229_lsf/binary_sensor.py b/esphome/components/ttp229_lsf/binary_sensor.py index 870bf16287..b52a9e8575 100644 --- a/esphome/components/ttp229_lsf/binary_sensor.py +++ b/esphome/components/ttp229_lsf/binary_sensor.py @@ -4,20 +4,22 @@ from esphome.components import binary_sensor from esphome.const import CONF_CHANNEL, CONF_ID from . import ttp229_lsf_ns, TTP229LSFComponent, CONF_TTP229_ID -DEPENDENCIES = ['ttp229_lsf'] -TTP229Channel = ttp229_lsf_ns.class_('TTP229Channel', binary_sensor.BinarySensor) +DEPENDENCIES = ["ttp229_lsf"] +TTP229Channel = ttp229_lsf_ns.class_("TTP229Channel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(TTP229Channel), - cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229LSFComponent), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), -}) +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TTP229Channel), + cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229LSFComponent), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), + } +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield binary_sensor.register_binary_sensor(var, config) + await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_channel(config[CONF_CHANNEL])) - hub = yield cg.get_variable(config[CONF_TTP229_ID]) + hub = await cg.get_variable(config[CONF_TTP229_ID]) cg.add(hub.register_channel(var)) diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.cpp b/esphome/components/ttp229_lsf/ttp229_lsf.cpp index 2bd5f8556d..21c7b02740 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.cpp +++ b/esphome/components/ttp229_lsf/ttp229_lsf.cpp @@ -4,11 +4,12 @@ namespace esphome { namespace ttp229_lsf { -static const char *TAG = "ttp229_lsf"; +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/__init__.py b/esphome/components/tuya/__init__.py index 541f10f862..965893e012 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -1,20 +1,126 @@ +from esphome.components import time +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TIME_ID, CONF_TRIGGER_ID, CONF_SENSOR_DATAPOINT -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -tuya_ns = cg.esphome_ns.namespace('tuya') -Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) +CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints" -CONF_TUYA_ID = 'tuya_id' -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(Tuya), -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) +CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update" +CONF_DATAPOINT_TYPE = "datapoint_type" + +tuya_ns = cg.esphome_ns.namespace("tuya") +Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) + +DPTYPE_ANY = "any" +DPTYPE_RAW = "raw" +DPTYPE_BOOL = "bool" +DPTYPE_INT = "int" +DPTYPE_UINT = "uint" +DPTYPE_STRING = "string" +DPTYPE_ENUM = "enum" +DPTYPE_BITMASK = "bitmask" + +DATAPOINT_TYPES = { + DPTYPE_ANY: tuya_ns.struct("TuyaDatapoint"), + DPTYPE_RAW: cg.std_vector.template(cg.uint8), + DPTYPE_BOOL: cg.bool_, + DPTYPE_INT: cg.int_, + DPTYPE_UINT: cg.uint32, + DPTYPE_STRING: cg.std_string, + DPTYPE_ENUM: cg.uint8, + DPTYPE_BITMASK: cg.uint32, +} + +DATAPOINT_TRIGGERS = { + DPTYPE_ANY: tuya_ns.class_( + "TuyaDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ANY]), + ), + DPTYPE_RAW: tuya_ns.class_( + "TuyaRawDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_RAW]), + ), + DPTYPE_BOOL: tuya_ns.class_( + "TuyaBoolDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BOOL]), + ), + DPTYPE_INT: tuya_ns.class_( + "TuyaIntDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_INT]), + ), + DPTYPE_UINT: tuya_ns.class_( + "TuyaUIntDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_UINT]), + ), + DPTYPE_STRING: tuya_ns.class_( + "TuyaStringDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_STRING]), + ), + DPTYPE_ENUM: tuya_ns.class_( + "TuyaEnumDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ENUM]), + ), + DPTYPE_BITMASK: tuya_ns.class_( + "TuyaBitmaskDatapointUpdateTrigger", + automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BITMASK]), + ), +} -def to_code(config): +def assign_declare_id(value): + value = value.copy() + value[CONF_TRIGGER_ID] = cv.declare_id( + DATAPOINT_TRIGGERS[value[CONF_DATAPOINT_TYPE]] + )(value[CONF_TRIGGER_ID].id) + return value + + +CONF_TUYA_ID = "tuya_id" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Tuya), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list( + cv.uint8_t + ), + cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DATAPOINT_TRIGGERS[DPTYPE_ANY] + ), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_DATAPOINT_TYPE, default=DPTYPE_ANY): cv.one_of( + *DATAPOINT_TRIGGERS, lower=True + ), + }, + extra_validators=assign_declare_id, + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - 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) + if CONF_TIME_ID in config: + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time_id(time_)) + if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config: + for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]: + cg.add(var.add_ignore_mcu_update_on_datapoints(dp)) + for conf in config.get(CONF_ON_DATAPOINT_UPDATE, []): + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], var, conf[CONF_SENSOR_DATAPOINT] + ) + await automation.build_automation( + trigger, [(DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]], "x")], conf + ) diff --git a/esphome/components/tuya/automation.cpp b/esphome/components/tuya/automation.cpp new file mode 100644 index 0000000000..a8cfd098f1 --- /dev/null +++ b/esphome/components/tuya/automation.cpp @@ -0,0 +1,67 @@ +#include "esphome/core/log.h" + +#include "automation.h" + +static const char *const TAG = "tuya.automation"; + +namespace esphome { +namespace tuya { + +void check_expected_datapoint(const TuyaDatapoint &dp, TuyaDatapointType expected) { + if (dp.type != expected) { + ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", dp.id, + static_cast(expected), static_cast(dp.type)); + } +} + +TuyaRawDatapointUpdateTrigger::TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::RAW); + this->trigger(dp.value_raw); + }); +} + +TuyaBoolDatapointUpdateTrigger::TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::BOOLEAN); + this->trigger(dp.value_bool); + }); +} + +TuyaIntDatapointUpdateTrigger::TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::INTEGER); + this->trigger(dp.value_int); + }); +} + +TuyaUIntDatapointUpdateTrigger::TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::INTEGER); + this->trigger(dp.value_uint); + }); +} + +TuyaStringDatapointUpdateTrigger::TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::STRING); + this->trigger(dp.value_string); + }); +} + +TuyaEnumDatapointUpdateTrigger::TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::ENUM); + this->trigger(dp.value_enum); + }); +} + +TuyaBitmaskDatapointUpdateTrigger::TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { + check_expected_datapoint(dp, TuyaDatapointType::BITMASK); + this->trigger(dp.value_bitmask); + }); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/automation.h b/esphome/components/tuya/automation.h new file mode 100644 index 0000000000..d7706e1d60 --- /dev/null +++ b/esphome/components/tuya/automation.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "tuya.h" + +namespace esphome { +namespace tuya { + +class TuyaDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { + parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { this->trigger(dp); }); + } +}; + +class TuyaRawDatapointUpdateTrigger : public Trigger> { + public: + explicit TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +class TuyaBoolDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +class TuyaIntDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +class TuyaUIntDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +class TuyaStringDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +class TuyaEnumDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +class TuyaBitmaskDatapointUpdateTrigger : public Trigger { + public: + explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py new file mode 100644 index 0000000000..cd4a2db89f --- /dev/null +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +from esphome.components import binary_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@jesserockz"] + +TuyaBinarySensor = tuya_ns.class_( + "TuyaBinarySensor", binary_sensor.BinarySensor, cg.Component +) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaBinarySensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp new file mode 100644 index 0000000000..edfbb2ac60 --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp @@ -0,0 +1,22 @@ +#include "esphome/core/log.h" +#include "tuya_binary_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.binary_sensor"; + +void TuyaBinarySensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported binary sensor %u is: %s", datapoint.id, ONOFF(datapoint.value_bool)); + this->publish_state(datapoint.value_bool); + }); +} + +void TuyaBinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Binary Sensor:"); + ESP_LOGCONFIG(TAG, " Binary Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h new file mode 100644 index 0000000000..1eeeb40477 --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaBinarySensor : public binary_sensor::BinarySensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py new file mode 100644 index 0000000000..275a87edd3 --- /dev/null +++ b/esphome/components/tuya/climate/__init__.py @@ -0,0 +1,198 @@ +from esphome import pins +from esphome.components import climate +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_ID, + CONF_SWITCH_DATAPOINT, + CONF_SUPPORTS_COOL, + CONF_SUPPORTS_HEAT, +) +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@jesserockz"] + +CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint" +CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value" +CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value" +CONF_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) + + +def validate_temperature_multipliers(value): + if CONF_TEMPERATURE_MULTIPLIER in value: + if ( + CONF_CURRENT_TEMPERATURE_MULTIPLIER in value + or CONF_TARGET_TEMPERATURE_MULTIPLIER in value + ): + raise cv.Invalid( + ( + f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" + ) + ) + if ( + CONF_CURRENT_TEMPERATURE_MULTIPLIER in value + and CONF_TARGET_TEMPERATURE_MULTIPLIER not in value + ): + raise cv.Invalid( + ( + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER} required if using " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER}" + ) + ) + if ( + CONF_TARGET_TEMPERATURE_MULTIPLIER in value + and CONF_CURRENT_TEMPERATURE_MULTIPLIER not in value + ): + raise cv.Invalid( + ( + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} required if using " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" + ) + ) + keys = ( + CONF_TEMPERATURE_MULTIPLIER, + CONF_CURRENT_TEMPERATURE_MULTIPLIER, + CONF_TARGET_TEMPERATURE_MULTIPLIER, + ) + if all(multiplier not in value for multiplier in keys): + value[CONF_TEMPERATURE_MULTIPLIER] = 1.0 + return value + + +def validate_active_state_values(value): + if CONF_ACTIVE_STATE_DATAPOINT not in value: + if CONF_ACTIVE_STATE_COOLING_VALUE in value: + raise cv.Invalid( + ( + f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " + f"{CONF_ACTIVE_STATE_COOLING_VALUE}" + ) + ) + else: + if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value: + raise cv.Invalid( + ( + f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " + f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" + ) + ) + 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 + + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaClimate), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_ACTIVE_STATE_HEATING_VALUE, default=1): cv.uint8_t, + cv.Optional(CONF_ACTIVE_STATE_COOLING_VALUE): cv.uint8_t, + cv.Optional(CONF_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, +) + + +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_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_ACTIVE_STATE_DATAPOINT in config: + cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT])) + if CONF_ACTIVE_STATE_HEATING_VALUE in config: + cg.add( + var.set_active_state_heating_value( + config[CONF_ACTIVE_STATE_HEATING_VALUE] + ) + ) + if CONF_ACTIVE_STATE_COOLING_VALUE in config: + cg.add( + var.set_active_state_cooling_value( + config[CONF_ACTIVE_STATE_COOLING_VALUE] + ) + ) + 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: + cg.add( + var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT]) + ) + if CONF_TEMPERATURE_MULTIPLIER in config: + cg.add( + var.set_target_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) + ) + cg.add( + var.set_current_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) + ) + else: + cg.add( + var.set_current_temperature_multiplier( + config[CONF_CURRENT_TEMPERATURE_MULTIPLIER] + ) + ) + cg.add( + var.set_target_temperature_multiplier( + config[CONF_TARGET_TEMPERATURE_MULTIPLIER] + ) + ) + if CONF_ECO_DATAPOINT in config: + cg.add(var.set_eco_id(config[CONF_ECO_DATAPOINT])) + if CONF_ECO_TEMPERATURE in config: + cg.add(var.set_eco_temperature(config[CONF_ECO_TEMPERATURE])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp new file mode 100644 index 0000000000..39d4203684 --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -0,0 +1,221 @@ +#include "esphome/core/log.h" +#include "tuya_climate.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.climate"; + +void TuyaClimate::setup() { + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + this->mode = climate::CLIMATE_MODE_OFF; + if (datapoint.value_bool) { + if (this->supports_heat_ && this->supports_cool_) { + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + } else if (this->supports_heat_) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else if (this->supports_cool_) { + this->mode = climate::CLIMATE_MODE_COOL; + } + } + this->compute_state_(); + this->publish_state(); + }); + } + if (this->active_state_id_.has_value()) { + this->parent_->register_listener(*this->active_state_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum); + this->active_state_ = datapoint.value_enum; + 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->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(); + }); + } + if (this->current_temperature_id_.has_value()) { + this->parent_->register_listener(*this->current_temperature_id_, [this](const TuyaDatapoint &datapoint) { + this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_; + ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature); + this->compute_state_(); + 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_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_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_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; +} + +void TuyaClimate::dump_config() { + LOG_CLIMATE("", "Tuya Climate", this); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->active_state_id_.has_value()) + ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *this->active_state_id_); + if (this->target_temperature_id_.has_value()) + 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 (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; + } + + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + 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; + } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() && + this->active_state_ == this->active_state_cooling_value_) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + } else if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) { + // Use state from input pins + if (this->heating_state_) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else if (this->cooling_state_) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + } else { + // Fallback to active state calc based on temp and hysteresis + const float temp_diff = this->target_temperature - this->current_temperature; + if (std::abs(temp_diff) > this->hysteresis_) { + if (this->supports_heat_ && temp_diff > 0) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else if (this->supports_cool_ && temp_diff < 0) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + } + } + + this->switch_to_action_(target_action); +} + +void TuyaClimate::switch_to_action_(climate::ClimateAction action) { + // For now this just sets the current action but could include triggers later + this->action = action; +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h new file mode 100644 index 0000000000..ec19d05308 --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -0,0 +1,83 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +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; } + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + 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; + } + void set_current_temperature_id(uint8_t current_temperature_id) { + this->current_temperature_id_ = current_temperature_id; + } + void set_current_temperature_multiplier(float temperature_multiplier) { + this->current_temperature_multiplier_ = temperature_multiplier; + } + 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_(); + + /// Switch the climate device to the given climate mode. + void switch_to_action_(climate::ClimateAction action); + + Tuya *parent_; + bool supports_heat_; + bool supports_cool_; + optional switch_id_{}; + 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 +} // namespace esphome diff --git a/esphome/components/tuya/cover/__init__.py b/esphome/components/tuya/cover/__init__.py new file mode 100644 index 0000000000..f886c7030f --- /dev/null +++ b/esphome/components/tuya/cover/__init__.py @@ -0,0 +1,76 @@ +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, + CONF_RESTORE_MODE, +) +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] + +CONF_CONTROL_DATAPOINT = "control_datapoint" +CONF_DIRECTION_DATAPOINT = "direction_datapoint" +CONF_POSITION_DATAPOINT = "position_datapoint" +CONF_POSITION_REPORT_DATAPOINT = "position_report_datapoint" +CONF_INVERT_POSITION = "invert_position" + +TuyaCover = tuya_ns.class_("TuyaCover", cover.Cover, cg.Component) + +TuyaCoverRestoreMode = tuya_ns.enum("TuyaCoverRestoreMode") +RESTORE_MODES = { + "NO_RESTORE": TuyaCoverRestoreMode.COVER_NO_RESTORE, + "RESTORE": TuyaCoverRestoreMode.COVER_RESTORE, + "RESTORE_AND_CALL": TuyaCoverRestoreMode.COVER_RESTORE_AND_CALL, +} + + +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.Optional(CONF_CONTROL_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, + cv.Required(CONF_POSITION_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_POSITION_REPORT_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, + cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + }, + ).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) + + if CONF_CONTROL_DATAPOINT in config: + cg.add(var.set_control_id(config[CONF_CONTROL_DATAPOINT])) + if CONF_DIRECTION_DATAPOINT in config: + cg.add(var.set_direction_id(config[CONF_DIRECTION_DATAPOINT])) + cg.add(var.set_position_id(config[CONF_POSITION_DATAPOINT])) + if CONF_POSITION_REPORT_DATAPOINT in config: + cg.add(var.set_position_report_id(config[CONF_POSITION_REPORT_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])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp new file mode 100644 index 0000000000..b63eb9109d --- /dev/null +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -0,0 +1,132 @@ +#include "esphome/core/log.h" +#include "tuya_cover.h" + +namespace esphome { +namespace tuya { + +const uint8_t COMMAND_OPEN = 0x00; +const uint8_t COMMAND_CLOSE = 0x02; +const uint8_t COMMAND_STOP = 0x01; + +using namespace esphome::cover; + +static const char *const TAG = "tuya.cover"; + +void TuyaCover::setup() { + this->value_range_ = this->max_value_ - this->min_value_; + + this->parent_->add_on_initialized_callback([this]() { + // Set the direction (if configured/supported). + this->set_direction_(this->invert_position_); + + // Handle configured restore mode. + switch (this->restore_mode_) { + case COVER_NO_RESTORE: + break; + case COVER_RESTORE: { + auto restore = this->restore_state_(); + if (restore.has_value()) + restore->apply(this); + break; + } + case COVER_RESTORE_AND_CALL: { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } + break; + } + } + }); + + uint8_t report_id = *this->position_id_; + if (this->position_report_id_.has_value()) { + // A position report datapoint is configured; listen to that instead. + report_id = *this->position_report_id_; + } + + this->parent_->register_listener(report_id, [this](const TuyaDatapoint &datapoint) { + if (datapoint.value_int == 123) { + ESP_LOGD(TAG, "Ignoring MCU position report - not calibrated"); + return; + } + auto pos = float(datapoint.value_uint - this->min_value_) / this->value_range_; + this->position = 1.0f - pos; + this->publish_state(); + }); +} + +void TuyaCover::control(const cover::CoverCall &call) { + if (call.get_stop()) { + if (this->control_id_.has_value()) { + this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_STOP); + } else { + auto pos = this->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->control_id_.has_value() && (pos == COVER_OPEN || pos == COVER_CLOSED)) { + if (pos == COVER_OPEN) { + this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_OPEN); + } else { + this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_CLOSE); + } + } else { + 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::set_direction_(bool inverted) { + if (!this->direction_id_.has_value()) { + return; + } + + if (inverted) { + ESP_LOGD(TAG, "Setting direction: inverted"); + } else { + ESP_LOGD(TAG, "Setting direction: normal"); + } + + this->parent_->set_boolean_datapoint_value(*this->direction_id_, inverted); +} + +void TuyaCover::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Cover:"); + if (this->invert_position_) { + if (this->direction_id_.has_value()) { + ESP_LOGCONFIG(TAG, " Inverted"); + } else { + ESP_LOGCONFIG(TAG, " Configured as Inverted, but direction_datapoint isn't configured"); + } + } + if (this->control_id_.has_value()) + ESP_LOGCONFIG(TAG, " Control has datapoint ID %u", *this->control_id_); + if (this->direction_id_.has_value()) + ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); + if (this->position_id_.has_value()) + ESP_LOGCONFIG(TAG, " Position has datapoint ID %u", *this->position_id_); + if (this->position_report_id_.has_value()) + ESP_LOGCONFIG(TAG, " Position Report has datapoint ID %u", *this->position_report_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..87c72b0e66 --- /dev/null +++ b/esphome/components/tuya/cover/tuya_cover.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace tuya { + +enum TuyaCoverRestoreMode { + COVER_NO_RESTORE, + COVER_RESTORE, + COVER_RESTORE_AND_CALL, +}; + +class TuyaCover : public cover::Cover, public Component { + public: + void setup() override; + void dump_config() override; + void set_control_id(uint8_t control_id) { this->control_id_ = control_id; } + void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; } + void set_position_id(uint8_t position_id) { this->position_id_ = position_id; } + void set_position_report_id(uint8_t position_report_id) { this->position_report_id_ = position_report_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; } + void set_restore_mode(TuyaCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } + + protected: + void control(const cover::CoverCall &call) override; + void set_direction_(bool inverted); + cover::CoverTraits get_traits() override; + + Tuya *parent_; + TuyaCoverRestoreMode restore_mode_{}; + optional control_id_{}; + optional direction_id_{}; + optional position_id_{}; + optional position_report_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/__init__.py b/esphome/components/tuya/fan/__init__.py index 8b4a0fa25f..6d660e6d29 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,35 +1,41 @@ from esphome.components import fan import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID +from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya -DEPENDENCIES = ['tuya'] +DEPENDENCIES = ["tuya"] CONF_SPEED_DATAPOINT = "speed_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" +CONF_DIRECTION_DATAPOINT = "direction_datapoint" -TuyaFan = tuya_ns.class_('TuyaFan', cg.Component) +TuyaFan = tuya_ns.class_("TuyaFan", cg.Component) -CONFIG_SCHEMA = cv.All(fan.FAN_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( - CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT)) +CONFIG_SCHEMA = cv.All( + fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), +) -def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) +async def to_code(config): + parent = await cg.get_variable(config[CONF_TUYA_ID]) + state = await fan.create_fan_state(config) - paren = yield cg.get_variable(config[CONF_TUYA_ID]) - fan_ = yield fan.create_fan_state(config) - cg.add(var.set_tuya_parent(paren)) - cg.add(var.set_fan(fan_)) + var = cg.new_Pvariable( + config[CONF_OUTPUT_ID], parent, state, config[CONF_SPEED_COUNT] + ) + await cg.register_component(var, config) if CONF_SPEED_DATAPOINT in config: cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT])) @@ -37,3 +43,5 @@ def to_code(config): cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) if CONF_OSCILLATION_DATAPOINT in config: cg.add(var.set_oscillation_id(config[CONF_OSCILLATION_DATAPOINT])) + if CONF_DIRECTION_DATAPOINT in config: + cg.add(var.set_direction_id(config[CONF_DIRECTION_DATAPOINT])) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index b9fe2c0829..d0c8809564 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -1,90 +1,92 @@ #include "esphome/core/log.h" +#include "esphome/components/fan/fan_helpers.h" #include "tuya_fan.h" namespace esphome { namespace tuya { -static const char *TAG = "tuya.fan"; +static const char *const TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value()); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), + this->direction_id_.has_value(), this->speed_count_); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { - this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) { + this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); auto call = this->fan_->make_call(); - if (datapoint.value_enum == 0x0) - call.set_speed(fan::FAN_SPEED_LOW); - else if (datapoint.value_enum == 0x1) - call.set_speed(fan::FAN_SPEED_MEDIUM); - else if (datapoint.value_enum == 0x2) - call.set_speed(fan::FAN_SPEED_HIGH); + if (datapoint.value_enum < this->speed_count_) + call.set_speed(datapoint.value_enum + 1); else ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum); - ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum); call.perform(); }); } if (this->switch_id_.has_value()) { - this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); auto call = this->fan_->make_call(); call.set_state(datapoint.value_bool); call.perform(); - ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); }); } if (this->oscillation_id_.has_value()) { - this->parent_->register_listener(*this->oscillation_id_, [this](TuyaDatapoint datapoint) { + this->parent_->register_listener(*this->oscillation_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); auto call = this->fan_->make_call(); call.set_oscillating(datapoint.value_bool); call.perform(); - ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); }); } + if (this->direction_id_.has_value()) { + this->parent_->register_listener(*this->direction_id_, [this](const TuyaDatapoint &datapoint) { + auto call = this->fan_->make_call(); + call.set_direction(datapoint.value_bool ? fan::FAN_DIRECTION_REVERSE : fan::FAN_DIRECTION_FORWARD); + call.perform(); + ESP_LOGD(TAG, "MCU reported reverse direction is: %s", ONOFF(datapoint.value_bool)); + }); + } + this->fan_->add_on_state_callback([this]() { this->write_state(); }); } void TuyaFan::dump_config() { ESP_LOGCONFIG(TAG, "Tuya Fan:"); + ESP_LOGCONFIG(TAG, " Speed count %d", this->speed_count_); if (this->speed_id_.has_value()) ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); if (this->switch_id_.has_value()) ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); if (this->oscillation_id_.has_value()) ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); + if (this->direction_id_.has_value()) + ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); } void TuyaFan::write_state() { if (this->switch_id_.has_value()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->switch_id_; - datapoint.type = TuyaDatapointType::BOOLEAN; - datapoint.value_bool = this->fan_->state; - this->parent_->set_datapoint_value(datapoint); - ESP_LOGD(TAG, "Setting switch: %s", ONOFF(this->fan_->state)); + ESP_LOGV(TAG, "Setting switch: %s", ONOFF(this->fan_->state)); + this->parent_->set_boolean_datapoint_value(*this->switch_id_, this->fan_->state); } if (this->oscillation_id_.has_value()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->oscillation_id_; - datapoint.type = TuyaDatapointType::BOOLEAN; - datapoint.value_bool = this->fan_->oscillating; - this->parent_->set_datapoint_value(datapoint); - ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating)); + ESP_LOGV(TAG, "Setting oscillating: %s", ONOFF(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_enum_datapoint_value(*this->direction_id_, enable); } if (this->speed_id_.has_value()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->speed_id_; - datapoint.type = TuyaDatapointType::ENUM; - if (this->fan_->speed == fan::FAN_SPEED_LOW) - datapoint.value_enum = 0; - if (this->fan_->speed == fan::FAN_SPEED_MEDIUM) - datapoint.value_enum = 1; - if (this->fan_->speed == fan::FAN_SPEED_HIGH) - datapoint.value_enum = 2; - ESP_LOGD(TAG, "Setting speed: %d", datapoint.value_enum); - this->parent_->set_datapoint_value(datapoint); + ESP_LOGV(TAG, "Setting speed: %d", 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 d31d490e1a..e96770d8c3 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -9,25 +9,29 @@ namespace tuya { 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; } void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } - void set_fan(fan::FanState *fan) { this->fan_ = fan; } - void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; } void write_state(); protected: void update_speed_(uint32_t value); void update_switch_(uint32_t value); void update_oscillation_(uint32_t value); + void update_direction_(uint32_t value); Tuya *parent_; optional speed_id_{}; optional switch_id_{}; optional oscillation_id_{}; + optional direction_id_{}; fan::FanState *fan_; + int speed_count_{}; }; } // namespace tuya diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index adaeb52531..b983e3f84e 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -1,45 +1,108 @@ from esphome.components import light import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ - CONF_DEFAULT_TRANSITION_LENGTH +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_MIN_VALUE, + CONF_MAX_VALUE, + CONF_GAMMA_CORRECT, + CONF_DEFAULT_TRANSITION_LENGTH, + CONF_SWITCH_DATAPOINT, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, + CONF_COLOR_INTERLOCK, +) from .. import tuya_ns, CONF_TUYA_ID, Tuya -DEPENDENCIES = ['tuya'] +DEPENDENCIES = ["tuya"] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" -CONF_SWITCH_DATAPOINT = "switch_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) +TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) -CONFIG_SCHEMA = cv.All(light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_MIN_VALUE): cv.int_, - cv.Optional(CONF_MAX_VALUE): cv.int_, - - # Change the default gamma_correct and default transition length settings. - # The Tuya MCU handles transitions and gamma correction on its own. - cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, - cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds, -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_DIMMER_DATAPOINT, - CONF_SWITCH_DATAPOINT)) +CONFIG_SCHEMA = cv.All( + light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + 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_, + cv.Inclusive( + CONF_COLD_WHITE_COLOR_TEMPERATURE, "color_temperature" + ): cv.color_temperature, + cv.Inclusive( + CONF_WARM_WHITE_COLOR_TEMPERATURE, "color_temperature" + ): cv.color_temperature, + # Change the default gamma_correct and default transition length settings. + # The Tuya MCU handles transitions and gamma correction on its own. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + cv.Optional( + CONF_DEFAULT_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_at_least_one_key( + CONF_DIMMER_DATAPOINT, + CONF_SWITCH_DATAPOINT, + CONF_RGB_DATAPOINT, + CONF_HSV_DATAPOINT, + ), +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) - yield light.register_light(var, config) + await cg.register_component(var, config) + await light.register_light(var, config) if CONF_DIMMER_DATAPOINT in config: cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT])) + if CONF_MIN_VALUE_DATAPOINT in 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]) + ) + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) 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])) - paren = yield cg.get_variable(config[CONF_TUYA_ID]) + if CONF_COLOR_TEMPERATURE_MAX_VALUE in config: + cg.add( + var.set_color_temperature_max_value( + 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 9696252049..ecd3802839 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -1,26 +1,68 @@ #include "esphome/core/log.h" #include "tuya_light.h" +#include "esphome/core/helpers.h" namespace esphome { namespace tuya { -static const char *TAG = "tuya.light"; +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) / this->color_temperature_max_value_)); + call.perform(); + }); + } if (this->dimmer_id_.has_value()) { - this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) { + this->parent_->register_listener(*this->dimmer_id_, [this](const TuyaDatapoint &datapoint) { auto call = this->state_->make_call(); call.set_brightness(float(datapoint.value_uint) / this->max_value_); call.perform(); }); } if (switch_id_.has_value()) { - this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { auto call = this->state_->make_call(); call.set_state(datapoint.value_bool); 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.substr(0, 2)); + auto green = parse_hex(datapoint.value_string.substr(2, 2)); + auto blue = parse_hex(datapoint.value_string.substr(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.substr(0, 4)); + auto saturation = parse_hex(datapoint.value_string.substr(4, 4)); + auto value = parse_hex(datapoint.value_string.substr(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_integer_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); + } } void TuyaLight::dump_config() { @@ -29,55 +71,102 @@ 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()); + 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; } 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()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->switch_id_; - datapoint.type = TuyaDatapointType::BOOLEAN; - datapoint.value_bool = false; - - parent_->set_datapoint_value(datapoint); - } else if (dimmer_id_.has_value()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->dimmer_id_; - datapoint.type = TuyaDatapointType::INTEGER; - datapoint.value_int = 0; - parent_->set_datapoint_value(datapoint); + 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; } - auto brightness_int = static_cast(brightness * this->max_value_); - brightness_int = std::max(brightness_int, this->min_value_); + 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()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->dimmer_id_; - datapoint.type = TuyaDatapointType::INTEGER; - datapoint.value_int = brightness_int; - parent_->set_datapoint_value(datapoint); + 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); + } } + + 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()) { - TuyaDatapoint datapoint{}; - datapoint.id = *this->switch_id_; - datapoint.type = TuyaDatapointType::BOOLEAN; - datapoint.value_bool = true; - parent_->set_datapoint_value(datapoint); + 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 581512c29c..3d9f25271c 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -12,10 +12,30 @@ class TuyaLight : public Component, public light::LightOutput { void setup() override; void dump_config() override; void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } + void set_min_value_datapoint_id(uint8_t min_value_datapoint_id) { + 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; } + void set_color_temperature_max_value(uint32_t color_temperature_max_value) { + this->color_temperature_max_value_ = color_temperature_max_value; + } + void set_cold_white_temperature(float cold_white_temperature) { + this->cold_white_temperature_ = cold_white_temperature; + } + 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; @@ -26,9 +46,18 @@ class TuyaLight : public Component, public light::LightOutput { Tuya *parent_; 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/number/__init__.py b/esphome/components/tuya/number/__init__.py new file mode 100644 index 0000000000..12c0c0f6e5 --- /dev/null +++ b/esphome/components/tuya/number/__init__.py @@ -0,0 +1,54 @@ +from esphome.components import number +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_ID, + CONF_NUMBER_DATAPOINT, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_STEP, +) +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@frankiboy1"] + +TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component) + + +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 + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaNumber), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_NUMBER_DATAPOINT): cv.uint8_t, + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.positive_float, + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, +) + + +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], + ) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp new file mode 100644 index 0000000000..5c7cafbf7a --- /dev/null +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -0,0 +1,38 @@ +#include "esphome/core/log.h" +#include "tuya_number.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.number"; + +void TuyaNumber::setup() { + this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { + if (datapoint.type == TuyaDatapointType::INTEGER) { + ESP_LOGV(TAG, "MCU reported number %u is: %d", datapoint.id, datapoint.value_int); + this->publish_state(datapoint.value_int); + } else if (datapoint.type == TuyaDatapointType::ENUM) { + ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); + this->publish_state(datapoint.value_enum); + } + this->type_ = datapoint.type; + }); +} + +void TuyaNumber::control(float value) { + ESP_LOGV(TAG, "Setting number %u: %f", this->number_id_, value); + if (this->type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(this->number_id_, value); + } else if (this->type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(this->number_id_, value); + } + this->publish_state(value); +} + +void TuyaNumber::dump_config() { + LOG_NUMBER("", "Tuya Number", this); + ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h new file mode 100644 index 0000000000..7cca9fc646 --- /dev/null +++ b/esphome/components/tuya/number/tuya_number.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace tuya { + +class TuyaNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + void control(float value) override; + + Tuya *parent_; + uint8_t number_id_{0}; + TuyaDatapointType type_{}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py new file mode 100644 index 0000000000..441400fa43 --- /dev/null +++ b/esphome/components/tuya/sensor/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@jesserockz"] + +TuyaSensor = tuya_ns.class_("TuyaSensor", sensor.Sensor, cg.Component) + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaSensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } +).extend(cv.COMPONENT_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) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp new file mode 100644 index 0000000000..1e39c1bc35 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/log.h" +#include "tuya_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.sensor"; + +void TuyaSensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](const TuyaDatapoint &datapoint) { + if (datapoint.type == TuyaDatapointType::BOOLEAN) { + ESP_LOGV(TAG, "MCU reported sensor %u is: %s", datapoint.id, ONOFF(datapoint.value_bool)); + this->publish_state(datapoint.value_bool); + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + ESP_LOGV(TAG, "MCU reported sensor %u is: %d", datapoint.id, datapoint.value_int); + this->publish_state(datapoint.value_int); + } else if (datapoint.type == TuyaDatapointType::ENUM) { + ESP_LOGV(TAG, "MCU reported sensor %u is: %u", datapoint.id, datapoint.value_enum); + this->publish_state(datapoint.value_enum); + } else if (datapoint.type == TuyaDatapointType::BITMASK) { + ESP_LOGV(TAG, "MCU reported sensor %u is: %x", datapoint.id, datapoint.value_bitmask); + this->publish_state(datapoint.value_bitmask); + } + }); +} + +void TuyaSensor::dump_config() { + LOG_SENSOR("", "Tuya Sensor", this); + ESP_LOGCONFIG(TAG, " Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/sensor/tuya_sensor.h b/esphome/components/tuya/sensor/tuya_sensor.h new file mode 100644 index 0000000000..8fd7cd1770 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py new file mode 100644 index 0000000000..4df6bba713 --- /dev/null +++ b/esphome/components/tuya/switch/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@jesserockz"] + +TuyaSwitch = tuya_ns.class_("TuyaSwitch", switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaSwitch), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SWITCH_DATAPOINT): cv.uint8_t, + } +).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) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp new file mode 100644 index 0000000000..cbd794b001 --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -0,0 +1,28 @@ +#include "esphome/core/log.h" +#include "tuya_switch.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.switch"; + +void TuyaSwitch::setup() { + this->parent_->register_listener(this->switch_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported switch %u is: %s", this->switch_id_, ONOFF(datapoint.value_bool)); + this->publish_state(datapoint.value_bool); + }); +} + +void TuyaSwitch::write_state(bool state) { + ESP_LOGV(TAG, "Setting switch %u: %s", this->switch_id_, ONOFF(state)); + this->parent_->set_boolean_datapoint_value(this->switch_id_, state); + this->publish_state(state); +} + +void TuyaSwitch::dump_config() { + LOG_SWITCH("", "Tuya Switch", this); + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", this->switch_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/tuya_switch.h b/esphome/components/tuya/switch/tuya_switch.h new file mode 100644 index 0000000000..89e6264e5c --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace tuya { + +class TuyaSwitch : public switch_::Switch, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + void write_state(bool state) override; + + Tuya *parent_; + uint8_t switch_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/text_sensor/__init__.py b/esphome/components/tuya/text_sensor/__init__.py new file mode 100644 index 0000000000..1989ca10e3 --- /dev/null +++ b/esphome/components/tuya/text_sensor/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@dentra"] + +TuyaTextSensor = tuya_ns.class_("TuyaTextSensor", text_sensor.TextSensor, cg.Component) + +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaTextSensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp new file mode 100644 index 0000000000..0b51ba90c4 --- /dev/null +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -0,0 +1,35 @@ +#include "esphome/core/log.h" +#include "tuya_text_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.text_sensor"; + +void TuyaTextSensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](const TuyaDatapoint &datapoint) { + switch (datapoint.type) { + case TuyaDatapointType::STRING: + ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, datapoint.value_string.c_str()); + this->publish_state(datapoint.value_string); + break; + case TuyaDatapointType::RAW: { + std::string data = format_hex_pretty(datapoint.value_raw); + ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, data.c_str()); + this->publish_state(data); + break; + } + default: + ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, datapoint.type); + break; + } + }); +} + +void TuyaTextSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Text Sensor:"); + ESP_LOGCONFIG(TAG, " Text Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.h b/esphome/components/tuya/text_sensor/tuya_text_sensor.h new file mode 100644 index 0000000000..502ae5e8c7 --- /dev/null +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaTextSensor : public text_sensor::TextSensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index b21df81d1e..7ff8c66c44 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,14 +1,18 @@ #include "tuya.h" #include "esphome/core/log.h" +#include "esphome/components/network/util.h" #include "esphome/core/helpers.h" +#include "esphome/core/util.h" namespace esphome { namespace tuya { -static const char *TAG = "tuya"; +static const char *const TAG = "tuya"; +static const int COMMAND_DELAY = 10; +static const int RECEIVE_TIMEOUT = 300; void Tuya::setup() { - this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); } void Tuya::loop() { @@ -17,27 +21,32 @@ void Tuya::loop() { this->read_byte(&c); this->handle_char_(c); } + process_command_queue_(); } void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { - ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", // NOLINT - this->init_state_); + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", + static_cast(this->init_state_)); ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); return; } for (auto &info : this->datapoints_) { - if (info.type == TuyaDatapointType::BOOLEAN) - ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); + if (info.type == TuyaDatapointType::RAW) + ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, format_hex_pretty(info.value_raw).c_str()); + else if (info.type == TuyaDatapointType::BOOLEAN) + ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool)); else if (info.type == TuyaDatapointType::INTEGER) - ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int); + ESP_LOGCONFIG(TAG, " Datapoint %u: int value (value: %d)", info.id, info.value_int); + else if (info.type == TuyaDatapointType::STRING) + ESP_LOGCONFIG(TAG, " Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str()); else if (info.type == TuyaDatapointType::ENUM) - ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum); + ESP_LOGCONFIG(TAG, " Datapoint %u: enum (value: %d)", info.id, info.value_enum); else if (info.type == TuyaDatapointType::BITMASK) - ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask); + ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %x)", info.id, info.value_bitmask); else - ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); + ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id); } if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) { ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_, @@ -94,8 +103,8 @@ bool Tuya::validate_message_() { // valid message const uint8_t *message_data = data + 6; - ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT - hexencode(message_data, length).c_str(), this->init_state_); + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, + format_hex_pretty(message_data, length).c_str(), static_cast(this->init_state_)); this->handle_command_(command, version, message_data, length); // return false to reset rx buffer @@ -106,13 +115,22 @@ 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; if (buffer[0] == 0) { ESP_LOGI(TAG, "MCU restarted"); this->init_state_ = TuyaInitState::INIT_HEARTBEAT; @@ -125,7 +143,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff case TuyaCommandType::PRODUCT_QUERY: { // check it is a valid string made up of printable characters bool valid = true; - for (int i = 0; i < len; i++) { + for (size_t i = 0; i < len; i++) { if (!std::isprint(buffer[i])) { valid = false; break; @@ -144,16 +162,18 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } case TuyaCommandType::CONF_QUERY: { if (len >= 2) { - gpio_status_ = buffer[0]; - gpio_reset_ = buffer[1]; + this->gpio_status_ = buffer[0]; + this->gpio_reset_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { - // If we were following the spec to the letter we would send - // state updates until connected to both WiFi and API/MQTT. - // Instead we just claim to be connected immediately and move on. - uint8_t c[] = {0x04}; - this->init_state_ = TuyaInitState::INIT_WIFI; - this->send_command_(TuyaCommandType::WIFI_STATE, c, 1); + // 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); + } else { + this->init_state_ = TuyaInitState::INIT_WIFI; + this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); }); + } } break; } @@ -164,10 +184,10 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } break; case TuyaCommandType::WIFI_RESET: - ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); + ESP_LOGE(TAG, "WIFI_RESET is not handled"); break; case TuyaCommandType::WIFI_SELECT: - ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); + ESP_LOGE(TAG, "WIFI_SELECT is not handled"); break; case TuyaCommandType::DATAPOINT_DELIVER: break; @@ -175,18 +195,30 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff if (this->init_state_ == TuyaInitState::INIT_DATAPOINT) { this->init_state_ = TuyaInitState::INIT_DONE; this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); }); + this->initialized_callback_.call(); } this->handle_datapoint_(buffer, len); break; case TuyaCommandType::DATAPOINT_QUERY: break; - case TuyaCommandType::WIFI_TEST: { - uint8_t c[] = {0x00, 0x00}; - this->send_command_(TuyaCommandType::WIFI_TEST, c, 2); + case TuyaCommandType::WIFI_TEST: + this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector{0x00, 0x00}}); + break; + case TuyaCommandType::LOCAL_TIME_QUERY: +#ifdef USE_TIME + if (this->time_id_.has_value()) { + this->send_local_time_(); + auto time_id = *this->time_id_; + time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); + } else { + ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured"); + } +#else + ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); +#endif break; - } default: - ESP_LOGE(TAG, "invalid command (%02x) received", command); + ESP_LOGE(TAG, "Invalid command (0x%02X) received", command); } } @@ -199,40 +231,79 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { datapoint.type = (TuyaDatapointType) buffer[1]; datapoint.value_uint = 0; + // Drop update if datapoint is in ignore_mcu_datapoint_update list + for (uint8_t i : this->ignore_mcu_update_on_datapoints_) { + if (datapoint.id == i) { + ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id); + return; + } + } + 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, "invalid datapoint update"); + 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; switch (datapoint.type) { + case TuyaDatapointType::RAW: + datapoint.value_raw = std::vector(data, data + data_len); + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, format_hex_pretty(datapoint.value_raw).c_str()); + break; case TuyaDatapointType::BOOLEAN: - if (data_len != 1) + if (data_len != 1) { + ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_len); return; + } datapoint.value_bool = data[0]; + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool)); break; case TuyaDatapointType::INTEGER: - if (data_len != 4) + if (data_len != 4) { + ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_len); return; - datapoint.value_uint = - (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0); + } + datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]); + ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_int); + break; + case TuyaDatapointType::STRING: + datapoint.value_string = std::string(reinterpret_cast(data), data_len); + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, datapoint.value_string.c_str()); break; case TuyaDatapointType::ENUM: - if (data_len != 1) + if (data_len != 1) { + ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_len); return; + } datapoint.value_enum = data[0]; + ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_enum); break; case TuyaDatapointType::BITMASK: - if (data_len != 2) - return; - datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0); + switch (data_len) { + case 1: + datapoint.value_bitmask = encode_uint32(0, 0, 0, data[0]); + break; + case 2: + datapoint.value_bitmask = encode_uint32(0, 0, data[0], data[1]); + break; + case 4: + datapoint.value_bitmask = encode_uint32(data[0], data[1], data[2], data[3]); + break; + default: + ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_len); + return; + } + 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, static_cast(datapoint.type)); return; } - ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint); // Update internal datapoints bool found = false; @@ -252,64 +323,251 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { listener.on_datapoint(datapoint); } -void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { - uint8_t len_hi = len >> 8; - uint8_t len_lo = len >> 0; +void Tuya::send_raw_command_(TuyaCommand command) { + uint8_t len_hi = (uint8_t)(command.payload.size() >> 8); + uint8_t len_lo = (uint8_t)(command.payload.size() & 0xFF); uint8_t version = 0; - ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT - hexencode(buffer, len).c_str(), this->init_state_); + 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: + case TuyaCommandType::DATAPOINT_QUERY: + this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; + break; + default: + break; + } - this->write_array({0x55, 0xAA, version, (uint8_t) command, len_hi, len_lo}); - if (len != 0) - this->write_array(buffer, len); + ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), + version, format_hex_pretty(command.payload).c_str(), static_cast(this->init_state_)); - uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo; - for (int i = 0; i < len; i++) - checksum += buffer[i]; + this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo}); + if (!command.payload.empty()) + this->write_array(command.payload.data(), command.payload.size()); + + uint8_t checksum = 0x55 + 0xAA + (uint8_t) command.cmd + len_hi + len_lo; + for (auto &data : command.payload) + checksum += data; this->write_byte(checksum); } -void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { - std::vector buffer; - ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint); - for (auto &other : this->datapoints_) { - if (other.id == datapoint.id) { - if (other.value_uint == datapoint.value_uint) { - ESP_LOGV(TAG, "Not sending unchanged value"); - return; +void Tuya::process_command_queue_() { + 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()); + } +} + +void Tuya::send_command_(const TuyaCommand &command) { + command_queue_.push_back(command); + process_command_queue_(); +} + +void Tuya::send_empty_command_(TuyaCommandType command) { + send_command_(TuyaCommand{.cmd = command, .payload = std::vector{}}); +} + +void Tuya::send_wifi_status_() { + uint8_t status = 0x02; + if (network::is_connected()) { + status = 0x03; + + // Protocol version 3 also supports specifying when connected to "the cloud" + if (this->protocol_version_ >= 0x03) { + if (remote_is_connected()) { + status = 0x04; } } } - buffer.push_back(datapoint.id); - buffer.push_back(static_cast(datapoint.type)); - std::vector data; - switch (datapoint.type) { - case TuyaDatapointType::BOOLEAN: - data.push_back(datapoint.value_bool); - break; - case TuyaDatapointType::INTEGER: - data.push_back(datapoint.value_uint >> 24); - data.push_back(datapoint.value_uint >> 16); - data.push_back(datapoint.value_uint >> 8); - data.push_back(datapoint.value_uint >> 0); - break; - case TuyaDatapointType::ENUM: - data.push_back(datapoint.value_enum); - break; - case TuyaDatapointType::BITMASK: - data.push_back(datapoint.value_bitmask >> 8); - data.push_back(datapoint.value_bitmask >> 0); - break; - default: - return; + if (status == this->wifi_status_) { + return; } + ESP_LOGD(TAG, "Sending WiFi Status"); + this->wifi_status_ = status; + this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_STATE, .payload = std::vector{status}}); +} + +#ifdef USE_TIME +void Tuya::send_local_time_() { + std::vector payload; + auto time_id = *this->time_id_; + time::ESPTime now = time_id->now(); + if (now.is_valid()) { + uint8_t year = now.year - 2000; + uint8_t month = now.month; + uint8_t day_of_month = now.day_of_month; + uint8_t hour = now.hour; + uint8_t minute = now.minute; + uint8_t second = now.second; + // Tuya days starts from Monday, esphome uses Sunday as day 1 + uint8_t day_of_week = now.day_of_week - 1; + if (day_of_week == 0) { + day_of_week = 7; + } + ESP_LOGD(TAG, "Sending local time"); + payload = std::vector{0x01, year, month, day_of_month, hour, minute, second, day_of_week}; + } else { + // By spec we need to notify MCU that the time was not obtained if this is a response to a query + ESP_LOGW(TAG, "Sending missing local time"); + payload = std::vector{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + this->send_command_(TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY, .payload = payload}); +} +#endif + +void Tuya::set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value) { + this->set_raw_datapoint_value_(datapoint_id, value, false); +} + +void Tuya::set_boolean_datapoint_value(uint8_t datapoint_id, bool value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1, false); +} + +void Tuya::set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4, false); +} + +void Tuya::set_string_datapoint_value(uint8_t datapoint_id, const std::string &value) { + this->set_string_datapoint_value_(datapoint_id, value, false); +} + +void Tuya::set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1, false); +} + +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, false); +} + +void Tuya::force_set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value) { + this->set_raw_datapoint_value_(datapoint_id, value, true); +} + +void Tuya::force_set_boolean_datapoint_value(uint8_t datapoint_id, bool value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1, true); +} + +void Tuya::force_set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4, true); +} + +void Tuya::force_set_string_datapoint_value(uint8_t datapoint_id, const std::string &value) { + this->set_string_datapoint_value_(datapoint_id, value, true); +} + +void Tuya::force_set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1, true); +} + +void Tuya::force_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, true); +} + +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, bool forced) { + ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value); + optional datapoint = this->get_datapoint_(datapoint_id); + if (!datapoint.has_value()) { + 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; + } else if (!forced && datapoint->value_uint == value) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + + std::vector data; + switch (length) { + case 4: + data.push_back(value >> 24); + data.push_back(value >> 16); + case 2: + data.push_back(value >> 8); + case 1: + data.push_back(value >> 0); + break; + default: + ESP_LOGE(TAG, "Unexpected datapoint length %u", length); + return; + } + this->send_datapoint_command_(datapoint_id, datapoint_type, data); +} + +void Tuya::set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced) { + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty(value).c_str()); + 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 (!forced && datapoint->value_raw == value) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + this->send_datapoint_command_(datapoint_id, TuyaDatapointType::RAW, value); +} + +void Tuya::set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced) { + 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 (!forced && 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::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data) { + std::vector buffer; + buffer.push_back(datapoint_id); + buffer.push_back(static_cast(datapoint_type)); buffer.push_back(data.size() >> 8); buffer.push_back(data.size() >> 0); buffer.insert(buffer.end(), data.begin(), data.end()); - this->send_command_(TuyaCommandType::DATAPOINT_DELIVER, buffer.data(), buffer.size()); + + this->send_command_(TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_DELIVER, .payload = buffer}); } void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { @@ -325,5 +583,7 @@ void Tuya::register_listener(uint8_t datapoint_id, const std::functioninit_state_; } + } // namespace tuya } // namespace esphome diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 2fc9a16d44..c46d61119e 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -1,8 +1,14 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/uart/uart.h" +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + namespace esphome { namespace tuya { @@ -12,19 +18,22 @@ 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 { uint8_t id; TuyaDatapointType type; + size_t len; union { bool value_bool; int value_int; uint32_t value_uint; uint8_t value_enum; - uint16_t value_bitmask; + uint32_t value_bitmask; }; + std::string value_string; + std::vector value_raw; }; struct TuyaDatapointListener { @@ -43,6 +52,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_REPORT = 0x07, DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, + LOCAL_TIME_QUERY = 0x1C, }; enum class TuyaInitState : uint8_t { @@ -54,6 +64,11 @@ enum class TuyaInitState : uint8_t { INIT_DONE, }; +struct TuyaCommand { + TuyaCommandType cmd; + std::vector payload; +}; + class Tuya : public Component, public uart::UARTDevice { public: float get_setup_priority() const override { return setup_priority::LATE; } @@ -61,24 +76,66 @@ 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(TuyaDatapoint datapoint); + 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); + void force_set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); + void force_set_boolean_datapoint_value(uint8_t datapoint_id, bool value); + void force_set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); + void force_set_string_datapoint_value(uint8_t datapoint_id, const std::string &value); + void force_set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value); + void force_set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length); + TuyaInitState get_init_state(); +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } +#endif + void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) { + this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints); + } + void add_on_initialized_callback(std::function callback) { + this->initialized_callback_.add(std::move(callback)); + } protected: void handle_char_(uint8_t c); void handle_datapoint_(const uint8_t *buffer, size_t len); + optional get_datapoint_(uint8_t datapoint_id); bool validate_message_(); void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); - void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); - void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + void send_raw_command_(TuyaCommand command); + 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, bool forced); + void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced); + void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced); + void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); + void send_wifi_status_(); +#ifdef USE_TIME + void send_local_time_(); + optional time_id_{}; +#endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; + uint8_t protocol_version_ = -1; 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; + CallbackManager initialized_callback_{}; }; } // namespace tuya diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 3547cdf50c..84df82b5e6 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -2,37 +2,55 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_ID, CONF_WIND_SPEED, CONF_PIN, \ - CONF_WIND_DIRECTION_DEGREES, UNIT_KILOMETER_PER_HOUR, \ - ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION, UNIT_DEGREES +from esphome.const import ( + CONF_ID, + CONF_WIND_SPEED, + CONF_PIN, + CONF_WIND_DIRECTION_DEGREES, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_KILOMETER_PER_HOUR, + ICON_WEATHER_WINDY, + ICON_SIGN_DIRECTION, + UNIT_DEGREES, +) -tx20_ns = cg.esphome_ns.namespace('tx20') -Tx20Component = tx20_ns.class_('Tx20Component', cg.Component) +tx20_ns = cg.esphome_ns.namespace("tx20") +Tx20Component = tx20_ns.class_("Tx20Component", cg.Component) -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), - cv.Optional(CONF_WIND_DIRECTION_DEGREES): - sensor.sensor_schema(UNIT_DEGREES, ICON_SIGN_DIRECTION, 1), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, - pins.validate_has_interrupt), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Tx20Component), + cv.Optional(CONF_WIND_SPEED): sensor.sensor_schema( + 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_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) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) if CONF_WIND_SPEED in config: conf = config[CONF_WIND_SPEED] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_wind_speed_sensor(sens)) if CONF_WIND_DIRECTION_DEGREES in config: conf = config[CONF_WIND_DIRECTION_DEGREES] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(var.set_wind_direction_degrees_sensor(sens)) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index f3dafda288..fefcc8f4d5 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -5,12 +5,12 @@ namespace esphome { namespace tx20 { -static const char *TAG = "tx20"; +static const char *const TAG = "tx20"; static const uint8_t MAX_BUFFER_SIZE = 41; static const uint16_t TX20_MAX_TIME = MAX_BUFFER_SIZE * 1200 + 5000; static const uint16_t TX20_BIT_TIME = 1200; -static const char *DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", - "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; +static const char *const DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; void Tx20Component::setup() { ESP_LOGCONFIG(TAG, "Setting up Tx20"); @@ -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:"); @@ -113,7 +113,7 @@ void Tx20Component::decode_and_publish_() { if (tx20_sa == 4) { if (chk == tx20_sd) { if (tx20_sf == tx20_sc) { - tx20_wind_speed_kmh = float(tx20_sc) * 0.36; + tx20_wind_speed_kmh = float(tx20_sc) * 0.36f; ESP_LOGV(TAG, "WindSpeed %f", tx20_wind_speed_kmh); if (this->wind_speed_sensor_ != nullptr) this->wind_speed_sensor_->publish_state(tx20_wind_speed_kmh); @@ -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 4312bd5d10..a63b220fc7 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,87 +1,328 @@ +from typing import Optional + import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.yaml_util import make_data_base from esphome import pins, automation -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, CONF_DATA -from esphome.core import CORE, coroutine +from esphome.const import ( + CONF_BAUD_RATE, + CONF_ID, + CONF_NUMBER, + CONF_RX_PIN, + CONF_TX_PIN, + CONF_UART_ID, + CONF_DATA, + CONF_RX_BUFFER_SIZE, + CONF_INVERTED, + CONF_INVERT, + CONF_TRIGGER_ID, + CONF_SEQUENCE, + CONF_TIMEOUT, + CONF_DEBUG, + CONF_DIRECTION, + CONF_AFTER, + CONF_BYTES, + CONF_DELIMITER, + CONF_DUMMY_RECEIVER, + CONF_DUMMY_RECEIVER_ID, + CONF_LAMBDA, +) +from esphome.core import CORE -uart_ns = cg.esphome_ns.namespace('uart') -UARTComponent = uart_ns.class_('UARTComponent', cg.Component) -UARTDevice = uart_ns.class_('UARTDevice') -UARTWriteAction = uart_ns.class_('UARTWriteAction', automation.Action) +CODEOWNERS = ["@esphome/core"] +uart_ns = cg.esphome_ns.namespace("uart") +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) +UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action) +UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component) MULTI_CONF = True def validate_raw_data(value): if isinstance(value, str): - return value.encode('utf-8') + return value.encode("utf-8") if isinstance(value, str): return value if isinstance(value, list): return cv.Schema([cv.hex_uint8_t])(value) - raise cv.Invalid("data must either be a string wrapped in quotes or a list of bytes") + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) 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 -CONF_STOP_BITS = 'stop_bits' -CONFIG_SCHEMA = cv.All(cv.Schema({ - cv.GenerateID(): cv.declare_id(UARTComponent), - cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), - cv.Optional(CONF_TX_PIN): pins.output_pin, - cv.Optional(CONF_RX_PIN): validate_rx_pin, - cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) +def validate_invert_esp32(config): + if ( + CORE.is_esp32 + and CONF_TX_PIN in config + and CONF_RX_PIN in config + and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED] + ): + raise cv.Invalid( + "Different invert values for TX and RX pin are not (yet) supported for ESP32." + ) + return config -def to_code(config): +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, + "EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN, + "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, +} + +CONF_STOP_BITS = "stop_bits" +CONF_DATA_BITS = "data_bits" +CONF_PARITY = "parity" + +UARTDirection = uart_ns.enum("UARTDirection") +UART_DIRECTIONS = { + "RX": UARTDirection.UART_DIRECTION_RX, + "TX": UARTDirection.UART_DIRECTION_TX, + "BOTH": UARTDirection.UART_DIRECTION_BOTH, +} + +# The reason for having CONF_BYTES at 150 by default: +# +# The log message buffer size is 512 bytes by default. About 35 bytes are +# used for the log prefix. That leaves us with 477 bytes for logging data. +# The default log output is hex, which uses 3 characters per represented +# byte (2 hex chars + 1 separator). That means that 477 / 3 = 159 bytes +# can be represented in a single log line. Using 150, because people love +# round numbers. +AFTER_DEFAULTS = {CONF_BYTES: 150, CONF_TIMEOUT: "100ms"} + +# By default, log in hex format when no specific sequence is provided. +DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':');" +DEFAULT_SEQUENCE = [{CONF_LAMBDA: make_data_base(DEFAULT_DEBUG_OUTPUT)}] + + +def maybe_empty_debug(value): + if value is None: + value = {} + return DEBUG_SCHEMA(value) + + +DEBUG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger), + cv.Optional(CONF_DIRECTION, default="BOTH"): cv.enum( + UART_DIRECTIONS, upper=True + ), + cv.Optional(CONF_AFTER, default=AFTER_DEFAULTS): cv.Schema( + { + cv.Optional( + CONF_BYTES, default=AFTER_DEFAULTS[CONF_BYTES] + ): cv.validate_bytes, + cv.Optional( + CONF_TIMEOUT, default=AFTER_DEFAULTS[CONF_TIMEOUT] + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data), + } + ), + cv.Optional( + CONF_SEQUENCE, default=DEFAULT_SEQUENCE + ): automation.validate_automation(), + cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, + cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver), + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): _uart_declare_type, + cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), + cv.Optional(CONF_TX_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), + cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), + cv.Optional(CONF_PARITY, default="NONE"): cv.enum( + UART_PARITY_OPTIONS, upper=True + ), + cv.Optional(CONF_INVERT): cv.invalid( + "This option has been removed. Please instead use invert in the tx/rx pin schemas." + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN), + validate_invert_esp32, +) + + +async def debug_to_code(config, parent): + trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], parent) + await cg.register_component(trigger, config) + for action in config[CONF_SEQUENCE]: + await automation.build_automation( + trigger, + [(UARTDirection, "direction"), (cg.std_vector.template(cg.uint8), "bytes")], + action, + ) + cg.add(trigger.set_direction(config[CONF_DIRECTION])) + after = config[CONF_AFTER] + cg.add(trigger.set_after_bytes(after[CONF_BYTES])) + cg.add(trigger.set_after_timeout(after[CONF_TIMEOUT])) + if CONF_DELIMITER in after: + data = after[CONF_DELIMITER] + if isinstance(data, bytes): + data = list(data) + for byte in after[CONF_DELIMITER]: + cg.add(trigger.add_delimiter_byte(byte)) + if config[CONF_DUMMY_RECEIVER]: + dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) + await cg.register_component(dummy, {}) + cg.add_define("USE_UART_DEBUGGER") + + +async def to_code(config): cg.add_global(uart_ns.using) var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, 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])) 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])) + + if CONF_DEBUG in config: + await debug_to_code(config[CONF_DEBUG], var) # A schema to use for all UART devices, all UART integrations must extend this! -UART_DEVICE_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_UART_ID): cv.use_id(UARTComponent), -}) +UART_DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_UART_ID): cv.use_id(UARTComponent), + } +) + +KEY_UART_DEVICES = "uart_devices" -@coroutine -def register_uart_device(var, config): +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. This is a coroutine, you need to await it with a 'yield' expression! """ - parent = yield cg.get_variable(config[CONF_UART_ID]) + parent = await cg.get_variable(config[CONF_UART_ID]) cg.add(var.set_uart_parent(parent)) -@automation.register_action('uart.write', UARTWriteAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(UARTComponent), - cv.Required(CONF_DATA): cv.templatable(validate_raw_data), -}, key=CONF_DATA)) -def uart_write_to_code(config, action_id, template_arg, args): +@automation.register_action( + "uart.write", + UARTWriteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(UARTComponent), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, + ), +) +async def uart_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) - yield cg.register_parented(var, config[CONF_ID]) + await cg.register_parented(var, config[CONF_ID]) data = config[CONF_DATA] if isinstance(data, bytes): data = list(data) if cg.is_template(data): - templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: cg.add(var.set_data_static(data)) - yield var + return var diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index 6cc11d8bbe..9e7f95bd2a 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -1,29 +1,41 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import switch, uart -from esphome.const import CONF_DATA, CONF_ID, CONF_INVERTED +from esphome.const import CONF_DATA, CONF_ID, CONF_INVERTED, CONF_SEND_EVERY from esphome.core import HexInt from .. import uart_ns, validate_raw_data -DEPENDENCIES = ['uart'] +DEPENDENCIES = ["uart"] -UARTSwitch = uart_ns.class_('UARTSwitch', switch.Switch, uart.UARTDevice, cg.Component) +UARTSwitch = uart_ns.class_("UARTSwitch", switch.Switch, uart.UARTDevice, cg.Component) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(UARTSwitch), - cv.Required(CONF_DATA): validate_raw_data, - cv.Optional(CONF_INVERTED): cv.invalid("UART switches do not support inverted mode!"), -}).extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UARTSwitch), + cv.Required(CONF_DATA): validate_raw_data, + cv.Optional(CONF_INVERTED): cv.invalid( + "UART switches do not support inverted mode!" + ), + cv.Optional(CONF_SEND_EVERY): cv.positive_time_period_milliseconds, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield switch.register_switch(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await switch.register_switch(var, config) + await uart.register_uart_device(var, config) data = config[CONF_DATA] if isinstance(data, bytes): data = [HexInt(x) for x in data] cg.add(var.set_data(data)) + + if CONF_SEND_EVERY in config: + cg.add(var.set_send_every(config[CONF_SEND_EVERY])) diff --git a/esphome/components/uart/switch/uart_switch.cpp b/esphome/components/uart/switch/uart_switch.cpp index 9974ee1179..ffed08c731 100644 --- a/esphome/components/uart/switch/uart_switch.cpp +++ b/esphome/components/uart/switch/uart_switch.cpp @@ -4,7 +4,22 @@ namespace esphome { namespace uart { -static const char *TAG = "uart.switch"; +static const char *const TAG = "uart.switch"; + +void UARTSwitch::loop() { + if (this->state && this->send_every_) { + const uint32_t now = millis(); + if (now - this->last_transmission_ > this->send_every_) { + this->write_command_(); + this->last_transmission_ = now; + } + } +} + +void UARTSwitch::write_command_() { + ESP_LOGD(TAG, "'%s': Sending data...", this->get_name().c_str()); + this->write_array(this->data_.data(), this->data_.size()); +} void UARTSwitch::write_state(bool state) { if (!state) { @@ -13,11 +28,20 @@ void UARTSwitch::write_state(bool state) { } this->publish_state(true); - ESP_LOGD(TAG, "'%s': Sending data...", this->get_name().c_str()); - this->write_array(this->data_.data(), this->data_.size()); - this->publish_state(false); + this->write_command_(); + + if (this->send_every_ == 0) { + this->publish_state(false); + } else { + this->last_transmission_ = millis(); + } +} +void UARTSwitch::dump_config() { + LOG_SWITCH("", "UART Switch", this); + if (this->send_every_) { + ESP_LOGCONFIG(TAG, " Send Every: %u", this->send_every_); + } } -void UARTSwitch::dump_config() { LOG_SWITCH("", "UART Switch", this); } } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index c8a1b0d8c5..4c82d5680a 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -9,13 +9,19 @@ namespace uart { class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { public: + void loop() override; + void set_data(const std::vector &data) { data_ = data; } + void set_send_every(uint32_t send_every) { this->send_every_ = send_every; } void dump_config() override; protected: + void write_command_(); void write_state(bool state) override; std::vector data_; + uint32_t send_every_; + uint32_t last_transmission_; }; } // namespace uart diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 08fc0a326e..22a22e2772 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -4,392 +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 *TAG = "uart"; +static const char *const TAG = "uart"; -#ifdef ARDUINO_ARCH_ESP32 -uint8_t next_uart_num = 1; -#endif - -#ifdef ARDUINO_ARCH_ESP32 -void UARTComponent::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) { - this->hw_serial_ = &Serial; - } else { - this->hw_serial_ = new HardwareSerial(next_uart_num++); - } - int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; - int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - uint32_t config = SERIAL_8N1; - if (this->stop_bits_ == 2) - config = SERIAL_8N2; - this->hw_serial_->begin(this->baud_rate_, config, rx, tx); -} - -void UARTComponent::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_); - } - ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); - 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) { - 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) { - if (!this->check_read_timeout_()) - return false; - *data = this->hw_serial_->peek(); - return true; -} -bool UARTComponent::read_array(uint8_t *data, size_t len) { - if (!this->check_read_timeout_(len)) - return false; - this->hw_serial_->readBytes(data, len); - 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; -} -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() { - ESP_LOGVV(TAG, " Flushing..."); - this->hw_serial_->flush(); -} -#endif // ESP32 - -#ifdef ARDUINO_ARCH_ESP8266 -void UARTComponent::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. - uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE; - if (this->stop_bits_ == 1) - mode |= UART_NB_STOP_BIT_1; - else - mode |= UART_NB_STOP_BIT_2; - SerialConfig config = static_cast(mode); - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { - this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_, config); - } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { - this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_, config); - this->hw_serial_->swap(); - } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { - this->hw_serial_ = &Serial1; - this->hw_serial_->begin(this->baud_rate_, config); - } 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_); - } -} - -void UARTComponent::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_); - } - ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - 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_(); -} - -void UARTComponent::write_byte(uint8_t data) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(data); - } else { - this->sw_serial_->write_byte(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) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(data, len); - } else { - for (size_t i = 0; i < len; i++) - this->sw_serial_->write_byte(data[i]); - } - 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) { - 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) { - if (!this->check_read_timeout_()) - return false; - if (this->hw_serial_ != nullptr) { - *data = this->hw_serial_->peek(); - } else { - *data = this->sw_serial_->peek_byte(); - } - return true; -} -bool UARTComponent::read_array(uint8_t *data, size_t len) { - if (!this->check_read_timeout_(len)) - return false; - if (this->hw_serial_ != nullptr) { - this->hw_serial_->readBytes(data, len); - } else { - for (size_t i = 0; i < len; i++) - data[i] = this->sw_serial_->read_byte(); - } - 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; -} -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() { - if (this->hw_serial_ != nullptr) { - return this->hw_serial_->available(); - } else { - return this->sw_serial_->available(); - } -} -void UARTComponent::flush() { - ESP_LOGVV(TAG, " Flushing..."); - if (this->hw_serial_ != nullptr) { - this->hw_serial_->flush(); - } else { - this->sw_serial_->flush(); - } -} - -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) { - this->bit_time_ = F_CPU / baud_rate; - if (tx_pin != -1) { - auto pin = GPIOPin(tx_pin, OUTPUT); - pin.setup(); - this->tx_pin_ = pin.to_isr(); - this->tx_pin_->digital_write(true); - } - if (rx_pin != -1) { - auto pin = GPIOPin(rx_pin, INPUT); - pin.setup(); - this->rx_pin_ = pin.to_isr(); - this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; - pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); - } - this->stop_bits_ = stop_bits; -} -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { - uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; - const uint32_t start = ESP.getCycleCount(); - uint8_t rec = 0; - // Manually unroll the loop - rec |= arg->read_bit_(&wait, start) << 0; - rec |= arg->read_bit_(&wait, start) << 1; - rec |= arg->read_bit_(&wait, start) << 2; - rec |= arg->read_bit_(&wait, start) << 3; - rec |= arg->read_bit_(&wait, start) << 4; - rec |= arg->read_bit_(&wait, start) << 5; - rec |= arg->read_bit_(&wait, start) << 6; - rec |= arg->read_bit_(&wait, start) << 7; - // Stop bit - arg->wait_(&wait, start); - if (arg->stop_bits_ == 2) - arg->wait_(&wait, start); - - 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(); -} -void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { - if (this->tx_pin_ == nullptr) { - ESP_LOGE(TAG, "UART doesn't have TX pins set!"); - return; - } - - { - InterruptLock lock; - uint32_t wait = this->bit_time_; - const uint32_t start = ESP.getCycleCount(); - // Start bit - this->write_bit_(false, &wait, start); - this->write_bit_(data & (1 << 0), &wait, start); - this->write_bit_(data & (1 << 1), &wait, start); - this->write_bit_(data & (1 << 2), &wait, start); - this->write_bit_(data & (1 << 3), &wait, start); - this->write_bit_(data & (1 << 4), &wait, start); - this->write_bit_(data & (1 << 5), &wait, start); - this->write_bit_(data & (1 << 6), &wait, start); - this->write_bit_(data & (1 << 7), &wait, start); - // Stop bit - this->write_bit_(true, &wait, start); - if (this->stop_bits_ == 2) - this->wait_(&wait, start); - } -} -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { - while (ESP.getCycleCount() - start < *wait) - ; - *wait += this->bit_time_; -} -bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { - this->wait_(wait, start); - 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); - this->wait_(wait, start); -} -uint8_t ESP8266SoftwareSerial::read_byte() { - if (this->rx_in_pos_ == this->rx_out_pos_) - return 0; - uint8_t data = this->rx_buffer_[this->rx_out_pos_]; - this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_; - return data; -} -uint8_t ESP8266SoftwareSerial::peek_byte() { - if (this->rx_in_pos_ == this->rx_out_pos_) - return 0; - return this->rx_buffer_[this->rx_out_pos_]; -} -void ESP8266SoftwareSerial::flush() { - // Flush is a NO-OP with software serial, all bytes are written immediately. -} -int ESP8266SoftwareSerial::available() { - int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); - if (avail < 0) - return avail + this->rx_buffer_size_; - return avail; -} -#endif // ESP8266 - -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) { - if (this->parent_->baud_rate_ != baud_rate) { +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, + uint8_t data_bits) { + if (this->parent_->get_baud_rate() != baud_rate) { ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, - this->parent_->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_->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_->get_data_bits()); + } + 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 LogString *parity_to_str(UARTParityOptions parity) { + switch (parity) { + case UART_CONFIG_PARITY_NONE: + return LOG_STR("NONE"); + case UART_CONFIG_PARITY_EVEN: + return LOG_STR("EVEN"); + case UART_CONFIG_PARITY_ODD: + return LOG_STR("ODD"); + default: + return LOG_STR("UNKNOWN"); } } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 76e7496b80..d41dbe26e6 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -1,100 +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 { -#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); - - 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_{512}; - volatile size_t rx_in_pos_{0}; - size_t rx_out_pos_{0}; - uint8_t stop_bits_; - 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; } - - 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_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } - - 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_; - uint32_t baud_rate_; - uint8_t stop_bits_; -}; - -#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) {} @@ -123,16 +38,31 @@ 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 (!this->read_byte(&data)) + return -1; + return data; + } + size_t write(uint8_t data) { + this->write_byte(data); + return 1; + } + int peek() { + uint8_t data; + if (!this->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); + void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, + UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8); protected: UARTComponent *parent_{nullptr}; 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..42702cf5b8 --- /dev/null +++ b/esphome/components/uart/uart_component.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#ifdef USE_UART_DEBUGGER +#include "esphome/core/automation.h" +#endif + +namespace esphome { +namespace uart { + +enum UARTParityOptions { + UART_CONFIG_PARITY_NONE, + UART_CONFIG_PARITY_EVEN, + UART_CONFIG_PARITY_ODD, +}; + +#ifdef USE_UART_DEBUGGER +enum UARTDirection { + UART_DIRECTION_RX, + UART_DIRECTION_TX, + UART_DIRECTION_BOTH, +}; +#endif + +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; } + size_t get_rx_buffer_size() { return this->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_; } + +#ifdef USE_UART_DEBUGGER + void add_debug_callback(std::function &&callback) { + this->debug_callback_.add(std::move(callback)); + } +#endif + + 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_; +#ifdef USE_UART_DEBUGGER + CallbackManager debug_callback_{}; +#endif +}; + +} // namespace uart +} // namespace esphome diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp new file mode 100644 index 0000000000..95cdde4a43 --- /dev/null +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -0,0 +1,167 @@ +#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.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_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; +static const uint32_t UART_NB_BIT_8 = 3 << 2; +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 ESP32ArduinoUARTComponent::get_config() { + uint32_t config = 0; + + /* + * All bits numbers below come from + * framework-arduinoespressif32/cores/esp32/esp32-hal-uart.h + * And more specifically conf0 union in uart_dev_t. + * + * Below is bit used from conf0 union. + * : + * parity:0 0:even 1:odd + * parity_en:1 Set this bit to enable uart parity check. + * bit_num:2-4 0:5bits 1:6bits 2:7bits 3:8bits + * stop_bit_num:4-6 stop bit. 1:1bit 2:1.5bits 3:2bits + * tick_ref_always_on:27 select the clock.1:apb clock:ref_tick + */ + + if (this->parity_ == UART_CONFIG_PARITY_EVEN) + config |= UART_PARITY_EVEN | UART_PARITY_ENABLE; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + config |= UART_PARITY_ODD | UART_PARITY_ENABLE; + + switch (this->data_bits_) { + case 5: + config |= UART_NB_BIT_5; + break; + case 6: + config |= UART_NB_BIT_6; + break; + case 7: + config |= UART_NB_BIT_7; + break; + case 8: + config |= UART_NB_BIT_8; + break; + } + + if (this->stop_bits_ == 1) + config |= UART_NB_STOP_BIT_1; + else + config |= UART_NB_STOP_BIT_2; + + config |= UART_TICK_APB_CLOCK; + + return config; +} + +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. + 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 { + 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_ != 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 ESP32ArduinoUARTComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UART Bus:"); + 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 ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) { + this->hw_serial_->write(data, len); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); + } +#endif +} + +bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + *data = this->hw_serial_->peek(); + return true; +} + +bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) { + if (!this->check_read_timeout_(len)) + return false; + this->hw_serial_->readBytes(data, len); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); + } +#endif + return true; +} + +int 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 // 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_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp new file mode 100644 index 0000000000..370adad779 --- /dev/null +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -0,0 +1,301 @@ +#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.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) + config |= UART_PARITY_NONE; + else if (this->parity_ == UART_CONFIG_PARITY_EVEN) + config |= UART_PARITY_EVEN; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + config |= UART_PARITY_ODD; + + switch (this->data_bits_) { + case 5: + config |= UART_NB_BIT_5; + break; + case 6: + config |= UART_NB_BIT_6; + break; + case 7: + config |= UART_NB_BIT_7; + break; + case 8: + config |= UART_NB_BIT_8; + break; + } + + if (this->stop_bits_ == 1) + config |= UART_NB_STOP_BIT_1; + else + config |= UART_NB_STOP_BIT_2; + + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) + config |= BIT(22); + if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) + config |= BIT(19); + + return config; +} + +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 (!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_); + 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(); + 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(); // 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 ESP8266UartComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UART Bus:"); + 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", 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(); +} + +void ESP8266UartComponent::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 ESP8266UartComponent::write_array(const uint8_t *data, size_t len) { + if (this->hw_serial_ != nullptr) { + this->hw_serial_->write(data, len); + } else { + for (size_t i = 0; i < len; i++) + this->sw_serial_->write_byte(data[i]); + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); + } +#endif +} +bool ESP8266UartComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + if (this->hw_serial_ != nullptr) { + *data = this->hw_serial_->peek(); + } else { + *data = this->sw_serial_->peek_byte(); + } + return true; +} +bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { + if (!this->check_read_timeout_(len)) + return false; + if (this->hw_serial_ != nullptr) { + this->hw_serial_->readBytes(data, len); + } else { + for (size_t i = 0; i < len; i++) + data[i] = this->sw_serial_->read_byte(); + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); + } +#endif + return true; +} +int ESP8266UartComponent::available() { + if (this->hw_serial_ != nullptr) { + return this->hw_serial_->available(); + } else { + return this->sw_serial_->available(); + } +} +void ESP8266UartComponent::flush() { + ESP_LOGVV(TAG, " Flushing..."); + if (this->hw_serial_ != nullptr) { + this->hw_serial_->flush(); + } else { + this->sw_serial_->flush(); + } +} +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 != 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 != 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 IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { + uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; + 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++) + rec |= arg->read_bit_(&wait, start) << i; + + /* If parity is enabled, just read it and ignore it. */ + /* TODO: Should we check parity? Or is it too slow for nothing added..*/ + if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD) + arg->read_bit_(&wait, start); + + // Stop bit + arg->wait_(&wait, start); + if (arg->stop_bits_ == 2) + arg->wait_(&wait, start); + + 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(); +} +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 = 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 = arch_get_cpu_cycle_count(); + // Start bit + this->write_bit_(false, &wait, start); + for (int i = 0; i < this->data_bits_; i++) { + bool bit = data & (1 << i); + this->write_bit_(bit, &wait, start); + if (need_parity_bit) + parity_bit ^= bit; + } + if (need_parity_bit) + this->write_bit_(parity_bit, &wait, start); + // Stop bit + this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&wait, start); + } +} +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 IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { + this->wait_(wait, start); + return this->rx_pin_.digital_read(); +} +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() { + if (this->rx_in_pos_ == this->rx_out_pos_) + return 0; + uint8_t data = this->rx_buffer_[this->rx_out_pos_]; + this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_; + return data; +} +uint8_t ESP8266SoftwareSerial::peek_byte() { + if (this->rx_in_pos_ == this->rx_out_pos_) + return 0; + return this->rx_buffer_[this->rx_out_pos_]; +} +void ESP8266SoftwareSerial::flush() { + // Flush is a NO-OP with software serial, all bytes are written immediately. +} +int ESP8266SoftwareSerial::available() { + int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); + if (avail < 0) + return avail + this->rx_buffer_size_; + return avail; +} + +} // namespace uart +} // namespace esphome +#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..4d6a6af0fc --- /dev/null +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -0,0 +1,205 @@ +#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_); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); + } +#endif +} + +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_); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); + } +#endif + return true; +} + +int 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/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp new file mode 100644 index 0000000000..e2d92eac60 --- /dev/null +++ b/esphome/components/uart/uart_debugger.cpp @@ -0,0 +1,202 @@ +#include "esphome/core/defines.h" +#ifdef USE_UART_DEBUGGER + +#include +#include "uart_debugger.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart_debug"; + +UARTDebugger::UARTDebugger(UARTComponent *parent) { + parent->add_debug_callback([this](UARTDirection direction, uint8_t byte) { + if (!this->is_my_direction_(direction) || this->is_recursive_()) { + return; + } + this->trigger_after_direction_change_(direction); + this->store_byte_(direction, byte); + this->trigger_after_delimiter_(byte); + this->trigger_after_bytes_(); + }); +} + +void UARTDebugger::loop() { this->trigger_after_timeout_(); } + +bool UARTDebugger::is_my_direction_(UARTDirection direction) { + return this->for_direction_ == UART_DIRECTION_BOTH || this->for_direction_ == direction; +} + +bool UARTDebugger::is_recursive_() { return this->is_triggering_; } + +void UARTDebugger::trigger_after_direction_change_(UARTDirection direction) { + if (this->has_buffered_bytes_() && this->for_direction_ == UART_DIRECTION_BOTH && + this->last_direction_ != direction) { + this->fire_trigger_(); + } +} + +void UARTDebugger::store_byte_(UARTDirection direction, uint8_t byte) { + this->bytes_.push_back(byte); + this->last_direction_ = direction; + this->last_time_ = millis(); +} + +void UARTDebugger::trigger_after_delimiter_(uint8_t byte) { + if (this->after_delimiter_.empty() || !this->has_buffered_bytes_()) { + return; + } + if (this->after_delimiter_[this->after_delimiter_pos_] != byte) { + this->after_delimiter_pos_ = 0; + return; + } + this->after_delimiter_pos_++; + if (this->after_delimiter_pos_ == this->after_delimiter_.size()) { + this->fire_trigger_(); + this->after_delimiter_pos_ = 0; + } +} + +void UARTDebugger::trigger_after_bytes_() { + if (this->has_buffered_bytes_() && this->after_bytes_ > 0 && this->bytes_.size() >= this->after_bytes_) { + this->fire_trigger_(); + } +} + +void UARTDebugger::trigger_after_timeout_() { + if (this->has_buffered_bytes_() && this->after_timeout_ > 0 && millis() - this->last_time_ >= this->after_timeout_) { + this->fire_trigger_(); + } +} + +bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); } + +void UARTDebugger::fire_trigger_() { + this->is_triggering_ = true; + trigger(this->last_direction_, this->bytes_); + this->bytes_.clear(); + this->is_triggering_ = false; +} + +void UARTDummyReceiver::loop() { + // Reading up to a limited number of bytes, to make sure that this loop() + // won't lock up the system on a continuous incoming stream of bytes. + uint8_t data; + int count = 50; + while (this->available() && count--) { + this->read_byte(&data); + } +} + +// In the upcoming log functions, a delay was added after all log calls. +// This is done to allow the system to ship the log lines via the API +// TCP connection(s). Without these delays, debug log lines could go +// missing when UART devices block the main loop for too long. + +void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + size_t len = bytes.size(); + char buf[5]; + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + sprintf(buf, "%02X", bytes[i]); + res += buf; + } + ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); +} + +void UARTDebug::log_string(UARTDirection direction, std::vector bytes) { + std::string res; + if (direction == UART_DIRECTION_RX) { + res += "<<< \""; + } else { + res += ">>> \""; + } + size_t len = bytes.size(); + char buf[5]; + for (size_t i = 0; i < len; i++) { + if (bytes[i] == 7) { + res += "\\a"; + } else if (bytes[i] == 8) { + res += "\\b"; + } else if (bytes[i] == 9) { + res += "\\t"; + } else if (bytes[i] == 10) { + res += "\\n"; + } else if (bytes[i] == 11) { + res += "\\v"; + } else if (bytes[i] == 12) { + res += "\\f"; + } else if (bytes[i] == 13) { + res += "\\r"; + } else if (bytes[i] == 27) { + res += "\\e"; + } else if (bytes[i] == 34) { + res += "\\\""; + } else if (bytes[i] == 39) { + res += "\\'"; + } else if (bytes[i] == 92) { + res += "\\\\"; + } else if (bytes[i] < 32 || bytes[i] > 127) { + sprintf(buf, "\\x%02X", bytes[i]); + res += buf; + } else { + res += bytes[i]; + } + } + res += '"'; + ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); +} + +void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + size_t len = bytes.size(); + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + res += to_string(bytes[i]); + } + ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); +} + +void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + size_t len = bytes.size(); + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + char buf[20]; + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + sprintf(buf, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); + res += buf; + } + ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); +} + +} // namespace uart +} // namespace esphome +#endif diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h new file mode 100644 index 0000000000..6e84bbe450 --- /dev/null +++ b/esphome/components/uart/uart_debugger.h @@ -0,0 +1,101 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_UART_DEBUGGER + +#include +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "uart.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +/// The UARTDebugger class adds debugging support to a UART bus. +/// +/// It accumulates bytes that travel over the UART bus and triggers one or +/// more actions that can log the data at an appropriate time. What +/// 'appropriate time' means exactly, is determined by a number of +/// configurable constraints. E.g. when a given number of bytes is gathered +/// and/or when no more data has been seen for a given time interval. +class UARTDebugger : public Component, public Trigger> { + public: + explicit UARTDebugger(UARTComponent *parent); + void loop() override; + + /// Set the direction in which to inspect the bytes: incoming, outgoing + /// or both. When debugging in both directions, logging will be triggered + /// when the direction of the data stream changes. + void set_direction(UARTDirection direction) { this->for_direction_ = direction; } + + /// Set the maximum number of bytes to accumulate. When the number of bytes + /// is reached, logging will be triggered. + void set_after_bytes(size_t size) { this->after_bytes_ = size; } + + /// Set a timeout for the data stream. When no new bytes are seen during + /// this timeout, logging will be triggered. + void set_after_timeout(uint32_t timeout) { this->after_timeout_ = timeout; } + + /// Add a delimiter byte. This can be called multiple times to setup a + /// multi-byte delimiter (a typical example would be '\r\n'). + /// When the constructued byte sequence is found in the data stream, + /// logging will be triggered. + void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); } + + protected: + UARTDirection for_direction_; + UARTDirection last_direction_{}; + std::vector bytes_{}; + size_t after_bytes_; + uint32_t after_timeout_; + uint32_t last_time_{}; + std::vector after_delimiter_{}; + size_t after_delimiter_pos_{}; + bool is_triggering_{false}; + + bool is_my_direction_(UARTDirection direction); + bool is_recursive_(); + void store_byte_(UARTDirection direction, uint8_t byte); + void trigger_after_direction_change_(UARTDirection direction); + void trigger_after_delimiter_(uint8_t byte); + void trigger_after_bytes_(); + void trigger_after_timeout_(); + bool has_buffered_bytes_(); + void fire_trigger_(); +}; + +/// This UARTDevice is used by the serial debugger to read data from a +/// serial interface when the 'dummy_receiver' option is enabled. +/// The data are not stored, nor processed. This is most useful when the +/// debugger is used to reverse engineer a serial protocol, for which no +/// specific UARTDevice implementation exists (yet), but for which the +/// incoming bytes must be read to drive the debugger. +class UARTDummyReceiver : public Component, public UARTDevice { + public: + UARTDummyReceiver(UARTComponent *parent) : UARTDevice(parent) {} + void loop() override; +}; + +/// This class contains some static methods, that can be used to easily +/// create a logging action for the debugger. +class UARTDebug { + public: + /// Log the bytes as hex values, separated by the provided separator + /// character. + static void log_hex(UARTDirection direction, std::vector bytes, uint8_t separator); + + /// Log the bytes as string values, escaping unprintable characters. + static void log_string(UARTDirection direction, std::vector bytes); + + /// Log the bytes as integer values, separated by the provided separator + /// character. + static void log_int(UARTDirection direction, std::vector bytes, uint8_t separator); + + /// Log the bytes as ' ()' values, separated by the provided + /// separator. + static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator); +}; + +} // namespace uart +} // namespace esphome +#endif diff --git a/esphome/components/uln2003/stepper.py b/esphome/components/uln2003/stepper.py index 278fcf67eb..88252ead73 100644 --- a/esphome/components/uln2003/stepper.py +++ b/esphome/components/uln2003/stepper.py @@ -2,43 +2,54 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import stepper -from esphome.const import CONF_ID, CONF_PIN_A, CONF_PIN_B, CONF_PIN_C, CONF_PIN_D, \ - CONF_SLEEP_WHEN_DONE, CONF_STEP_MODE +from esphome.const import ( + CONF_ID, + CONF_PIN_A, + CONF_PIN_B, + CONF_PIN_C, + CONF_PIN_D, + CONF_SLEEP_WHEN_DONE, + CONF_STEP_MODE, +) -uln2003_ns = cg.esphome_ns.namespace('uln2003') -ULN2003StepMode = uln2003_ns.enum('ULN2003StepMode') +uln2003_ns = cg.esphome_ns.namespace("uln2003") +ULN2003StepMode = uln2003_ns.enum("ULN2003StepMode") STEP_MODES = { - 'FULL_STEP': ULN2003StepMode.ULN2003_STEP_MODE_FULL_STEP, - 'HALF_STEP': ULN2003StepMode.ULN2003_STEP_MODE_HALF_STEP, - 'WAVE_DRIVE': ULN2003StepMode.ULN2003_STEP_MODE_WAVE_DRIVE, + "FULL_STEP": ULN2003StepMode.ULN2003_STEP_MODE_FULL_STEP, + "HALF_STEP": ULN2003StepMode.ULN2003_STEP_MODE_HALF_STEP, + "WAVE_DRIVE": ULN2003StepMode.ULN2003_STEP_MODE_WAVE_DRIVE, } -ULN2003 = uln2003_ns.class_('ULN2003', stepper.Stepper, cg.Component) +ULN2003 = uln2003_ns.class_("ULN2003", stepper.Stepper, cg.Component) -CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend({ - cv.Required(CONF_ID): cv.declare_id(ULN2003), - cv.Required(CONF_PIN_A): pins.gpio_output_pin_schema, - cv.Required(CONF_PIN_B): pins.gpio_output_pin_schema, - cv.Required(CONF_PIN_C): pins.gpio_output_pin_schema, - cv.Required(CONF_PIN_D): pins.gpio_output_pin_schema, - cv.Optional(CONF_SLEEP_WHEN_DONE, default=False): cv.boolean, - cv.Optional(CONF_STEP_MODE, default='FULL_STEP'): cv.enum(STEP_MODES, upper=True, space='_') -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(ULN2003), + cv.Required(CONF_PIN_A): pins.gpio_output_pin_schema, + cv.Required(CONF_PIN_B): pins.gpio_output_pin_schema, + cv.Required(CONF_PIN_C): pins.gpio_output_pin_schema, + cv.Required(CONF_PIN_D): pins.gpio_output_pin_schema, + cv.Optional(CONF_SLEEP_WHEN_DONE, default=False): cv.boolean, + cv.Optional(CONF_STEP_MODE, default="FULL_STEP"): cv.enum( + STEP_MODES, upper=True, space="_" + ), + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield stepper.register_stepper(var, config) + await cg.register_component(var, config) + await stepper.register_stepper(var, config) - pin_a = yield cg.gpio_pin_expression(config[CONF_PIN_A]) + pin_a = await cg.gpio_pin_expression(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a)) - pin_b = yield cg.gpio_pin_expression(config[CONF_PIN_B]) + pin_b = await cg.gpio_pin_expression(config[CONF_PIN_B]) cg.add(var.set_pin_b(pin_b)) - pin_c = yield cg.gpio_pin_expression(config[CONF_PIN_C]) + pin_c = await cg.gpio_pin_expression(config[CONF_PIN_C]) cg.add(var.set_pin_c(pin_c)) - pin_d = yield cg.gpio_pin_expression(config[CONF_PIN_D]) + pin_d = await cg.gpio_pin_expression(config[CONF_PIN_D]) cg.add(var.set_pin_d(pin_d)) cg.add(var.set_sleep_when_done(config[CONF_SLEEP_WHEN_DONE])) diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp index b1a397ad6c..1af9806906 100644 --- a/esphome/components/uln2003/uln2003.cpp +++ b/esphome/components/uln2003/uln2003.cpp @@ -4,7 +4,7 @@ namespace esphome { namespace uln2003 { -static const char *TAG = "uln2003.stepper"; +static const char *const TAG = "uln2003.stepper"; void ULN2003::setup() { this->pin_a_->setup(); @@ -14,8 +14,8 @@ void ULN2003::setup() { this->loop(); } void ULN2003::loop() { - bool at_target = this->has_reached_target(); - if (at_target) { + int dir = this->should_step_(); + if (dir == 0 && this->has_reached_target()) { this->high_freq_.stop(); if (this->sleep_when_done_) { @@ -28,8 +28,6 @@ void ULN2003::loop() { } } else { this->high_freq_.start(); - - int dir = this->should_step_(); this->current_uln_pos_ += dir; } @@ -70,14 +68,8 @@ void ULN2003::write_step_(int32_t step) { } case ULN2003_STEP_MODE_HALF_STEP: { // A, AB, B, BC, C, CD, D, DA - if (i == 0 || i == 2 || i == 7) - res |= 1 << 0; - if (i == 1 || i == 2 || i == 3) - res |= 1 << 1; - if (i == 3 || i == 4 || i == 5) - res |= 1 << 2; - if (i == 5 || i == 6 || i == 7) - res |= 1 << 3; + res |= 1 << (i >> 1); + res |= 1 << (((i + 1) >> 1) & 0x3); break; } case ULN2003_STEP_MODE_WAVE_DRIVE: { 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/__init__.py b/esphome/components/ultrasonic/__init__.py index e69de29bb2..71a87b6ae5 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index e4364f271c..f7026e884c 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -2,38 +2,53 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_ECHO_PIN, CONF_ID, CONF_TRIGGER_PIN, \ - CONF_TIMEOUT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL +from esphome.const import ( + CONF_ECHO_PIN, + CONF_ID, + CONF_TRIGGER_PIN, + CONF_TIMEOUT, + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, +) -CONF_PULSE_TIME = 'pulse_time' +CONF_PULSE_TIME = "pulse_time" -ultrasonic_ns = cg.esphome_ns.namespace('ultrasonic') -UltrasonicSensorComponent = ultrasonic_ns.class_('UltrasonicSensorComponent', - sensor.Sensor, cg.PollingComponent) +ultrasonic_ns = cg.esphome_ns.namespace("ultrasonic") +UltrasonicSensorComponent = ultrasonic_ns.class_( + "UltrasonicSensorComponent", sensor.Sensor, cg.PollingComponent +) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ - cv.GenerateID(): cv.declare_id(UltrasonicSensorComponent), - cv.Required(CONF_TRIGGER_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - - cv.Optional(CONF_TIMEOUT, default='2m'): cv.distance, - cv.Optional(CONF_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')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(UltrasonicSensorComponent), + cv.Required(CONF_TRIGGER_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, + cv.Optional( + CONF_PULSE_TIME, default="10us" + ): cv.positive_time_period_microseconds, + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) - trigger = yield cg.gpio_pin_expression(config[CONF_TRIGGER_PIN]) + trigger = await cg.gpio_pin_expression(config[CONF_TRIGGER_PIN]) cg.add(var.set_trigger_pin(trigger)) - echo = yield cg.gpio_pin_expression(config[CONF_ECHO_PIN]) + echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index f8130f7d1f..9f47f9f6b9 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -1,31 +1,42 @@ #include "ultrasonic_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ultrasonic { -static const char *TAG = "ultrasonic.sensor"; +static const char *const TAG = "ultrasonic.sensor"; void UltrasonicSensorComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Ultrasonic Sensor..."); 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(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 1dacc99653..16a1e4c125 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -1,17 +1,35 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import CONF_ID, UNIT_SECOND, ICON_TIMER +from esphome.const import ( + CONF_ID, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_TOTAL_INCREASING, + UNIT_SECOND, + ICON_TIMER, +) -uptime_ns = cg.esphome_ns.namespace('uptime') -UptimeSensor = uptime_ns.class_('UptimeSensor', sensor.Sensor, cg.PollingComponent) +uptime_ns = cg.esphome_ns.namespace("uptime") +UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingComponent) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_SECOND, ICON_TIMER, 0).extend({ - cv.GenerateID(): cv.declare_id(UptimeSensor), -}).extend(cv.polling_component_schema('60s')) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(UptimeSensor), + } + ) + .extend(cv.polling_component_schema("60s")) +) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp index 5d117ab61d..40325d2a36 100644 --- a/esphome/components/uptime/uptime_sensor.cpp +++ b/esphome/components/uptime/uptime_sensor.cpp @@ -1,11 +1,12 @@ #include "uptime_sensor.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace uptime { -static const char *TAG = "uptime.sensor"; +static const char *const TAG = "uptime.sensor"; void UptimeSensor::update() { const uint32_t ms = millis(); diff --git a/esphome/components/version/__init__.py b/esphome/components/version/__init__.py index e69de29bb2..f70ffa9520 100644 --- a/esphome/components/version/__init__.py +++ b/esphome/components/version/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index 21044bb89f..4835caf35b 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,18 +1,34 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_ICON, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_NEW_BOX, + CONF_HIDE_TIMESTAMP, +) -version_ns = cg.esphome_ns.namespace('version') -VersionTextSensor = version_ns.class_('VersionTextSensor', text_sensor.TextSensor, cg.Component) +version_ns = cg.esphome_ns.namespace("version") +VersionTextSensor = version_ns.class_( + "VersionTextSensor", text_sensor.TextSensor, cg.Component +) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(VersionTextSensor), - cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VersionTextSensor), + cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon, + cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC + ): cv.entity_category, + } +).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield text_sensor.register_text_sensor(var, config) - yield cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + await cg.register_component(var, config) + cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 6aedfdedcd..5b2437ab62 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -6,11 +6,17 @@ namespace esphome { namespace version { -static const char *TAG = "version.text_sensor"; +static const char *const TAG = "version.text_sensor"; -void VersionTextSensor::setup() { this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); } +void VersionTextSensor::setup() { + if (this->hide_timestamp_) { + this->publish_state(ESPHOME_VERSION); + } else { + this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); + } +} float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } - +void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index cc798939ef..9355e78442 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -8,10 +8,14 @@ namespace version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: + void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; float get_setup_priority() const override; std::string unique_id() override; + + protected: + bool hide_timestamp_{false}; }; } // namespace version 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 6740d53e13..0ce3197366 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -1,24 +1,78 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, + CONF_ADDRESS, + CONF_TIMEOUT, + CONF_ENABLE_PIN, +) +from esphome import pins -DEPENDENCIES = ['i2c'] +DEPENDENCIES = ["i2c"] -vl53l0x_ns = cg.esphome_ns.namespace('vl53l0x') -VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComponent, - i2c.I2CDevice) +vl53l0x_ns = cg.esphome_ns.namespace("vl53l0x") +VL53L0XSensor = vl53l0x_ns.class_( + "VL53L0XSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) -CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ - cv.GenerateID(): cv.declare_id(VL53L0XSensor), - cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( - min=0.0, max=512.0, min_included=False, max_included=False) -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) +CONF_SIGNAL_RATE_LIMIT = "signal_rate_limit" +CONF_LONG_RANGE = "long_range" -def to_code(config): +def check_keys(obj): + if obj[CONF_ADDRESS] != 0x29 and CONF_ENABLE_PIN not in obj: + msg = "Address other then 0x29 requires enable_pin definition to allow sensor\r" + msg += "re-addressing. Also if you have more then one VL53 device on the same\r" + msg += "i2c bus, then all VL53 devices must have enable_pin defined." + raise cv.Invalid(msg) + return obj + + +def check_timeout(value): + value = cv.positive_time_period_microseconds(value) + if value.total_seconds > 60: + raise cv.Invalid("Maximum timeout can not be greater then 60 seconds") + return value + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(VL53L0XSensor), + cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( + min=0.0, max=512.0, min_included=False, max_included=False + ), + cv.Optional(CONF_LONG_RANGE, default=False): cv.boolean, + cv.Optional(CONF_TIMEOUT, default="10ms"): check_timeout, + cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x29)), + check_keys, +) + + +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + cg.add(var.set_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT])) + cg.add(var.set_long_range(config[CONF_LONG_RANGE])) + cg.add(var.set_timeout_us(config[CONF_TIMEOUT])) + + if CONF_ENABLE_PIN in config: + enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) + cg.add(var.set_enable_pin(enable)) + + await sensor.register_sensor(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 231bed99ac..d68d69b79c 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -13,27 +13,62 @@ namespace esphome { namespace vl53l0x { -static const char *TAG = "vl53l0x"; +static const char *const TAG = "vl53l0x"; + +std::list VL53L0XSensor::vl53_sensors; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +bool VL53L0XSensor::enable_pin_setup_complete = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +VL53L0XSensor::VL53L0XSensor() { VL53L0XSensor::vl53_sensors.push_back(this); } void VL53L0XSensor::dump_config() { LOG_SENSOR("", "VL53L0X", this); LOG_UPDATE_INTERVAL(this); LOG_I2C_DEVICE(this); + if (this->enable_pin_ != nullptr) { + LOG_PIN(" Enable Pin: ", this->enable_pin_); + } + ESP_LOGCONFIG(TAG, " Timeout: %u%s", this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)"); } + void VL53L0XSensor::setup() { + ESP_LOGD(TAG, "'%s' - setup BEGIN", this->name_.c_str()); + + if (!esphome::vl53l0x::VL53L0XSensor::enable_pin_setup_complete) { + for (auto &vl53_sensor : vl53_sensors) { + if (vl53_sensor->enable_pin_ != nullptr) { + // 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); + } + } + esphome::vl53l0x::VL53L0XSensor::enable_pin_setup_complete = true; + } + + if (this->enable_pin_ != nullptr) { + // Enable the enable pin to cause FW boot (to get back to 0x29 default address) + this->enable_pin_->digital_write(true); + delayMicroseconds(100); + } + + // Save the i2c address we want and force it to use the default 0x29 + // until we finish setup, then re-address to final desired address. + uint8_t final_address = address_; + this->set_i2c_address(0x29); + reg(0x89) |= 0x01; reg(0x88) = 0x00; reg(0x80) = 0x01; reg(0xFF) = 0x01; reg(0x00) = 0x00; - stop_variable_ = reg(0x91).get(); + this->stop_variable_ = reg(0x91).get(); reg(0x00) = 0x01; reg(0xFF) = 0x00; reg(0x80) = 0x00; reg(0x60) |= 0x12; - + if (this->long_range_) + this->signal_rate_limit_ = 0.1; auto rate_value = static_cast(signal_rate_limit_ * 128); write_byte_16(0x44, rate_value); @@ -51,8 +86,15 @@ void VL53L0XSensor::setup() { reg(0x94) = 0x6B; reg(0x83) = 0x00; - while (reg(0x83).get() == 0x00) + this->timeout_start_us_ = micros(); + while (reg(0x83).get() == 0x00) { + if (this->timeout_us_ > 0 && ((uint16_t)(micros() - this->timeout_start_us_) > this->timeout_us_)) { + ESP_LOGE(TAG, "'%s' - setup timeout", this->name_.c_str()); + this->mark_failed(); + return; + } yield(); + } reg(0x83) = 0x01; uint8_t tmp = reg(0x92).get(); @@ -67,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; @@ -104,7 +146,11 @@ void VL53L0XSensor::setup() { reg(0x48) = 0x00; reg(0x30) = 0x20; reg(0xFF) = 0x00; - reg(0x30) = 0x09; + if (this->long_range_) { + reg(0x30) = 0x07; // WAS 0x09 + } else { + reg(0x30) = 0x09; + } reg(0x54) = 0x00; reg(0x31) = 0x04; reg(0x32) = 0x03; @@ -116,7 +162,11 @@ void VL53L0XSensor::setup() { reg(0x51) = 0x00; reg(0x52) = 0x96; reg(0x56) = 0x08; - reg(0x57) = 0x30; + if (this->long_range_) { + reg(0x57) = 0x50; // was 0x30 + } else { + reg(0x57) = 0x30; + } reg(0x61) = 0x00; reg(0x62) = 0x00; reg(0x64) = 0x00; @@ -153,7 +203,11 @@ void VL53L0XSensor::setup() { reg(0x44) = 0x00; reg(0x45) = 0x20; reg(0x47) = 0x08; - reg(0x48) = 0x28; + if (this->long_range_) { + reg(0x48) = 0x48; // was 0x28 + } else { + reg(0x48) = 0x28; + } reg(0x67) = 0x00; reg(0x70) = 0x04; reg(0x71) = 0x01; @@ -192,11 +246,22 @@ void VL53L0XSensor::setup() { return; } reg(0x01) = 0xE8; + + // Set the sensor to the desired final address + // The following is different for VL53L0X vs VL53L1X + // I2C_SXXXX_DEVICE_ADDRESS = 0x8A for VL53L0X + // I2C_SXXXX__DEVICE_ADDRESS = 0x0001 for VL53L1X + reg(0x8A) = final_address & 0x7F; + this->set_i2c_address(final_address); + + ESP_LOGD(TAG, "'%s' - setup END", this->name_.c_str()); } void VL53L0XSensor::update() { if (this->initiated_read_ || this->waiting_for_interrupt_) { this->publish_state(NAN); - this->status_set_warning(); + this->status_momentary_warning("update", 5000); + ESP_LOGW(TAG, "%s - update called before prior reading complete - initiated:%d waiting_for_interrupt:%d", + this->name_.c_str(), this->initiated_read_, this->waiting_for_interrupt_); } // initiate single shot measurement @@ -204,7 +269,7 @@ void VL53L0XSensor::update() { reg(0xFF) = 0x01; reg(0x00) = 0x00; - reg(0x91) = stop_variable_; + reg(0x91) = this->stop_variable_; reg(0x00) = 0x01; reg(0xFF) = 0x00; reg(0x80) = 0x00; @@ -227,13 +292,13 @@ 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; if (range_mm >= 8190) { - ESP_LOGW(TAG, "'%s' - Distance is out of range, please move the target closer", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' - Distance is out of range, please move the target closer", this->name_.c_str()); this->publish_state(NAN); return; } diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 1825383cee..a2e24e7550 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -1,6 +1,9 @@ #pragma once +#include + #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -20,6 +23,8 @@ struct SequenceStepTimeouts { class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: + VL53L0XSensor(); + void setup() override; void dump_config() override; @@ -29,6 +34,9 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void loop() override; void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + void set_long_range(bool long_range) { long_range_ = long_range; } + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } + void set_enable_pin(GPIOPin *enable) { this->enable_pin_ = enable; } protected: uint32_t get_measurement_timing_budget_() { @@ -247,10 +255,18 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c } float signal_rate_limit_; + bool long_range_; + GPIOPin *enable_pin_{nullptr}; uint32_t measurement_timing_budget_us_; bool initiated_read_{false}; bool waiting_for_interrupt_{false}; uint8_t stop_variable_; + + uint16_t timeout_start_us_; + uint16_t timeout_us_{}; + + static std::list vl53_sensors; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static bool enable_pin_setup_complete; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) }; } // namespace vl53l0x diff --git a/esphome/components/voltage_sampler/__init__.py b/esphome/components/voltage_sampler/__init__.py index 64161205d8..e60918096e 100644 --- a/esphome/components/voltage_sampler/__init__.py +++ b/esphome/components/voltage_sampler/__init__.py @@ -1,4 +1,4 @@ import esphome.codegen as cg -voltage_sampler_ns = cg.esphome_ns.namespace('voltage_sampler') -VoltageSampler = voltage_sampler_ns.class_('VoltageSampler') +voltage_sampler_ns = cg.esphome_ns.namespace("voltage_sampler") +VoltageSampler = voltage_sampler_ns.class_("VoltageSampler") diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index a654e55981..1d1644dc25 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -1,87 +1,144 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import core, pins from esphome.components import display, spi -from esphome.const import CONF_BUSY_PIN, CONF_DC_PIN, CONF_FULL_UPDATE_EVERY, \ - CONF_ID, CONF_LAMBDA, CONF_MODEL, CONF_PAGES, CONF_RESET_PIN +from esphome.const import ( + CONF_BUSY_PIN, + CONF_DC_PIN, + CONF_FULL_UPDATE_EVERY, + CONF_ID, + CONF_LAMBDA, + CONF_MODEL, + CONF_PAGES, + CONF_RESET_DURATION, + CONF_RESET_PIN, +) -DEPENDENCIES = ['spi'] +DEPENDENCIES = ["spi"] -waveshare_epaper_ns = cg.esphome_ns.namespace('waveshare_epaper') -WaveshareEPaper = waveshare_epaper_ns.class_('WaveshareEPaper', cg.PollingComponent, spi.SPIDevice, - display.DisplayBuffer) -WaveshareEPaperTypeA = waveshare_epaper_ns.class_('WaveshareEPaperTypeA', WaveshareEPaper) -WaveshareEPaper2P7In = waveshare_epaper_ns.class_('WaveshareEPaper2P7In', WaveshareEPaper) -WaveshareEPaper2P9InB = waveshare_epaper_ns.class_('WaveshareEPaper2P9InB', WaveshareEPaper) -WaveshareEPaper4P2In = waveshare_epaper_ns.class_('WaveshareEPaper4P2In', WaveshareEPaper) -WaveshareEPaper5P8In = waveshare_epaper_ns.class_('WaveshareEPaper5P8In', WaveshareEPaper) -WaveshareEPaper7P5In = waveshare_epaper_ns.class_('WaveshareEPaper7P5In', WaveshareEPaper) +waveshare_epaper_ns = cg.esphome_ns.namespace("waveshare_epaper") +WaveshareEPaper = waveshare_epaper_ns.class_( + "WaveshareEPaper", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) +WaveshareEPaperTypeA = waveshare_epaper_ns.class_( + "WaveshareEPaperTypeA", WaveshareEPaper +) +WaveshareEPaper2P7In = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7In", WaveshareEPaper +) +WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InB", WaveshareEPaper +) +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') +WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") +WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel") MODELS = { - '1.54in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN), - '2.13in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN), - '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), - '2.13in-ttgo-b73': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B73), - '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), - '2.70in': ('b', WaveshareEPaper2P7In), - '2.90in-b': ('b', WaveshareEPaper2P9InB), - '4.20in': ('b', WaveshareEPaper4P2In), - '5.83in': ('b', WaveshareEPaper5P8In), - '7.50in': ('b', WaveshareEPaper7P5In), + "1.54in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN), + "1.54inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN_V2), + "2.13in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN), + "2.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': - raise cv.Invalid("The 'full_update_every' option is only available for models " - "'1.54in', '2.13in' and '2.90in'.") + 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'." + ) return value -CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(WaveshareEPaper), - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True), - cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), - validate_full_update_every_only_type_a, - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WaveshareEPaper), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, + cv.Optional(CONF_RESET_DURATION): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(spi.spi_device_schema()), + validate_full_update_every_only_type_a, + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) -def to_code(config): +async def to_code(config): model_type, model = MODELS[config[CONF_MODEL]] - if model_type == 'a': + 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: raise NotImplementedError() - yield cg.register_component(var, config) - yield display.register_display(var, config) - yield spi.register_spi_device(var, config) + await cg.register_component(var, config) + await display.register_display(var, config) + await spi.register_spi_device(var, config) - dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], - return_type=cg.void) + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) cg.add(var.set_writer(lambda_)) if CONF_RESET_PIN in config: - reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) if CONF_BUSY_PIN in config: - reset = yield cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + reset = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) cg.add(var.set_busy_pin(reset)) if CONF_FULL_UPDATE_EVERY in config: cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + if CONF_RESET_DURATION in config: + cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index c145fb361c..322c375f0e 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -6,9 +6,10 @@ namespace esphome { namespace waveshare_epaper { -static const char *TAG = "waveshare_epaper"; +static const char *const TAG = "waveshare_epaper"; static const uint8_t LUT_SIZE_WAVESHARE = 30; + static const uint8_t FULL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00}; @@ -18,7 +19,6 @@ static const uint8_t PARTIAL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; static const uint8_t LUT_SIZE_TTGO = 70; -static const uint8_t LUT_SIZE_TTGO_B73 = 100; static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 @@ -35,6 +35,23 @@ static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 }; +static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2: WB: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM: VS 0 ~7 + 0x0A, 0x00, 0x00, 0x00, 0x00, // TP0 A~D RP0 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP1 A~D RP1 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 +}; + +static const uint8_t LUT_SIZE_TTGO_B73 = 100; + static const uint8_t FULL_UPDATE_LUT_TTGO_B73[LUT_SIZE_TTGO_B73] = { 0xA0, 0x90, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x90, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0x90, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x90, 0xA0, 0x00, @@ -55,20 +72,15 @@ static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B73[LUT_SIZE_TTGO_B73] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; -static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 - 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 - 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2: WB: VS 0 ~7 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3: WW: VS 0 ~7 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM: VS 0 ~7 - 0x0A, 0x00, 0x00, 0x00, 0x00, // TP0 A~D RP0 - 0x00, 0x00, 0x00, 0x00, 0x00, // TP1 A~D RP1 - 0x00, 0x00, 0x00, 0x00, 0x00, // TP2 A~D RP2 - 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 - 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 - 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 - 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 -}; +static const uint8_t LUT_SIZE_TTGO_B1 = 29; + +static const uint8_t FULL_UPDATE_LUT_TTGO_B1[LUT_SIZE_TTGO_B1] = { + 0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x01, 0x00, 0x00, 0x00, 0x00}; + +static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B1[LUT_SIZE_TTGO_B1] = { + 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; void WaveshareEPaper::setup_pins_() { this->init_internal_(this->get_buffer_length_()); @@ -103,7 +115,7 @@ bool WaveshareEPaper::wait_until_idle_() { const uint32_t start = millis(); while (this->busy_pin_->digital_read()) { - if (millis() - start > 1000) { + if (millis() - start > this->idle_timeout_()) { ESP_LOGE(TAG, "Timeout while displaying image!"); return false; } @@ -115,20 +127,20 @@ void WaveshareEPaper::update() { this->do_update_(); this->display(); } -void WaveshareEPaper::fill(int color) { +void WaveshareEPaper::fill(Color color) { // flip logic - const uint8_t fill = color ? 0x00 : 0xFF; + const uint8_t fill = color.is_on() ? 0x00 : 0xFF; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } -void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; const uint32_t pos = (x + y * this->get_width_internal()) / 8u; const uint8_t subpos = x & 0x07; // flip logic - if (!color) + if (!color.is_on()) this->buffer_[pos] |= 0x80 >> subpos; else this->buffer_[pos] &= ~(0x80 >> subpos); @@ -151,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); @@ -177,7 +200,21 @@ void WaveshareEPaperTypeA::initialize() { // COMMAND DATA ENTRY MODE SETTING this->command(0x11); - this->data(0x03); // from top left to bottom right + switch (this->model_) { + 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 + this->command(0x21); + this->data(0x00); + this->data(0x80); + break; + default: + this->data(0x03); // from top left to bottom right + } } void WaveshareEPaperTypeA::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); @@ -185,6 +222,9 @@ void WaveshareEPaperTypeA::dump_config() { case WAVESHARE_EPAPER_1_54_IN: ESP_LOGCONFIG(TAG, " Model: 1.54in"); break; + case WAVESHARE_EPAPER_1_54_IN_V2: + ESP_LOGCONFIG(TAG, " Model: 1.54inV2"); + break; case WAVESHARE_EPAPER_2_13_IN: ESP_LOGCONFIG(TAG, " Model: 2.13in"); break; @@ -194,9 +234,18 @@ 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; case WAVESHARE_EPAPER_2_9_IN: ESP_LOGCONFIG(TAG, " Model: 2.9in"); break; + case WAVESHARE_EPAPER_2_9_IN_V2: + ESP_LOGCONFIG(TAG, " Model: 2.9inV2"); + break; } ESP_LOGCONFIG(TAG, " Full Update Every: %u", this->full_update_every_); LOG_PIN(" Reset Pin: ", this->reset_pin_); @@ -205,45 +254,85 @@ void WaveshareEPaperTypeA::dump_config() { LOG_UPDATE_INTERVAL(this); } void HOT WaveshareEPaperTypeA::display() { + bool full_update = this->at_update_ == 0; + bool prev_full_update = this->at_update_ == 1; + if (!this->wait_until_idle_()) { this->status_set_warning(); return; } - if (this->full_update_every_ >= 2) { - bool prev_full_update = this->at_update_ == 1; - bool full_update = this->at_update_ == 0; + if (this->full_update_every_ >= 1) { if (full_update != prev_full_update) { - if (this->model_ == TTGO_EPAPER_2_13_IN) { - this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO); - } else if (this->model_ == 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); - } else { - this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE); + switch (this->model_) { + case TTGO_EPAPER_2_13_IN: + this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO); + break; + 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; + default: + this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE); } } this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; } // Set x & y regions we want to write to (full) - // COMMAND SET RAM X ADDRESS START END POSITION - this->command(0x44); - this->data(0x00); - this->data((this->get_width_internal() - 1) >> 3); - // COMMAND SET RAM Y ADDRESS START END POSITION - this->command(0x45); - this->data(0x00); - this->data(0x00); - this->data(this->get_height_internal() - 1); - this->data((this->get_height_internal() - 1) >> 8); + switch (this->model_) { + case TTGO_EPAPER_2_13_IN_B1: + // COMMAND SET RAM X ADDRESS START END POSITION + this->command(0x44); + this->data(0x00); + this->data((this->get_width_internal() - 1) >> 3); + // COMMAND SET RAM Y ADDRESS START END POSITION + this->command(0x45); + this->data(this->get_height_internal() - 1); + this->data((this->get_height_internal() - 1) >> 8); + this->data(0x00); + this->data(0x00); - // COMMAND SET RAM X ADDRESS COUNTER - this->command(0x4E); - this->data(0x00); - // COMMAND SET RAM Y ADDRESS COUNTER - this->command(0x4F); - this->data(0x00); - this->data(0x00); + // COMMAND SET RAM X ADDRESS COUNTER + this->command(0x4E); + this->data(0x00); + // COMMAND SET RAM Y ADDRESS COUNTER + this->command(0x4F); + this->data(this->get_height_internal() - 1); + 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); + this->data(0x00); + this->data((this->get_width_internal() - 1) >> 3); + // COMMAND SET RAM Y ADDRESS START END POSITION + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data(this->get_height_internal() - 1); + this->data((this->get_height_internal() - 1) >> 8); + + // COMMAND SET RAM X ADDRESS COUNTER + this->command(0x4E); + this->data(0x00); + // COMMAND SET RAM Y ADDRESS COUNTER + this->command(0x4F); + this->data(0x00); + this->data(0x00); + } if (!this->wait_until_idle_()) { this->status_set_warning(); @@ -253,12 +342,38 @@ void HOT WaveshareEPaperTypeA::display() { // COMMAND WRITE RAM this->command(0x24); this->start_data_(); - this->write_array(this->buffer_, this->get_buffer_length_()); + switch (this->model_) { + case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations + int16_t wb = ((this->get_width_internal()) >> 3); + for (int i = 0; i < this->get_height_internal(); i++) { + for (int j = 0; j < wb; j++) { + int idx = j + (this->get_height_internal() - 1 - i) * wb; + this->write_byte(this->buffer_[idx]); + } + } + break; + } + default: + this->write_array(this->buffer_, this->get_buffer_length_()); + } this->end_data_(); // COMMAND DISPLAY UPDATE CONTROL 2 this->command(0x22); - this->data(0xC4); + switch (this->model_) { + case WAVESHARE_EPAPER_2_9_IN_V2: + case WAVESHARE_EPAPER_1_54_IN_V2: + case TTGO_EPAPER_2_13_IN_B74: + this->data(full_update ? 0xF7 : 0xFF); + break; + case TTGO_EPAPER_2_13_IN_B73: + this->data(0xC7); + break; + default: + this->data(0xC4); + break; + } + // COMMAND MASTER ACTIVATION this->command(0x20); // COMMAND TERMINATE FRAME READ WRITE @@ -269,14 +384,15 @@ void HOT WaveshareEPaperTypeA::display() { int WaveshareEPaperTypeA::get_width_internal() { switch (this->model_) { case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: return 200; case WAVESHARE_EPAPER_2_13_IN: - return 128; case TTGO_EPAPER_2_13_IN: - return 128; case TTGO_EPAPER_2_13_IN_B73: - return 128; + case TTGO_EPAPER_2_13_IN_B74: + case TTGO_EPAPER_2_13_IN_B1: case WAVESHARE_EPAPER_2_9_IN: + case WAVESHARE_EPAPER_2_9_IN_V2: return 128; } return 0; @@ -284,14 +400,16 @@ int WaveshareEPaperTypeA::get_width_internal() { int WaveshareEPaperTypeA::get_height_internal() { switch (this->model_) { case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: return 200; case WAVESHARE_EPAPER_2_13_IN: - return 250; case TTGO_EPAPER_2_13_IN: - return 250; case TTGO_EPAPER_2_13_IN_B73: + case TTGO_EPAPER_2_13_IN_B74: + case TTGO_EPAPER_2_13_IN_B1: return 250; case WAVESHARE_EPAPER_2_9_IN: + case WAVESHARE_EPAPER_2_9_IN_V2: return 296; } return 0; @@ -307,6 +425,15 @@ void WaveshareEPaperTypeA::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } +uint32_t WaveshareEPaperTypeA::idle_timeout_() { + switch (this->model_) { + case TTGO_EPAPER_2_13_IN_B1: + return 2500; + default: + return WaveshareEPaper::idle_timeout_(); + } +} + // ======================================================== // Type B // ======================================================== @@ -490,7 +617,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 @@ -510,7 +637,7 @@ void HOT WaveshareEPaper2P9InB::display() { this->command(0x13); delay(2); this->start_data_(); - for (int i = 0; i < this->get_buffer_length_(); i++) + for (size_t i = 0; i < this->get_buffer_length_(); i++) this->write_byte(0x00); this->end_data_(); delay(2); @@ -657,6 +784,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 (size_t 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); @@ -751,63 +934,51 @@ void WaveshareEPaper5P8In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } - void WaveshareEPaper7P5In::initialize() { // COMMAND POWER SETTING this->command(0x01); this->data(0x37); this->data(0x00); - // COMMAND PANEL SETTING this->command(0x00); this->data(0xCF); this->data(0x0B); - // COMMAND BOOSTER SOFT START this->command(0x06); this->data(0xC7); this->data(0xCC); this->data(0x28); - // COMMAND POWER ON this->command(0x04); this->wait_until_idle_(); delay(10); - // COMMAND PLL CONTROL this->command(0x30); this->data(0x3C); - // COMMAND TEMPERATURE SENSOR CALIBRATION this->command(0x41); this->data(0x00); - // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x77); - // COMMAND TCON SETTING this->command(0x60); this->data(0x22); - // COMMAND RESOLUTION SETTING this->command(0x61); this->data(0x02); this->data(0x80); this->data(0x01); this->data(0x80); - // COMMAND VCM DC SETTING REGISTER this->command(0x82); this->data(0x1E); - this->command(0xE5); this->data(0x03); } void HOT WaveshareEPaper7P5In::display() { // COMMAND DATA START TRANSMISSION 1 this->command(0x10); - this->start_data_(); for (size_t i = 0; i < this->get_buffer_length_(); i++) { uint8_t temp1 = this->buffer_[i]; @@ -817,7 +988,6 @@ void HOT WaveshareEPaper7P5In::display() { temp2 = 0x03; else temp2 = 0x00; - temp2 <<= 4; temp1 <<= 1; j++; @@ -828,11 +998,9 @@ void HOT WaveshareEPaper7P5In::display() { temp1 <<= 1; this->write_byte(temp2); } - App.feed_wdt(); } this->end_data_(); - // COMMAND DISPLAY REFRESH this->command(0x12); } @@ -846,6 +1014,289 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InV2::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + this->command(0x04); + + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); + this->data(0x20); + this->data(0x01); + this->data(0xE0); + // COMMAND ...? + this->command(0x15); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); +} +void HOT WaveshareEPaper7P5InV2::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION NEW DATA + this->command(0x13); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~(this->buffer_[i])); + } + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } +int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } +void WaveshareEPaper7P5InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5inV2"); + 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); +} + +/* 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, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + // 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; } +uint32_t WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } +void WaveshareEPaper2P13InDKE::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); + LOG_PIN(" 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 1ed7475350..4de2ac7d97 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -16,6 +16,7 @@ class WaveshareEPaper : public PollingComponent, float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } + void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } void command(uint8_t value); void data(uint8_t value); @@ -26,7 +27,7 @@ class WaveshareEPaper : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -36,7 +37,7 @@ class WaveshareEPaper : public PollingComponent, void on_safe_shutdown() override; protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; bool wait_until_idle_(); @@ -45,13 +46,14 @@ class WaveshareEPaper : public PollingComponent, void reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(200); // NOLINT + delay(reset_duration_); // NOLINT this->reset_pin_->digital_write(true); delay(200); // NOLINT } } uint32_t get_buffer_length_(); + uint32_t reset_duration_{200}; void start_command_(); void end_command_(); @@ -61,14 +63,19 @@ class WaveshareEPaper : public PollingComponent, GPIOPin *reset_pin_{nullptr}; GPIOPin *dc_pin_; GPIOPin *busy_pin_{nullptr}; + virtual uint32_t idle_timeout_() { return 1000u; } // NOLINT(readability-identifier-naming) }; enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_1_54_IN = 0, + WAVESHARE_EPAPER_1_54_IN_V2, WAVESHARE_EPAPER_2_13_IN, WAVESHARE_EPAPER_2_9_IN, + WAVESHARE_EPAPER_2_9_IN_V2, 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 { @@ -82,8 +89,14 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { void display() override; void deep_sleep() override { - // COMMAND DEEP SLEEP MODE - this->command(0x10); + if (this->model_ == WAVESHARE_EPAPER_2_9_IN_V2 || this->model_ == WAVESHARE_EPAPER_1_54_IN_V2) { + // COMMAND DEEP SLEEP MODE + this->command(0x10); + this->data(0x01); + } else { + // COMMAND DEEP SLEEP MODE + this->command(0x10); + } this->wait_until_idle_(); } @@ -99,12 +112,15 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { uint32_t full_update_every_{30}; uint32_t at_update_{0}; WaveshareEPaperTypeAModel model_; + uint32_t idle_timeout_() override; }; 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, }; class WaveshareEPaper2P7In : public WaveshareEPaper { @@ -190,6 +206,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; @@ -236,5 +280,79 @@ 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; + + 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 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; + + uint32_t 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 2f0d179eba..d9ff84d501 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -3,49 +3,78 @@ 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_CSS_INCLUDE, CONF_CSS_URL, CONF_ID, CONF_JS_INCLUDE, CONF_JS_URL, CONF_PORT, - CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) -from esphome.core import coroutine_with_priority + CONF_CSS_INCLUDE, + CONF_CSS_URL, + CONF_ID, + CONF_JS_INCLUDE, + CONF_JS_URL, + CONF_PORT, + CONF_AUTH, + CONF_USERNAME, + CONF_PASSWORD, + CONF_INCLUDE_INTERNAL, + CONF_OTA, +) +from esphome.core import CORE, coroutine_with_priority -AUTO_LOAD = ['json', 'web_server_base'] +AUTO_LOAD = ["json", "web_server_base"] -web_server_ns = cg.esphome_ns.namespace('web_server') -WebServer = web_server_ns.class_('WebServer', cg.Component, cg.Controller) +web_server_ns = cg.esphome_ns.namespace("web_server") +WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(WebServer), - cv.Optional(CONF_PORT, default=80): cv.port, - cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string, - cv.Optional(CONF_CSS_INCLUDE): cv.file_, - cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, - 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.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase), -}).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServer), + cv.Optional(CONF_PORT, default=80): cv.port, + cv.Optional( + CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css" + ): cv.string, + cv.Optional(CONF_CSS_INCLUDE): cv.file_, + cv.Optional( + CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js" + ): cv.string, + cv.Optional(CONF_JS_INCLUDE): cv.file_, + cv.Optional(CONF_AUTH): cv.Schema( + { + 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( + web_server_base.WebServerBase + ), + cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, + cv.Optional(CONF_OTA, default=True): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) @coroutine_with_priority(40.0) -def to_code(config): - paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) +async def to_code(config): + paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) - yield cg.register_component(var, config) + await cg.register_component(var, config) + + cg.add_define("USE_WEBSERVER") cg.add(paren.set_port(config[CONF_PORT])) + cg.add_define("WEBSERVER_PORT", config[CONF_PORT]) + cg.add_define("USE_WEBSERVER") cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) + cg.add(var.set_allow_ota(config[CONF_OTA])) 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: + cg.add_define("WEBSERVER_CSS_INCLUDE") + 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: + cg.add_define("WEBSERVER_JS_INCLUDE") + 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())) + cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1f6cd10666..1e7696edfb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,27 +1,40 @@ +#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 +#ifdef USE_FAN +#include "esphome/components/fan/fan_helpers.h" +#endif + namespace esphome { namespace web_server { -static const char *TAG = "web_server"; +static const char *const TAG = "web_server"; -void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &klass, const std::string &action) { - if (obj->is_internal()) - return; +void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, + const std::function &action_func = nullptr) { stream->print("print(klass.c_str()); + if (obj->is_internal()) + stream->print(" internal"); stream->print("\" id=\""); stream->print(klass.c_str()); stream->print("-"); @@ -30,7 +43,11 @@ void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &kl stream->print(obj->get_name().c_str()); stream->print(""); stream->print(action.c_str()); + if (action_func) { + action_func(*stream, obj); + } stream->print(""); + stream->print(""); } UrlMatch match_url(const std::string &url, bool only_domain = false) { @@ -66,7 +83,7 @@ void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_ void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); - this->setup_controller(); + this->setup_controller(this->include_internal_); this->base_->init(); this->events_.onConnect([this](AsyncEventSourceClient *client) { @@ -75,45 +92,57 @@ void WebServer::setup() { #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->sensor_json(obj, obj->state).c_str(), "state"); #endif #ifdef USE_SWITCH for (auto *obj : App.get_switches()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->switch_json(obj, obj->state).c_str(), "state"); #endif #ifdef USE_BINARY_SENSOR for (auto *obj : App.get_binary_sensors()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->binary_sensor_json(obj, obj->state).c_str(), "state"); #endif #ifdef USE_FAN for (auto *obj : App.get_fans()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->fan_json(obj).c_str(), "state"); #endif #ifdef USE_LIGHT for (auto *obj : App.get_lights()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->light_json(obj).c_str(), "state"); #endif #ifdef USE_TEXT_SENSOR for (auto *obj : App.get_text_sensors()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->text_sensor_json(obj, obj->state).c_str(), "state"); #endif #ifdef USE_COVER for (auto *obj : App.get_covers()) - if (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->cover_json(obj).c_str(), "state"); #endif + +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) + if (this->include_internal_ || !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 (this->include_internal_ || !obj->is_internal()) + client->send(this->select_json(obj, obj->state).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -123,23 +152,24 @@ void WebServer::setup() { #endif this->base_->add_handler(&this->events_); this->base_->add_handler(this); - this->base_->add_ota_handler(); + + if (this->allow_ota_) + this->base_->add_ota_handler(); this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } 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 @@ -158,44 +188,82 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) - write_row(stream, obj, "sensor", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "sensor", ""); #endif #ifdef USE_SWITCH for (auto *obj : App.get_switches()) - write_row(stream, obj, "switch", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "switch", ""); +#endif + +#ifdef USE_BUTTON + for (auto *obj : App.get_buttons()) + write_row(stream, obj, "button", ""); #endif #ifdef USE_BINARY_SENSOR for (auto *obj : App.get_binary_sensors()) - write_row(stream, obj, "binary_sensor", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "binary_sensor", ""); #endif #ifdef USE_FAN for (auto *obj : App.get_fans()) - write_row(stream, obj, "fan", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "fan", ""); #endif #ifdef USE_LIGHT for (auto *obj : App.get_lights()) - write_row(stream, obj, "light", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "light", ""); #endif #ifdef USE_TEXT_SENSOR for (auto *obj : App.get_text_sensors()) - write_row(stream, obj, "text_sensor", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "text_sensor", ""); #endif #ifdef USE_COVER for (auto *obj : App.get_covers()) - write_row(stream, obj, "cover", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "cover", ""); +#endif + +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "number", ""); +#endif + +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { + select::Select *select = (select::Select *) obj; + stream.print(""); + }); #endif stream->print(F("

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

" - "

OTA Update

" - "

Debug Log

"));
+                  "REST API documentation.

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

OTA Update

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

Debug Log

"));
+
 #ifdef WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
     stream->print(F(""));
@@ -237,10 +305,8 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
 void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
   this->events_.send(this->sensor_json(obj, state).c_str(), "state");
 }
-void WebServer::handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (sensor::Sensor *obj : App.get_sensors()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
     std::string data = this->sensor_json(obj, obj->state);
@@ -262,13 +328,11 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value) {
 #endif
 
 #ifdef USE_TEXT_SENSOR
-void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) {
+void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
   this->events_.send(this->text_sensor_json(obj, state).c_str(), "state");
 }
-void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
     std::string data = this->text_sensor_json(obj, obj->state);
@@ -297,10 +361,8 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value) {
     root["value"] = value;
   });
 }
-void WebServer::handle_switch_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (switch_::Switch *obj : App.get_switches()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
 
@@ -325,10 +387,26 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, UrlMatch m
 }
 #endif
 
+#ifdef USE_BUTTON
+void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+  for (button::Button *obj : App.get_buttons()) {
+    if (obj->get_object_id() != match.id)
+      continue;
+
+    if (request->method() == HTTP_POST && match.method == "press") {
+      this->defer([obj]() { obj->press(); });
+      request->send(200);
+    } else {
+      request->send(404);
+    }
+    return;
+  }
+  request->send(404);
+}
+#endif
+
 #ifdef USE_BINARY_SENSOR
 void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
-  if (obj->is_internal())
-    return;
   this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state");
 }
 std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) {
@@ -338,10 +416,8 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
     root["value"] = value;
   });
 }
-void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
     std::string data = this->binary_sensor_json(obj, obj->state);
@@ -353,37 +429,37 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, Url
 #endif
 
 #ifdef USE_FAN
-void WebServer::on_fan_update(fan::FanState *obj) {
-  if (obj->is_internal())
-    return;
-  this->events_.send(this->fan_json(obj).c_str(), "state");
-}
+void WebServer::on_fan_update(fan::FanState *obj) { this->events_.send(this->fan_json(obj).c_str(), "state"); }
 std::string WebServer::fan_json(fan::FanState *obj) {
   return json::build_json([obj](JsonObject &root) {
     root["id"] = "fan-" + obj->get_object_id();
     root["state"] = obj->state ? "ON" : "OFF";
     root["value"] = obj->state;
-    if (obj->get_traits().supports_speed()) {
-      switch (obj->speed) {
-        case fan::FAN_SPEED_LOW:
+    const auto traits = obj->get_traits();
+    if (traits.supports_speed()) {
+      root["speed_level"] = obj->speed;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+      // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
+      switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) {
+        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;
       }
+#pragma GCC diagnostic pop
     }
     if (obj->get_traits().supports_oscillation())
       root["oscillation"] = obj->oscillating;
   });
 }
-void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (fan::FanState *obj : App.get_fans()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
 
@@ -397,7 +473,19 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch matc
       auto call = obj->turn_on();
       if (request->hasParam("speed")) {
         String speed = request->getParam("speed")->value();
-        call.set_speed(speed.c_str());
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+        call.set_speed(speed.c_str());  // NOLINT(clang-diagnostic-deprecated-declarations)
+#pragma GCC diagnostic pop
+      }
+      if (request->hasParam("speed_level")) {
+        String speed_level = request->getParam("speed_level")->value();
+        auto val = parse_number(speed_level.c_str());
+        if (!val.has_value()) {
+          ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str());
+          return;
+        }
+        call.set_speed(*val);
       }
       if (request->hasParam("oscillation")) {
         String speed = request->getParam("oscillation")->value();
@@ -432,15 +520,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch matc
 #endif
 
 #ifdef USE_LIGHT
-void WebServer::on_light_update(light::LightState *obj) {
-  if (obj->is_internal())
-    return;
-  this->events_.send(this->light_json(obj).c_str(), "state");
-}
-void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::on_light_update(light::LightState *obj) { this->events_.send(this->light_json(obj).c_str(), "state"); }
+void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (light::LightState *obj : App.get_lights()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
 
@@ -501,21 +583,15 @@ 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
 
 #ifdef USE_COVER
-void WebServer::on_cover_update(cover::Cover *obj) {
-  if (obj->is_internal())
-    return;
-  this->events_.send(this->cover_json(obj).c_str(), "state");
-}
-void WebServer::handle_cover_request(AsyncWebServerRequest *request, UrlMatch match) {
+void WebServer::on_cover_update(cover::Cover *obj) { this->events_.send(this->cover_json(obj).c_str(), "state"); }
+void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (cover::Cover *obj : App.get_covers()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
 
@@ -568,6 +644,73 @@ 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->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->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;
@@ -595,6 +738,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
     return true;
 #endif
 
+#ifdef USE_BUTTON
+  if (request->method() == HTTP_POST && match.domain == "button")
+    return true;
+#endif
+
 #ifdef USE_BINARY_SENSOR
   if (request->method() == HTTP_GET && match.domain == "binary_sensor")
     return true;
@@ -620,13 +768,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;
@@ -661,6 +815,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
   }
 #endif
 
+#ifdef USE_BUTTON
+  if (match.domain == "button") {
+    this->handle_button_request(request, match);
+    return;
+  }
+#endif
+
 #ifdef USE_BINARY_SENSOR
   if (match.domain == "binary_sensor") {
     this->handle_binary_sensor_request(request, match);
@@ -695,9 +856,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 b3bf2ef7f7..8edb4237a2 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
    *
@@ -60,6 +58,18 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
    */
   void set_js_include(const char *js_include);
 
+  /** Determine whether internal components should be displayed on the web server.
+   * Defaults to false.
+   *
+   * @param include_internal Whether internal components should be displayed.
+   */
+  void set_include_internal(bool include_internal) { include_internal_ = include_internal; }
+  /** Set whether or not the webserver should expose the OTA form and handler.
+   *
+   * @param allow_ota.
+   */
+  void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; }
+
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these)
   /// Setup the internal web server and register handlers.
@@ -83,12 +93,10 @@ 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/'.
-  void handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the sensor state with its value as a JSON string.
   std::string sensor_json(sensor::Sensor *obj, float value);
@@ -98,17 +106,22 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   void on_switch_update(switch_::Switch *obj, bool state) override;
 
   /// Handle a switch request under '/switch//'.
-  void handle_switch_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the switch state with its value as a JSON string.
   std::string switch_json(switch_::Switch *obj, bool value);
 #endif
 
+#ifdef USE_BUTTON
+  /// Handle a button request under '/button//press'.
+  void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
+#endif
+
 #ifdef USE_BINARY_SENSOR
   void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
 
   /// Handle a binary sensor request under '/binary_sensor/'.
-  void handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the binary sensor state with its value as a JSON string.
   std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value);
@@ -118,7 +131,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   void on_fan_update(fan::FanState *obj) override;
 
   /// Handle a fan request under '/fan//'.
-  void handle_fan_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the fan state as a JSON string.
   std::string fan_json(fan::FanState *obj);
@@ -128,17 +141,17 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   void on_light_update(light::LightState *obj) override;
 
   /// Handle a light request under '/light//'.
-  void handle_light_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the light state as a JSON string.
   std::string light_json(light::LightState *obj);
 #endif
 
 #ifdef USE_TEXT_SENSOR
-  void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override;
+  void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
 
   /// Handle a text sensor request under '/text_sensor/'.
-  void handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the text sensor state with its value as a JSON string.
   std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value);
@@ -148,12 +161,30 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   void on_cover_update(cover::Cover *obj) override;
 
   /// Handle a cover request under '/cover//'.
-  void handle_cover_request(AsyncWebServerRequest *request, UrlMatch match);
+  void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
 
   /// Dump the cover state as a JSON string.
   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,13 +195,15 @@ 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};
   const char *js_include_{nullptr};
+  bool include_internal_{false};
+  bool allow_ota_{true};
 };
 
 }  // 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 d2faaf7162..14fb033a56 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -3,24 +3,29 @@ import esphome.codegen as cg
 from esphome.const import CONF_ID
 from esphome.core import coroutine_with_priority, CORE
 
-DEPENDENCIES = ['network']
-AUTO_LOAD = ['async_tcp']
+CODEOWNERS = ["@OttoWinter"]
+DEPENDENCIES = ["network"]
+AUTO_LOAD = ["async_tcp"]
 
-web_server_base_ns = cg.esphome_ns.namespace('web_server_base')
-WebServerBase = web_server_base_ns.class_('WebServerBase', cg.Component)
+web_server_base_ns = cg.esphome_ns.namespace("web_server_base")
+WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component)
 
-CONF_WEB_SERVER_BASE_ID = 'web_server_base_id'
-CONFIG_SCHEMA = cv.Schema({
-    cv.GenerateID(): cv.declare_id(WebServerBase),
-})
+CONF_WEB_SERVER_BASE_ID = "web_server_base_id"
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(WebServerBase),
+    }
+)
 
 
 @coroutine_with_priority(65.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CORE.is_esp32:
-        cg.add_library('FS', None)
-    # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json
-    cg.add_library('ESPAsyncWebServer-esphome', '1.2.6')
+        cg.add_library("WiFi", None)
+        cg.add_library("FS", None)
+        cg.add_library("Update", None)
+    # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json
+    cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0")
diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp
index b7548504e3..3c269b28b8 100644
--- a/esphome/components/web_server_base/web_server_base.cpp
+++ b/esphome/components/web_server_base/web_server_base.cpp
@@ -1,19 +1,32 @@
+#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
 
 namespace esphome {
 namespace web_server_base {
 
-static const char *TAG = "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;
@@ -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/climate.py b/esphome/components/whirlpool/climate.py
index 1083b86618..c5b953c46f 100644
--- a/esphome/components/whirlpool/climate.py
+++ b/esphome/components/whirlpool/climate.py
@@ -3,24 +3,27 @@ import esphome.config_validation as cv
 from esphome.components import climate_ir
 from esphome.const import CONF_ID, CONF_MODEL
 
-AUTO_LOAD = ['climate_ir']
+AUTO_LOAD = ["climate_ir"]
+CODEOWNERS = ["@glmnet"]
 
-whirlpool_ns = cg.esphome_ns.namespace('whirlpool')
-WhirlpoolClimate = whirlpool_ns.class_('WhirlpoolClimate', climate_ir.ClimateIR)
+whirlpool_ns = cg.esphome_ns.namespace("whirlpool")
+WhirlpoolClimate = whirlpool_ns.class_("WhirlpoolClimate", climate_ir.ClimateIR)
 
-Model = whirlpool_ns.enum('Model')
+Model = whirlpool_ns.enum("Model")
 MODELS = {
-    'DG11J1-3A': Model.MODEL_DG11J1_3A,
-    'DG11J1-91': Model.MODEL_DG11J1_91,
+    "DG11J1-3A": Model.MODEL_DG11J1_3A,
+    "DG11J1-91": Model.MODEL_DG11J1_91,
 }
 
-CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({
-    cv.GenerateID(): cv.declare_id(WhirlpoolClimate),
-    cv.Optional(CONF_MODEL, default='DG11J1-3A'): cv.enum(MODELS, upper=True)
-})
+CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
+    {
+        cv.GenerateID(): cv.declare_id(WhirlpoolClimate),
+        cv.Optional(CONF_MODEL, default="DG11J1-3A"): cv.enum(MODELS, upper=True),
+    }
+)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
     cg.add(var.set_model(config[CONF_MODEL]))
diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp
index 0956f816ce..d705b42a8c 100644
--- a/esphome/components/whirlpool/whirlpool.cpp
+++ b/esphome/components/whirlpool/whirlpool.cpp
@@ -4,7 +4,7 @@
 namespace esphome {
 namespace whirlpool {
 
-static const char *TAG = "whirlpool.climate";
+static const char *const TAG = "whirlpool.climate";
 
 const uint16_t WHIRLPOOL_HEADER_MARK = 9000;
 const uint16_t WHIRLPOOL_HEADER_SPACE = 4494;
@@ -41,14 +41,14 @@ void WhirlpoolClimate::transmit_state() {
   remote_state[18] = 0x08;
 
   auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
-  if (powered_on != this->powered_on_assumed_) {
+  if (powered_on != this->powered_on_assumed) {
     // Set power toggle command
     remote_state[2] = 4;
     remote_state[15] = 1;
-    this->powered_on_assumed_ = powered_on;
+    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
@@ -81,7 +81,7 @@ void WhirlpoolClimate::transmit_state() {
   remote_state[3] |= (uint8_t)(temp - this->temperature_min_()) << 4;
 
   // Fan speed
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       remote_state[2] |= WHIRLPOOL_FAN_HIGH;
       break;
@@ -105,7 +105,7 @@ void WhirlpoolClimate::transmit_state() {
   }
 
   // Checksum
-  for (uint8_t i = 2; i < 12; i++)
+  for (uint8_t i = 2; i < 13; i++)
     remote_state[13] ^= remote_state[i];
   for (uint8_t i = 14; i < 20; i++)
     remote_state[20] ^= remote_state[i];
@@ -184,7 +184,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
   uint8_t checksum13 = 0;
   uint8_t checksum20 = 0;
   // Calculate  checksum and compare with signal value.
-  for (uint8_t i = 2; i < 12; i++)
+  for (uint8_t i = 2; i < 13; i++)
     checksum13 ^= remote_state[i];
   for (uint8_t i = 14; i < 20; i++)
     checksum20 ^= remote_state[i];
@@ -215,14 +215,14 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
 
     if (powered_on) {
       this->mode = climate::CLIMATE_MODE_OFF;
-      this->powered_on_assumed_ = false;
+      this->powered_on_assumed = false;
     } else {
-      this->powered_on_assumed_ = true;
+      this->powered_on_assumed = true;
     }
   }
 
   // Set received mode
-  if (powered_on_assumed_) {
+  if (powered_on_assumed) {
     auto mode = remote_state[3] & 0x7;
     ESP_LOGV(TAG, "Mode: %02X", mode);
     switch (mode) {
@@ -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/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h
index 44116b340c..7f31894df9 100644
--- a/esphome/components/whirlpool/whirlpool.h
+++ b/esphome/components/whirlpool/whirlpool.h
@@ -28,7 +28,7 @@ class WhirlpoolClimate : public climate_ir::ClimateIR {
   void setup() override {
     climate_ir::ClimateIR::setup();
 
-    this->powered_on_assumed_ = this->mode != climate::CLIMATE_MODE_OFF;
+    this->powered_on_assumed = this->mode != climate::CLIMATE_MODE_OFF;
   }
 
   /// Override control to change settings of the climate device.
@@ -39,15 +39,15 @@ class WhirlpoolClimate : public climate_ir::ClimateIR {
 
   void set_model(Model model) { this->model_ = model; }
 
+  // used to track when to send the power toggle command
+  bool powered_on_assumed;
+
  protected:
   /// Transmit via IR the state of this climate controller.
   void transmit_state() override;
   /// Handle received IR Buffer
   bool on_receive(remote_base::RemoteReceiveData data) override;
 
-  // used to track when to send the power toggle command
-  bool powered_on_assumed_;
-
   bool send_swing_cmd_{false};
   Model model_;
 
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index d3c7e51603..a24791b458 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -1,28 +1,56 @@
 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.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \
-    CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \
-    CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \
-    CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY
+from esphome.const import (
+    CONF_AP,
+    CONF_BSSID,
+    CONF_CHANNEL,
+    CONF_DNS1,
+    CONF_DNS2,
+    CONF_DOMAIN,
+    CONF_FAST_CONNECT,
+    CONF_GATEWAY,
+    CONF_HIDDEN,
+    CONF_ID,
+    CONF_MANUAL_IP,
+    CONF_NETWORKS,
+    CONF_PASSWORD,
+    CONF_POWER_SAVE_MODE,
+    CONF_REBOOT_TIMEOUT,
+    CONF_SSID,
+    CONF_STATIC_IP,
+    CONF_SUBNET,
+    CONF_USE_ADDRESS,
+    CONF_PRIORITY,
+    CONF_IDENTITY,
+    CONF_CERTIFICATE_AUTHORITY,
+    CONF_CERTIFICATE,
+    CONF_KEY,
+    CONF_USERNAME,
+    CONF_EAP,
+)
 from esphome.core import CORE, HexInt, coroutine_with_priority
+from esphome.components.network import IPAddress
+from . import wpa2_eap
 
-AUTO_LOAD = ['network']
 
-wifi_ns = cg.esphome_ns.namespace('wifi')
-IPAddress = cg.global_ns.class_('IPAddress')
-ManualIP = wifi_ns.struct('ManualIP')
-WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component)
-WiFiAP = wifi_ns.struct('WiFiAP')
+AUTO_LOAD = ["network"]
 
-WiFiPowerSaveMode = wifi_ns.enum('WiFiPowerSaveMode')
+wifi_ns = cg.esphome_ns.namespace("wifi")
+EAPAuth = wifi_ns.struct("EAPAuth")
+ManualIP = wifi_ns.struct("ManualIP")
+WiFiComponent = wifi_ns.class_("WiFiComponent", cg.Component)
+WiFiAP = wifi_ns.struct("WiFiAP")
+
+WiFiPowerSaveMode = wifi_ns.enum("WiFiPowerSaveMode")
 WIFI_POWER_SAVE_MODES = {
-    'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE,
-    'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
-    'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
+    "NONE": WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE,
+    "LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
+    "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
 }
-WiFiConnectedCondition = wifi_ns.class_('WiFiConnectedCondition', Condition)
+WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
 
 
 def validate_password(value):
@@ -45,53 +73,145 @@ def validate_channel(value):
     return value
 
 
-AP_MANUAL_IP_SCHEMA = cv.Schema({
-    cv.Required(CONF_STATIC_IP): cv.ipv4,
-    cv.Required(CONF_GATEWAY): cv.ipv4,
-    cv.Required(CONF_SUBNET): cv.ipv4,
-})
+AP_MANUAL_IP_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_STATIC_IP): cv.ipv4,
+        cv.Required(CONF_GATEWAY): cv.ipv4,
+        cv.Required(CONF_SUBNET): cv.ipv4,
+    }
+)
 
-STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({
-    cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
-    cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
-})
+STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend(
+    {
+        cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
+        cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
+    }
+)
 
-WIFI_NETWORK_BASE = cv.Schema({
-    cv.GenerateID(): cv.declare_id(WiFiAP),
-    cv.Optional(CONF_SSID): cv.ssid,
-    cv.Optional(CONF_PASSWORD): validate_password,
-    cv.Optional(CONF_CHANNEL): validate_channel,
-    cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
-})
+EAP_AUTH_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.Optional(CONF_IDENTITY): cv.string_strict,
+            cv.Optional(CONF_USERNAME): cv.string_strict,
+            cv.Optional(CONF_PASSWORD): cv.string_strict,
+            cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
+            cv.Inclusive(
+                CONF_CERTIFICATE, "certificate_and_key"
+            ): wpa2_eap.validate_certificate,
+            # Only validate as file first because we need the password to load it
+            # Actual validation happens in validate_eap.
+            cv.Inclusive(CONF_KEY, "certificate_and_key"): cv.file_,
+        }
+    ),
+    wpa2_eap.validate_eap,
+    cv.has_at_least_one_key(CONF_IDENTITY, CONF_CERTIFICATE),
+)
 
-CONF_AP_TIMEOUT = 'ap_timeout'
-WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
-    cv.Optional(CONF_AP_TIMEOUT, default='1min'): cv.positive_time_period_milliseconds,
-})
+WIFI_NETWORK_BASE = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(WiFiAP),
+        cv.Optional(CONF_SSID): cv.ssid,
+        cv.Optional(CONF_PASSWORD): validate_password,
+        cv.Optional(CONF_CHANNEL): validate_channel,
+        cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
+    }
+)
 
-WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
-    cv.Optional(CONF_BSSID): cv.mac_address,
-    cv.Optional(CONF_HIDDEN): cv.boolean,
-    cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
-})
+CONF_AP_TIMEOUT = "ap_timeout"
+WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend(
+    {
+        cv.Optional(
+            CONF_AP_TIMEOUT, default="1min"
+        ): cv.positive_time_period_milliseconds,
+    }
+)
+
+WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
+    {
+        cv.Optional(CONF_BSSID): cv.mac_address,
+        cv.Optional(CONF_HIDDEN): cv.boolean,
+        cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
+        cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
+    }
+)
 
 
-def validate(config):
+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()
+    has_improv_serial = "improv_serial" in fv.full_config.get()
+    if not (has_sta or has_ap or has_improv or has_improv_serial):
+        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",
+    ]:
+        if conflicting not in fv.full_config.get():
+            continue
+
+        try:
+            # Only arduino 1.0.5+ and esp-idf impacted
+            cv.require_framework_version(
+                esp32_arduino=cv.Version(1, 0, 5),
+                esp_idf=cv.Version(4, 0, 0),
+            )(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!")
 
     if CONF_SSID in config:
+        # Automatically move single network to 'networks' section
+        config = config.copy()
         network = {CONF_SSID: config.pop(CONF_SSID)}
         if CONF_PASSWORD in config:
             network[CONF_PASSWORD] = config.pop(CONF_PASSWORD)
+        if CONF_EAP in config:
+            network[CONF_EAP] = config.pop(CONF_EAP)
         if CONF_NETWORKS in config:
-            raise cv.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please "
-                             "copy your network into the 'networks:' key")
+            raise cv.Invalid(
+                "You cannot use the 'ssid:' option together with 'networks:'. Please "
+                "copy your network into the 'networks:' key"
+            )
         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, [])
@@ -101,36 +221,81 @@ def validate(config):
             raise cv.Invalid("Fast connect can only be used with one network!")
 
     if CONF_USE_ADDRESS not in config:
+        use_address = CORE.name + config[CONF_DOMAIN]
         if CONF_MANUAL_IP in config:
             use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP])
-        else:
-            use_address = CORE.name + config[CONF_DOMAIN]
+        elif CONF_NETWORKS in config:
+            ips = set(
+                str(net[CONF_MANUAL_IP][CONF_STATIC_IP])
+                for net in config[CONF_NETWORKS]
+                if CONF_MANUAL_IP in net
+            )
+            if len(ips) > 1:
+                raise cv.Invalid(
+                    "Must specify use_address when using multiple static IP addresses."
+                )
+            if len(ips) == 1:
+                use_address = next(iter(ips))
+
         config[CONF_USE_ADDRESS] = use_address
 
     return config
 
 
-CONF_OUTPUT_POWER = 'output_power'
-CONFIG_SCHEMA = cv.All(cv.Schema({
-    cv.GenerateID(): cv.declare_id(WiFiComponent),
-    cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA),
+CONF_OUTPUT_POWER = "output_power"
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(WiFiComponent),
+            cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA),
+            cv.Optional(CONF_SSID): cv.ssid,
+            cv.Optional(CONF_PASSWORD): validate_password,
+            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_DOMAIN, default=".local"): cv.domain_name,
+            cv.Optional(
+                CONF_REBOOT_TIMEOUT, default="15min"
+            ): cv.positive_time_period_milliseconds,
+            cv.SplitDefault(
+                CONF_POWER_SAVE_MODE, esp8266="none", esp32="light"
+            ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
+            cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
+            cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
+            cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
+                cv.decibel, cv.float_range(min=10.0, max=20.5)
+            ),
+            cv.Optional("enable_mdns"): cv.invalid(
+                "This option has been removed. Please use the [disabled] option under the "
+                "new mdns component instead."
+            ),
+        }
+    ),
+    _validate,
+)
 
-    cv.Optional(CONF_SSID): cv.ssid,
-    cv.Optional(CONF_PASSWORD): validate_password,
-    cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
 
-    cv.Optional(CONF_AP): WIFI_NETWORK_AP,
-    cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
-    cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds,
-    cv.SplitDefault(CONF_POWER_SAVE_MODE, esp8266='none', esp32='light'):
-        cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
-    cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
-    cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
-    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"),
-}), validate)
+def eap_auth(config):
+    if config is None:
+        return None
+    ca_cert = ""
+    if CONF_CERTIFICATE_AUTHORITY in config:
+        ca_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE_AUTHORITY])
+    client_cert = ""
+    if CONF_CERTIFICATE in config:
+        client_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE])
+    key = ""
+    if CONF_KEY in config:
+        key = wpa2_eap.read_relative_config_path(config[CONF_KEY])
+    return cg.StructInitializer(
+        EAPAuth,
+        ("identity", config.get(CONF_IDENTITY, "")),
+        ("username", config.get(CONF_USERNAME, "")),
+        ("password", config.get(CONF_PASSWORD, "")),
+        ("ca_cert", ca_cert),
+        ("client_cert", client_cert),
+        ("client_key", key),
+    )
 
 
 def safe_ip(ip):
@@ -144,11 +309,11 @@ def manual_ip(config):
         return None
     return cg.StructInitializer(
         ManualIP,
-        ('static_ip', safe_ip(config[CONF_STATIC_IP])),
-        ('gateway', safe_ip(config[CONF_GATEWAY])),
-        ('subnet', safe_ip(config[CONF_SUBNET])),
-        ('dns1', safe_ip(config.get(CONF_DNS1))),
-        ('dns2', safe_ip(config.get(CONF_DNS2))),
+        ("static_ip", safe_ip(config[CONF_STATIC_IP])),
+        ("gateway", safe_ip(config[CONF_GATEWAY])),
+        ("subnet", safe_ip(config[CONF_SUBNET])),
+        ("dns1", safe_ip(config.get(CONF_DNS1))),
+        ("dns2", safe_ip(config.get(CONF_DNS2))),
     )
 
 
@@ -158,6 +323,9 @@ def wifi_network(config, static_ip):
         cg.add(ap.set_ssid(config[CONF_SSID]))
     if CONF_PASSWORD in config:
         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("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:
@@ -173,12 +341,13 @@ def wifi_network(config, static_ip):
 
 
 @coroutine_with_priority(60.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
 
     for network in config.get(CONF_NETWORKS, []):
-        cg.add(var.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP))))
+        ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
+        cg.add(var.add_sta(wifi_network(network, ip_config)))
 
     if CONF_AP in config:
         conf = config[CONF_AP]
@@ -193,14 +362,16 @@ def to_code(config):
         cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
 
     if CORE.is_esp8266:
-        cg.add_library('ESP8266WiFi', None)
+        cg.add_library("ESP8266WiFi", None)
+    elif CORE.is_esp32 and CORE.using_arduino:
+        cg.add_library("WiFi", None)
 
-    cg.add_define('USE_WIFI')
+    cg.add_define("USE_WIFI")
 
     # Register at end for OTA safe mode
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
 
-@automation.register_condition('wifi.connected', WiFiConnectedCondition, cv.Schema({}))
-def wifi_connected_to_code(config, condition_id, template_arg, args):
-    yield cg.new_Pvariable(condition_id, template_arg)
+@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
+async def wifi_connected_to_code(config, condition_id, template_arg, args):
+    return cg.new_Pvariable(condition_id, template_arg)
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 40f12a8adc..36944e3633 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"
 
@@ -22,10 +22,14 @@
 #include "esphome/components/captive_portal/captive_portal.h"
 #endif
 
+#ifdef USE_IMPROV
+#include "esphome/components/esp32_improv/esp32_improv_component.h"
+#endif
+
 namespace esphome {
 namespace wifi {
 
-static const char *TAG = "wifi";
+static const char *const TAG = "wifi";
 
 float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
 
@@ -34,6 +38,19 @@ void WiFiComponent::setup() {
   this->last_connected_ = millis();
   this->wifi_pre_setup_();
 
+  uint32_t hash = fnv1_hash(App.get_compilation_time());
+  this->pref_ = global_preferences->make_preference(hash, true);
+
+  SavedWifiSettings save{};
+  if (this->pref_.load(&save)) {
+    ESP_LOGD(TAG, "Loaded saved wifi settings: %s", save.ssid);
+
+    WiFiAP sta{};
+    sta.set_ssid(save.ssid);
+    sta.set_password(save.password);
+    this->set_sta(sta);
+  }
+
   if (this->has_sta()) {
     this->wifi_sta_pre_setup_();
     if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
@@ -60,14 +77,16 @@ void WiFiComponent::setup() {
       captive_portal::global_captive_portal->start();
 #endif
   }
-
-  this->wifi_apply_hostname_();
-#ifdef ARDUINO_ARCH_ESP32
-  network_setup_mdns();
+#ifdef USE_IMPROV
+  if (esp32_improv::global_improv_component != nullptr)
+    if (this->wifi_mode_(true, {}))
+      esp32_improv::global_improv_component->start();
 #endif
+  this->wifi_apply_hostname_();
 }
 
 void WiFiComponent::loop() {
+  this->wifi_loop_();
   const uint32_t now = millis();
 
   if (this->has_sta()) {
@@ -98,6 +117,7 @@ void WiFiComponent::loop() {
       case WIFI_COMPONENT_STATE_STA_CONNECTED: {
         if (!this->is_connected()) {
           ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting...");
+          this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
           this->retry_connect();
         } else {
           this->status_clear_warning();
@@ -121,6 +141,14 @@ void WiFiComponent::loop() {
       }
     }
 
+#ifdef USE_IMPROV
+    if (esp32_improv::global_improv_component != nullptr)
+      if (!this->is_connected())
+        if (this->wifi_mode_(true, {}))
+          esp32_improv::global_improv_component->start();
+
+#endif
+
     if (!this->has_ap() && this->reboot_timeout_ != 0) {
       if (now - this->last_connected_ > this->reboot_timeout_) {
         ESP_LOGE(TAG, "Can't connect to WiFi, rebooting...");
@@ -128,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 {};
@@ -157,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());
-#ifdef ARDUINO_ARCH_ESP8266
-  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;
@@ -182,12 +217,29 @@ 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->sta_.clear();
+  this->clear_sta();
   this->add_sta(ap);
 }
+void WiFiComponent::clear_sta() { this->sta_.clear(); }
+void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
+  SavedWifiSettings save{};
+  strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid));
+  strncpy(save.password, password.c_str(), sizeof(save.password));
+  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);
+}
 
 void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
   ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str());
@@ -200,7 +252,26 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
   } else {
     ESP_LOGV(TAG, "  BSSID: Not Set");
   }
-  ESP_LOGV(TAG, "  Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
+
+#ifdef USE_WIFI_WPA2_EAP
+  if (ap.get_eap().has_value()) {
+    ESP_LOGV(TAG, "  WPA2 Enterprise authentication configured:");
+    EAPAuth eap_config = ap.get_eap().value();
+    ESP_LOGV(TAG, "    Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
+    ESP_LOGV(TAG, "    Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
+    ESP_LOGV(TAG, "    Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
+    bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
+    bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
+    bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
+    ESP_LOGV(TAG, "    CA Cert:     %s", ca_cert_present ? "present" : "not present");
+    ESP_LOGV(TAG, "    Client Cert: %s", client_cert_present ? "present" : "not present");
+    ESP_LOGV(TAG, "    Client Key:  %s", client_key_present ? "present" : "not present");
+  } else {
+#endif
+    ESP_LOGV(TAG, "  Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
+#ifdef USE_WIFI_WPA2_EAP
+  }
+#endif
   if (ap.get_channel().has_value()) {
     ESP_LOGV(TAG, "  Channel: %u", *ap.get_channel());
   } else {
@@ -208,9 +279,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");
   }
@@ -231,7 +301,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
@@ -241,62 +311,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() {
@@ -357,16 +423,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())));
     }
   }
 
@@ -399,9 +464,17 @@ void WiFiComponent::check_scanning_finished() {
       connect_params.set_channel(scan_res.get_channel());
       connect_params.set_bssid(scan_res.get_bssid());
     }
-    // set manual IP+password (if any)
+    // copy manual IP (if set)
     connect_params.set_manual_ip(config.get_manual_ip());
+
+#ifdef USE_WIFI_WPA2_EAP
+    // copy EAP parameters (if set)
+    connect_params.set_eap(config.get_eap());
+#endif
+
+    // copy password (if set)
     connect_params.set_password(config.get_password());
+
     break;
   }
 
@@ -417,10 +490,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;
@@ -438,9 +511,12 @@ void WiFiComponent::check_connecting_finished() {
       ESP_LOGD(TAG, "Disabling AP...");
       this->wifi_mode_({}, false);
     }
-#ifdef ARDUINO_ARCH_ESP8266
-    network_setup_mdns(this->wifi_sta_ip_(), 0);
+#ifdef USE_IMPROV
+    if (this->is_esp32_improv_active_()) {
+      esp32_improv::global_improv_component->stop();
+    }
 #endif
+
     this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
     this->num_retried_ = 0;
     return;
@@ -459,26 +535,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() {
@@ -489,7 +562,8 @@ void WiFiComponent::retry_connect() {
   }
 
   delay(10);
-  if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) {
+  if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() &&
+      (this->num_retried_ > 5 || this->error_from_callback_)) {
     // If retry failed for more than 5 times, let's restart STA
     ESP_LOGW(TAG, "Restarting WiFi adapter...");
     this->wifi_mode_(false, {});
@@ -511,15 +585,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; }
 
@@ -535,24 +609,42 @@ bool WiFiComponent::is_captive_portal_active_() {
   return false;
 #endif
 }
+bool WiFiComponent::is_esp32_improv_active_() {
+#ifdef USE_IMPROV
+  return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active();
+#else
+  return false;
+#endif
+}
 
 void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
 void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
 void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; }
 void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
+#ifdef USE_WIFI_WPA2_EAP
+void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); }
+#endif
 void WiFiAP::set_channel(optional channel) { this->channel_ = channel; }
 void WiFiAP::set_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 USE_WIFI_WPA2_EAP
+const optional &WiFiAP::get_eap() const { return this->eap_; }
+#endif
 const optional &WiFiAP::get_channel() const { return this->channel_; }
 const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
 bool WiFiAP::get_hidden() const { return this->hidden_; }
 
-WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi,
-                               bool with_auth, bool is_hidden)
-    : bssid_(bssid), ssid_(ssid), channel_(channel), rssi_(rssi), with_auth_(with_auth), is_hidden_(is_hidden) {}
+WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
+                               bool is_hidden)
+    : bssid_(bssid),
+      ssid_(std::move(ssid)),
+      channel_(channel),
+      rssi_(rssi),
+      with_auth_(with_auth),
+      is_hidden_(is_hidden) {}
 bool WiFiScanResult::matches(const WiFiAP &config) {
   if (config.get_hidden()) {
     // User configured a hidden network, only match actually hidden networks
@@ -569,9 +661,21 @@ bool WiFiScanResult::matches(const WiFiAP &config) {
   // If BSSID configured, only match for correct BSSIDs
   if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
     return false;
-  // If PW given, only match for networks with auth (and vice versa)
+
+#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;
+
+  // BSSID does not require auth, but PSK or EAP credentials given
+  if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value()))
+    return false;
+#else
+  // If PSK given, only match for networks with auth (and vice versa)
   if (config.get_password().empty() == this->with_auth_)
     return false;
+#endif
+
   // If channel configured, only match networks on that channel.
   if (config.get_channel().has_value() && *config.get_channel() != this->channel_) {
     return false;
@@ -587,7 +691,7 @@ int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
 bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
 bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
 
-WiFiComponent *global_wifi_component;
+WiFiComponent *global_wifi_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 }  // namespace wifi
 }  // namespace esphome
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index d04e1c2ce0..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 
 };
@@ -27,6 +28,11 @@ extern "C" {
 namespace esphome {
 namespace wifi {
 
+struct SavedWifiSettings {
+  char ssid[33];
+  char password[65];
+} PACKED;  // NOLINT
+
 enum WiFiComponentState {
   /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */
   WIFI_COMPONENT_STATE_OFF = 0,
@@ -48,15 +54,35 @@ enum WiFiComponentState {
   WIFI_COMPONENT_STATE_AP,
 };
 
+enum class WiFiSTAConnectStatus : int {
+  IDLE,
+  CONNECTING,
+  CONNECTED,
+  ERROR_NETWORK_NOT_FOUND,
+  ERROR_CONNECT_FAILED,
+};
+
 /// 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.
+  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;
+  std::string password;
+  const char *ca_cert;  // optionally verify authentication server
+  // used for EAP-TLS
+  const char *client_cert;
+  const char *client_key;
+};
+#endif  // USE_WIFI_WPA2_EAP
+
 using bssid_t = std::array;
 
 class WiFiAP {
@@ -65,6 +91,9 @@ class WiFiAP {
   void set_bssid(bssid_t bssid);
   void set_bssid(optional bssid);
   void set_password(const std::string &password);
+#ifdef USE_WIFI_WPA2_EAP
+  void set_eap(optional eap_auth);
+#endif  // USE_WIFI_WPA2_EAP
   void set_channel(optional channel);
   void set_priority(float priority) { priority_ = priority; }
   void set_manual_ip(optional manual_ip);
@@ -72,6 +101,9 @@ class WiFiAP {
   const std::string &get_ssid() const;
   const optional &get_bssid() const;
   const std::string &get_password() const;
+#ifdef USE_WIFI_WPA2_EAP
+  const optional &get_eap() const;
+#endif  // USE_WIFI_WPA2_EAP
   const optional &get_channel() const;
   float get_priority() const { return priority_; }
   const optional &get_manual_ip() const;
@@ -81,6 +113,9 @@ class WiFiAP {
   std::string ssid_;
   optional bssid_;
   std::string password_;
+#ifdef USE_WIFI_WPA2_EAP
+  optional eap_;
+#endif  // USE_WIFI_WPA2_EAP
   optional channel_;
   float priority_{0};
   optional manual_ip_;
@@ -89,8 +124,7 @@ class WiFiAP {
 
 class WiFiScanResult {
  public:
-  WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi, bool with_auth,
-                 bool is_hidden);
+  WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
 
   bool matches(const WiFiAP &config);
 
@@ -127,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:
@@ -135,6 +173,7 @@ class WiFiComponent : public Component {
 
   void set_sta(const WiFiAP &ap);
   void add_sta(const WiFiAP &ap);
+  void clear_sta();
 
   /** Setup an Access Point that should be created if no connection to a station can be made.
    *
@@ -164,6 +203,7 @@ class WiFiComponent : public Component {
   void set_power_save_mode(WiFiPowerSaveMode power_save);
   void set_output_power(float output_power) { output_power_ = output_power; }
 
+  void save_wifi_sta(const std::string &ssid, const std::string &password);
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these)
   /// Setup WiFi interface.
@@ -179,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_)
@@ -211,38 +251,56 @@ 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_(WiFiAP ap);
+  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
   void wifi_event_callback_(system_event_id_t event, system_event_info_t info);
+#endif
   void wifi_scan_done_callback_();
 #endif
+#ifdef USE_ESP_IDF
+  void wifi_process_event_(IDFWiFiEvent *);
+#endif
 
   std::string use_address_;
   std::vector sta_;
@@ -250,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_;
@@ -263,9 +322,11 @@ class WiFiComponent : public Component {
   bool scan_done_{false};
   bool ap_setup_{false};
   optional output_power_;
+  ESPPreferenceObject pref_;
+  bool has_saved_wifi_settings_{false};
 };
 
-extern WiFiComponent *global_wifi_component;
+extern WiFiComponent *global_wifi_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 template class WiFiConnectedCondition : public Condition {
  public:
diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
similarity index 50%
rename from esphome/components/wifi/wifi_component_esp32.cpp
rename to esphome/components/wifi/wifi_component_esp32_arduino.cpp
index e345ab1671..e1332e3181 100644
--- a/esphome/components/wifi/wifi_component_esp32.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -1,51 +1,57 @@
 #include "wifi_component.h"
 
-#ifdef ARDUINO_ARCH_ESP32
+#ifdef USE_ESP32_FRAMEWORK_ARDUINO
 
 #include 
 
 #include 
 #include 
+#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"
 
 namespace esphome {
 namespace wifi {
 
-static const char *TAG = "wifi_esp32";
+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!");
@@ -89,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);
@@ -107,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);
@@ -130,41 +141,70 @@ 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_(WiFiAP ap) {
+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));
-  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;
@@ -187,8 +227,57 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
     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
+
   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);
@@ -215,7 +304,14 @@ const char *get_auth_mode_str(uint8_t mode) {
       return "UNKNOWN";
   }
 }
-std::string format_ip4_addr(const ip4_addr_t &ip) {
+
+#if ESP_IDF_VERSION_MAJOR >= 4
+using esphome_ip4_addr_t = esp_ip4_addr_t;
+#else
+using esphome_ip4_addr_t = ip4_addr_t;
+#endif
+
+std::string format_ip4_addr(const esphome_ip4_addr_t &ip) {
   char buf[20];
   sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
           uint8_t(ip.addr >> 24));
@@ -291,41 +387,104 @@ 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";
   }
 }
-void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_info_t info) {
+
+#if ESP_IDF_VERSION_MAJOR >= 4
+
+#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
       auto it = info.connected;
+#endif
       char buf[33];
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
       ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
                format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
+
       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
       auto it = info.disconnected;
+#endif
       char buf[33];
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
@@ -335,70 +494,100 @@ 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
       auto it = info.auth_change;
+#endif
       ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode),
                get_auth_mode_str(it.new_mode));
+      // Mitigate CVE-2020-12638
+      // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
+      if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
+        ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting...");
+        // we can't call retry_connect() from this context, so disconnect immediately
+        // and notify main thread with error_from_callback_
+        err_t err = esp_wifi_disconnect();
+        if (err != ESP_OK) {
+          ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err));
+        }
+        this->error_from_callback_ = true;
+      }
       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;
+#else
       auto it = info.sta_connected;
-      ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      auto &mac = it.mac;
+#endif
+      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;
+#else
       auto it = info.sta_disconnected;
-      ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      auto &mac = it.mac;
+#endif
+      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
       auto it = info.ap_probereqrecved;
+#endif
       ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
       break;
     }
     default:
       break;
   }
-
-  if (event == SYSTEM_EVENT_STA_DISCONNECTED) {
-    uint8_t reason = info.disconnected.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;
-    }
-  }
-  if (event == SYSTEM_EVENT_SCAN_DONE) {
-    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);
@@ -407,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, {}))
@@ -458,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);
@@ -478,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) {
@@ -508,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;
@@ -519,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);
@@ -537,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 deee578b4c..2021773209 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -1,32 +1,49 @@
 #include "wifi_component.h"
+#include "esphome/core/macros.h"
 
-#ifdef ARDUINO_ARCH_ESP8266
+#ifdef USE_ESP8266
 
 #include 
 
 #include 
 #include 
+#ifdef USE_WIFI_WPA2_EAP
+#include 
+#endif
 
 extern "C" {
 #include "lwip/err.h"
 #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"
 
 namespace esphome {
 namespace wifi {
 
-static const char *TAG = "wifi_esp8266";
+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();
@@ -102,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();
@@ -161,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 {};
@@ -197,7 +219,7 @@ bool WiFiComponent::wifi_apply_hostname_() {
   return ret;
 }
 
-bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
+bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // enable STA
   if (!this->wifi_mode_(true, {}))
     return false;
@@ -206,8 +228,8 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
 
   struct station_config conf {};
   memset(&conf, 0, sizeof(conf));
-  strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str());
-  strcpy(reinterpret_cast(conf.password), ap.get_password().c_str());
+  strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid));
+  strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password));
 
   if (ap.get_bssid().has_value()) {
     conf.bssid_set = 1;
@@ -216,10 +238,11 @@ bool WiFiComponent::wifi_sta_connect_(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 {
+    // Only allow auth modes with at least WPA
     conf.threshold.authmode = AUTH_WPA_PSK;
   }
   conf.threshold.rssi = -127;
@@ -238,8 +261,62 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
     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();
+    ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
+    if (ret) {
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", ret);
+    }
+    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) {
+      ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", ret);
+      }
+    }
+    // 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
+      ret = wifi_station_set_enterprise_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 (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", ret);
+      }
+    } else {
+      // in the absence of certs, assume this is username/password based
+      ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length());
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", ret);
+      }
+      ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length());
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", ret);
+      }
+    }
+    ret = wifi_station_set_wpa2_enterprise_auth(true);
+    if (ret) {
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret);
+    }
+  }
+#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();
@@ -264,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
@@ -295,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");
   }
 }
 
@@ -380,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: {
@@ -389,22 +477,36 @@ 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) {
+        ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting...");
+        // we can't call retry_connect() from this context, so disconnect immediately
+        // and notify main thread with error_from_callback_
+        wifi_station_disconnect();
+        global_wifi_component->error_from_callback_ = true;
+      }
       break;
     }
     case EVENT_STAMODE_GOT_IP: {
       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: {
@@ -426,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: {
@@ -481,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, {}))
@@ -511,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 {
@@ -521,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!");
@@ -574,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) {
@@ -590,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;
@@ -629,7 +736,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     return false;
 
   struct softap_config conf {};
-  strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str());
+  strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid));
   conf.ssid_len = static_cast(ap.get_ssid().size());
   conf.channel = ap.get_channel().value_or(1);
   conf.ssid_hidden = ap.get_hidden();
@@ -641,7 +748,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.password = 0;
   } else {
     conf.authmode = AUTH_WPA2_PSK;
-    strcpy(reinterpret_cast(conf.password), ap.get_password().c_str());
+    strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password));
   }
 
   ETS_UART_INTR_DISABLE();
@@ -660,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..5a81fd0a39
--- /dev/null
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -0,0 +1,909 @@
+#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) {  // NOLINT(bugprone-branch-clone)
+    // no data
+  } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_STOP) {  // NOLINT(bugprone-branch-clone)
+    // 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) {  // NOLINT(bugprone-branch-clone)
+    // 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) {  // NOLINT(bugprone-branch-clone)
+    // no data
+  } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STOP) {  // NOLINT(bugprone-branch-clone)
+    // 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);
+      }
+    }
+    err = esp_wifi_sta_wpa2_ent_enable();
+    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 && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
+    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
new file mode 100644
index 0000000000..3cb60e6175
--- /dev/null
+++ b/esphome/components/wifi/wpa2_eap.py
@@ -0,0 +1,152 @@
+"""Module for all WPA2 Utilities.
+
+The cryptography package is loaded lazily in the functions
+so that it doesn't crash if it's not installed.
+"""
+import logging
+from pathlib import Path
+
+from esphome.core import CORE
+import esphome.config_validation as cv
+from esphome.const import (
+    CONF_USERNAME,
+    CONF_IDENTITY,
+    CONF_PASSWORD,
+    CONF_CERTIFICATE,
+    CONF_KEY,
+)
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def validate_cryptography_installed():
+    try:
+        import cryptography
+    except ImportError as err:
+        raise cv.Invalid(
+            "This settings requires the cryptography python package. "
+            "Please install it with `pip install cryptography`"
+        ) from err
+
+    if cryptography.__version__[0] < "2":
+        raise cv.Invalid(
+            "Please update your python cryptography installation to least 2.x "
+            "(pip install -U cryptography)"
+        )
+
+
+def wrapped_load_pem_x509_certificate(value):
+    validate_cryptography_installed()
+
+    from cryptography import x509
+    from cryptography.hazmat.backends import default_backend
+
+    return x509.load_pem_x509_certificate(value.encode("UTF-8"), default_backend())
+
+
+def wrapped_load_pem_private_key(value, password):
+    validate_cryptography_installed()
+
+    from cryptography.hazmat.primitives.serialization import load_pem_private_key
+    from cryptography.hazmat.backends import default_backend
+
+    if password:
+        password = password.encode("UTF-8")
+    return load_pem_private_key(value.encode("UTF-8"), password, default_backend())
+
+
+def read_relative_config_path(value):
+    # pylint: disable=unspecified-encoding
+    return Path(CORE.relative_config_path(value)).read_text()
+
+
+def _validate_load_certificate(value):
+    value = cv.file_(value)
+    try:
+        contents = read_relative_config_path(value)
+        return wrapped_load_pem_x509_certificate(contents)
+    except ValueError as err:
+        raise cv.Invalid(f"Invalid certificate: {err}")
+
+
+def validate_certificate(value):
+    _validate_load_certificate(value)
+    # Validation result should be the path, not the loaded certificate
+    return value
+
+
+def _validate_load_private_key(key, cert_pw):
+    key = cv.file_(key)
+    try:
+        contents = read_relative_config_path(key)
+        return wrapped_load_pem_private_key(contents, cert_pw)
+    except ValueError as e:
+        raise cv.Invalid(
+            f"There was an error with the EAP 'password:' provided for 'key' {e}"
+        )
+    except TypeError as e:
+        raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}")
+
+
+def _check_private_key_cert_match(key, cert):
+    from cryptography.hazmat.primitives.asymmetric import rsa, ec
+
+    def check_match_a():
+        return key.public_key().public_numbers() == cert.public_key().public_numbers()
+
+    def check_match_b():
+        return key.public_key() == cert
+
+    private_key_types = {
+        rsa.RSAPrivateKey: check_match_a,
+        ec.EllipticCurvePrivateKey: check_match_a,
+    }
+
+    try:
+        # pylint: disable=no-name-in-module
+        from cryptography.hazmat.primitives.asymmetric import ed448, ed25519
+
+        private_key_types.update(
+            {
+                ed448.Ed448PrivateKey: check_match_b,
+                ed25519.Ed25519PrivateKey: check_match_b,
+            }
+        )
+    except ImportError:
+        # ed448, ed25519 not supported
+        pass
+
+    key_type = next((kt for kt in private_key_types if isinstance(key, kt)), None)
+    if key_type is None:
+        _LOGGER.warning(
+            "Unrecognised EAP 'certificate:' 'key:' pair format: %s. Proceed with caution!",
+            type(key),
+        )
+    elif not private_key_types[key_type]():
+        raise cv.Invalid("The provided EAP 'key' is not valid for the 'certificate'.")
+
+
+def validate_eap(value):
+    if CONF_USERNAME in value:
+        if CONF_IDENTITY not in value:
+            _LOGGER.info("EAP 'identity:' is not set, assuming username.")
+            value = value.copy()
+            value[CONF_IDENTITY] = value[CONF_USERNAME]
+        if CONF_PASSWORD not in value:
+            raise cv.Invalid(
+                "You cannot use the EAP 'username:' option without a 'password:'. "
+                "Please provide the 'password:' key"
+            )
+
+    if CONF_CERTIFICATE in value or CONF_KEY in value:
+        # Check the key is valid and for this certificate, just to check the user hasn't pasted
+        # the wrong thing. I write this after I spent a while debugging that exact issue.
+        # This may require a password to decrypt to key, so we should verify that at the same time.
+        cert_pw = value.get(CONF_PASSWORD)
+        key = _validate_load_private_key(value[CONF_KEY], cert_pw)
+
+        cert = _validate_load_certificate(value[CONF_CERTIFICATE])
+        _check_private_key_cert_match(key, cert)
+
+    return value
diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py
index 56670b4173..706a8967be 100644
--- a/esphome/components/wifi_info/text_sensor.py
+++ b/esphome/components/wifi_info/text_sensor.py
@@ -1,44 +1,91 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import text_sensor
-from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_SSID, CONF_MAC_ADDRESS
-from esphome.core import coroutine
+from esphome.const import (
+    CONF_BSSID,
+    CONF_ENTITY_CATEGORY,
+    CONF_ID,
+    CONF_IP_ADDRESS,
+    CONF_SCAN_RESULTS,
+    CONF_SSID,
+    CONF_MAC_ADDRESS,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+)
 
-DEPENDENCIES = ['wifi']
+DEPENDENCIES = ["wifi"]
 
-wifi_info_ns = cg.esphome_ns.namespace('wifi_info')
-IPAddressWiFiInfo = wifi_info_ns.class_('IPAddressWiFiInfo', text_sensor.TextSensor, cg.Component)
-SSIDWiFiInfo = wifi_info_ns.class_('SSIDWiFiInfo', text_sensor.TextSensor, cg.Component)
-BSSIDWiFiInfo = wifi_info_ns.class_('BSSIDWiFiInfo', text_sensor.TextSensor, cg.Component)
-MacAddressWifiInfo = wifi_info_ns.class_('MacAddressWifiInfo', text_sensor.TextSensor, cg.Component)
+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
+)
+MacAddressWifiInfo = wifi_info_ns.class_(
+    "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component
+)
 
-CONFIG_SCHEMA = cv.Schema({
-    cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({
-        cv.GenerateID(): cv.declare_id(IPAddressWiFiInfo),
-    }),
-    cv.Optional(CONF_SSID): text_sensor.TEXT_SENSOR_SCHEMA.extend({
-        cv.GenerateID(): cv.declare_id(SSIDWiFiInfo),
-    }),
-    cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend({
-        cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo),
-    }),
-    cv.Optional(CONF_MAC_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({
-        cv.GenerateID(): cv.declare_id(MacAddressWifiInfo),
-    })
-})
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(IPAddressWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
+            }
+        ),
+        cv.Optional(CONF_SCAN_RESULTS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(ScanResultsWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
+            }
+        ).extend(cv.polling_component_schema("60s")),
+        cv.Optional(CONF_SSID): text_sensor.TEXT_SENSOR_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(SSIDWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
+            }
+        ),
+        cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
+            }
+        ),
+        cv.Optional(CONF_MAC_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(MacAddressWifiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
+            }
+        ),
+    }
+)
 
 
-@coroutine
-def setup_conf(config, key):
+async def setup_conf(config, key):
     if key in config:
         conf = config[key]
         var = cg.new_Pvariable(conf[CONF_ID])
-        yield cg.register_component(var, conf)
-        yield text_sensor.register_text_sensor(var, conf)
+        await cg.register_component(var, conf)
+        await text_sensor.register_text_sensor(var, conf)
 
 
-def to_code(config):
-    yield setup_conf(config, CONF_IP_ADDRESS)
-    yield setup_conf(config, CONF_SSID)
-    yield setup_conf(config, CONF_BSSID)
-    yield setup_conf(config, CONF_MAC_ADDRESS)
+async def to_code(config):
+    await setup_conf(config, CONF_IP_ADDRESS)
+    await setup_conf(config, CONF_SSID)
+    await setup_conf(config, CONF_BSSID)
+    await setup_conf(config, CONF_MAC_ADDRESS)
+    await setup_conf(config, CONF_SCAN_RESULTS)
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
index 08a69998fb..0b73de68de 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
@@ -4,9 +4,10 @@
 namespace esphome {
 namespace wifi_info {
 
-static const char *TAG = "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 1cc58009af..2097c21bd7 100644
--- a/esphome/components/wifi_signal/sensor.py
+++ b/esphome/components/wifi_signal/sensor.py
@@ -1,18 +1,38 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor
-from esphome.const import CONF_ID, ICON_WIFI, UNIT_DECIBEL
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_SIGNAL_STRENGTH,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_DECIBEL_MILLIWATT,
+)
 
-DEPENDENCIES = ['wifi']
-wifi_signal_ns = cg.esphome_ns.namespace('wifi_signal')
-WiFiSignalSensor = wifi_signal_ns.class_('WiFiSignalSensor', sensor.Sensor, cg.PollingComponent)
+DEPENDENCIES = ["wifi"]
+wifi_signal_ns = cg.esphome_ns.namespace("wifi_signal")
+WiFiSignalSensor = wifi_signal_ns.class_(
+    "WiFiSignalSensor", sensor.Sensor, cg.PollingComponent
+)
 
-CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DECIBEL, ICON_WIFI, 0).extend({
-    cv.GenerateID(): cv.declare_id(WiFiSignalSensor),
-}).extend(cv.polling_component_schema('60s'))
+CONFIG_SCHEMA = (
+    sensor.sensor_schema(
+        unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
+        state_class=STATE_CLASS_MEASUREMENT,
+        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+    )
+    .extend(
+        {
+            cv.GenerateID(): cv.declare_id(WiFiSignalSensor),
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp
index 7b2f010c07..ba22138e2a 100644
--- a/esphome/components/wifi_signal/wifi_signal_sensor.cpp
+++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp
@@ -4,7 +4,7 @@
 namespace esphome {
 namespace wifi_signal {
 
-static const char *TAG = "wifi_signal.sensor";
+static const char *const TAG = "wifi_signal.sensor";
 
 void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); }
 
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
new file mode 100644
index 0000000000..2795529203
--- /dev/null
+++ b/esphome/components/wled/__init__.py
@@ -0,0 +1,25 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components.light.types import AddressableLightEffect
+from esphome.components.light.effects import register_addressable_effect
+from esphome.const import CONF_NAME, CONF_PORT
+
+wled_ns = cg.esphome_ns.namespace("wled")
+WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect)
+
+CONFIG_SCHEMA = cv.All(cv.Schema({}), cv.only_with_arduino)
+
+
+@register_addressable_effect(
+    "wled",
+    WLEDLightEffect,
+    "WLED",
+    {
+        cv.Optional(CONF_PORT, default=21324): cv.port,
+    },
+)
+async def wled_light_effect_to_code(config, effect_id):
+    effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
+    cg.add(effect.set_port(config[CONF_PORT]))
+
+    return effect
diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp
new file mode 100644
index 0000000000..8c68bca6e3
--- /dev/null
+++ b/esphome/components/wled/wled_light_effect.cpp
@@ -0,0 +1,252 @@
+#ifdef USE_ARDUINO
+
+#include "wled_light_effect.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+#ifdef USE_ESP32
+#include 
+#endif
+
+#ifdef USE_ESP8266
+#include 
+#include 
+#endif
+
+namespace esphome {
+namespace wled {
+
+// Description of protocols:
+// https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control
+enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 };
+
+const int DEFAULT_BLANK_TIME = 1000;
+
+static const char *const TAG = "wled_light_effect";
+
+WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {}
+
+void WLEDLightEffect::start() {
+  AddressableLightEffect::start();
+
+  blank_at_ = 0;
+}
+
+void WLEDLightEffect::stop() {
+  AddressableLightEffect::stop();
+
+  if (udp_) {
+    udp_->stop();
+    udp_.reset();
+  }
+}
+
+void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) {
+  for (int led = it.size(); led-- > 0;) {
+    it[led].set(Color::BLACK);
+  }
+  it.schedule_show();
+}
+
+void WLEDLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) {
+  // Init UDP lazily
+  if (!udp_) {
+    udp_ = make_unique();
+
+    if (!udp_->begin(port_)) {
+      ESP_LOGW(TAG, "Cannot bind WLEDLightEffect to %d.", port_);
+      return;
+    }
+  }
+
+  std::vector payload;
+  while (uint16_t packet_size = udp_->parsePacket()) {
+    payload.resize(packet_size);
+
+    if (!udp_->read(&payload[0], payload.size())) {
+      continue;
+    }
+
+    if (!this->parse_frame_(it, &payload[0], payload.size())) {
+      ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=0x%02X).", payload.size(), payload[0]);
+      continue;
+    }
+  }
+
+  // FIXME: Use roll-over safe arithmetic
+  if (blank_at_ < millis()) {
+    blank_all_leds_(it);
+    blank_at_ = millis() + DEFAULT_BLANK_TIME;
+  }
+}
+
+bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) {
+  // At minimum frame needs to have:
+  // 1b - protocol
+  // 1b - timeout
+  if (size < 2) {
+    return false;
+  }
+
+  uint8_t protocol = payload[0];
+  uint8_t timeout = payload[1];
+
+  payload += 2;
+  size -= 2;
+
+  switch (protocol) {
+    case WLED_NOTIFIER:
+      // Hyperion Port
+      if (port_ == 19446) {
+        if (!parse_drgb_frame_(it, payload, size))
+          return false;
+      } else {
+        if (!parse_notifier_frame_(it, payload, size))
+          return false;
+      }
+      break;
+
+    case WARLS:
+      if (!parse_warls_frame_(it, payload, size))
+        return false;
+      break;
+
+    case DRGB:
+      if (!parse_drgb_frame_(it, payload, size))
+        return false;
+      break;
+
+    case DRGBW:
+      if (!parse_drgbw_frame_(it, payload, size))
+        return false;
+      break;
+
+    case DNRGB:
+      if (!parse_dnrgb_frame_(it, payload, size))
+        return false;
+      break;
+
+    default:
+      return false;
+  }
+
+  if (timeout == UINT8_MAX) {
+    blank_at_ = UINT32_MAX;
+  } else if (timeout > 0) {
+    blank_at_ = millis() + timeout * 1000;
+  } else {
+    blank_at_ = millis() + DEFAULT_BLANK_TIME;
+  }
+
+  it.schedule_show();
+  return true;
+}
+
+bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) {
+  // Packet needs to be empty
+  return size == 0;
+}
+
+bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) {
+  // packet: index, r, g, b
+  if ((size % 4) != 0) {
+    return false;
+  }
+
+  auto count = size / 4;
+  auto max_leds = it.size();
+
+  for (; count > 0; count--, payload += 4) {
+    uint8_t led = payload[0];
+    uint8_t r = payload[1];
+    uint8_t g = payload[2];
+    uint8_t b = payload[3];
+
+    if (led < max_leds) {
+      it[led].set(Color(r, g, b));
+    }
+  }
+
+  return true;
+}
+
+bool WLEDLightEffect::parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) {
+  // packet: r, g, b
+  if ((size % 3) != 0) {
+    return false;
+  }
+
+  auto count = size / 3;
+  auto max_leds = it.size();
+
+  for (uint16_t led = 0; led < count; ++led, payload += 3) {
+    uint8_t r = payload[0];
+    uint8_t g = payload[1];
+    uint8_t b = payload[2];
+
+    if (led < max_leds) {
+      it[led].set(Color(r, g, b));
+    }
+  }
+
+  return true;
+}
+
+bool WLEDLightEffect::parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) {
+  // packet: r, g, b, w
+  if ((size % 4) != 0) {
+    return false;
+  }
+
+  auto count = size / 4;
+  auto max_leds = it.size();
+
+  for (uint16_t led = 0; led < count; ++led, payload += 4) {
+    uint8_t r = payload[0];
+    uint8_t g = payload[1];
+    uint8_t b = payload[2];
+    uint8_t w = payload[3];
+
+    if (led < max_leds) {
+      it[led].set(Color(r, g, b, w));
+    }
+  }
+
+  return true;
+}
+
+bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) {
+  // offset: high, low
+  if (size < 2) {
+    return false;
+  }
+
+  uint16_t led = (uint16_t(payload[0]) << 8) + payload[1];
+  payload += 2;
+  size -= 2;
+
+  // packet: r, g, b
+  if ((size % 3) != 0) {
+    return false;
+  }
+
+  auto count = size / 3;
+  auto max_leds = it.size();
+
+  for (; count > 0; count--, payload += 3, led++) {
+    uint8_t r = payload[0];
+    uint8_t g = payload[1];
+    uint8_t b = payload[2];
+
+    if (led < max_leds) {
+      it[led].set(Color(r, g, b));
+    }
+  }
+
+  return true;
+}
+
+}  // 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
new file mode 100644
index 0000000000..f0021ca978
--- /dev/null
+++ b/esphome/components/wled/wled_light_effect.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#ifdef USE_ARDUINO
+
+#include "esphome/core/component.h"
+#include "esphome/components/light/addressable_light_effect.h"
+
+#include 
+#include 
+
+class UDP;
+
+namespace esphome {
+namespace wled {
+
+class WLEDLightEffect : public light::AddressableLightEffect {
+ public:
+  WLEDLightEffect(const std::string &name);
+
+ public:
+  void start() override;
+  void stop() override;
+  void apply(light::AddressableLight &it, const Color ¤t_color) override;
+  void set_port(uint16_t port) { this->port_ = port; }
+
+ protected:
+  void blank_all_leds_(light::AddressableLight &it);
+  bool parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size);
+  bool parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size);
+  bool parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size);
+  bool parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size);
+  bool parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size);
+  bool parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size);
+
+ protected:
+  uint16_t port_{0};
+  std::unique_ptr udp_;
+  uint32_t blank_at_{0};
+  uint32_t dropped_{0};
+};
+
+}  // namespace wled
+}  // namespace esphome
+
+#endif  // USE_ARDUINO
diff --git a/esphome/components/xiaomi_ble/__init__.py b/esphome/components/xiaomi_ble/__init__.py
index 2b36090293..046adc6248 100644
--- a/esphome/components/xiaomi_ble/__init__.py
+++ b/esphome/components/xiaomi_ble/__init__.py
@@ -3,16 +3,20 @@ import esphome.config_validation as cv
 from esphome.components import esp32_ble_tracker
 from esphome.const import CONF_ID
 
-DEPENDENCIES = ['esp32_ble_tracker']
+DEPENDENCIES = ["esp32_ble_tracker"]
 
-xiaomi_ble_ns = cg.esphome_ns.namespace('xiaomi_ble')
-XiaomiListener = xiaomi_ble_ns.class_('XiaomiListener', esp32_ble_tracker.ESPBTDeviceListener)
+xiaomi_ble_ns = cg.esphome_ns.namespace("xiaomi_ble")
+XiaomiListener = xiaomi_ble_ns.class_(
+    "XiaomiListener", esp32_ble_tracker.ESPBTDeviceListener
+)
 
-CONFIG_SCHEMA = cv.Schema({
-    cv.GenerateID(): cv.declare_id(XiaomiListener),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(XiaomiListener),
+    }
+).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 030ee73d4b..583b68a77b 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -1,187 +1,360 @@
 #include "xiaomi_ble.h"
 #include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
 
-#ifdef ARDUINO_ARCH_ESP32
+#ifdef USE_ESP32
+
+#include 
+#include "mbedtls/ccm.h"
 
 namespace esphome {
 namespace xiaomi_ble {
 
-static const char *TAG = "xiaomi_ble";
+static const char *const TAG = "xiaomi_ble";
 
-bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result) {
-  switch (data_type) {
-    case 0x0D: {  // temperature+humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 %
-      if (data_length != 4)
-        return false;
-      const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
-      const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8);
-      result.temperature = temperature / 10.0f;
-      result.humidity = humidity / 10.0f;
-      return true;
-    }
-    case 0x0A: {  // battery, 1 byte, 8-bit unsigned integer, 1 %
-      if (data_length != 1)
-        return false;
-      result.battery_level = data[0];
-      return true;
-    }
-    case 0x06: {  // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 %
-      if (data_length != 2)
-        return false;
-      const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
-      result.humidity = humidity / 10.0f;
-      return true;
-    }
-    case 0x04: {  // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C
-      if (data_length != 2)
-        return false;
-      const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
-      result.temperature = temperature / 10.0f;
-      return true;
-    }
-    case 0x09: {  // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm
-      if (data_length != 2)
-        return false;
-      const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
-      result.conductivity = conductivity;
-      return true;
-    }
-    case 0x07: {  // illuminance, 3 bytes, 24-bit unsigned integer (LE), 1 lx
-      if (data_length != 3)
-        return false;
-      const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16);
-      result.illuminance = illuminance;
-      return true;
-    }
-    case 0x08: {  // soil moisture, 1 byte, 8-bit unsigned integer, 1 %
-      if (data_length != 1)
-        return false;
-      result.moisture = data[0];
-      return true;
-    }
-    default:
-      return false;
+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];
   }
+  // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C
+  else if ((value_type == 0x04) && (value_length == 2)) {
+    const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
+    result.temperature = temperature / 10.0f;
+  }
+  // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 %
+  else if ((value_type == 0x06) && (value_length == 2)) {
+    const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
+    result.humidity = humidity / 10.0f;
+  }
+  // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx
+  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;
+    if (value_type == 0x0F)
+      result.has_motion = true;
+  }
+  // soil moisture, 1 byte, 8-bit unsigned integer, 1 %
+  else if ((value_type == 0x08) && (value_length == 1)) {
+    result.moisture = data[0];
+  }
+  // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm
+  else if ((value_type == 0x09) && (value_length == 2)) {
+    const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
+    result.conductivity = conductivity;
+  }
+  // battery, 1 byte, 8-bit unsigned integer, 1 %
+  else if ((value_type == 0x0A) && (value_length == 1)) {
+    result.battery_level = data[0];
+  }
+  // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 %
+  else if ((value_type == 0x0D) && (value_length == 4)) {
+    const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
+    const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8);
+    result.temperature = temperature / 10.0f;
+    result.humidity = humidity / 10.0f;
+  }
+  // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3
+  else if ((value_type == 0x10) && (value_length == 2)) {
+    const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8);
+    result.formaldehyde = formaldehyde / 100.0f;
+  }
+  // on/off state, 1 byte, 8-bit unsigned integer
+  else if ((value_type == 0x12) && (value_length == 1)) {
+    result.is_active = data[0];
+  }
+  // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 %
+  else if ((value_type == 0x13) && (value_length == 1)) {
+    result.tablet = data[0];
+  }
+  // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min
+  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;
+  } else {
+    return false;
+  }
+
+  return true;
 }
-bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracker::ServiceData &service_data) {
-  if (!service_data.uuid.contains(0x95, 0xFE)) {
-    // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes");
+
+bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) {
+  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;
   }
 
-  const auto raw = service_data.data;
-
-  if (raw.size() < 14) {
-    // ESP_LOGVV(TAG, "Xiaomi service data too short!");
-    return false;
-  }
-
-  bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01;
-  bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00;
-  bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04;
-  bool is_cgg1 = ((raw[1] & 0x30) == 0x30 || (raw[1] & 0x20) == 0x20) && raw[2] == 0x47 && raw[3] == 0x03;
-
-  if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) {
-    // ESP_LOGVV(TAG, "Xiaomi no magic bytes");
-    return false;
-  }
-
-  result.type = XiaomiParseResult::TYPE_HHCCJCY01;
-  if (is_lywsdcgq) {
-    result.type = XiaomiParseResult::TYPE_LYWSDCGQ;
-  } else if (is_lywsd02) {
-    result.type = XiaomiParseResult::TYPE_LYWSD02;
-  } else if (is_cgg1) {
-    result.type = XiaomiParseResult::TYPE_CGG1;
-  }
-
-  uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12;
-
   // Data point specs
   // Byte 0: type
   // Byte 1: fixed 0x10
   // Byte 2: length
   // Byte 3..3+len-1: data point value
 
-  const uint8_t *raw_data = &raw[raw_offset];
-  uint8_t data_offset = 0;
-  uint8_t data_length = raw.size() - raw_offset;
+  const uint8_t *payload = message.data() + result.raw_offset;
+  uint8_t payload_length = message.size() - result.raw_offset;
+  uint8_t payload_offset = 0;
   bool success = false;
 
-  while (true) {
-    if (data_length < 4)
-      // at least 4 bytes required
-      // type, fixed 0x10, length, 1 byte value
+  if (payload_length < 4) {
+    ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", payload_length);
+    return false;
+  }
+
+  while (payload_length > 3) {
+    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;
+    }
 
-    const uint8_t datapoint_type = raw_data[data_offset + 0];
-    const uint8_t datapoint_length = raw_data[data_offset + 2];
-
-    if (data_length < 3 + datapoint_length)
-      // 3 fixed bytes plus value length
+    const uint8_t value_length = payload[payload_offset + 2];
+    if ((value_length < 1) || (value_length > 4) || (payload_length < (3 + value_length))) {
+      ESP_LOGVV(TAG, "parse_xiaomi_message(): value has wrong size (%d)!", value_length);
       break;
+    }
 
-    const uint8_t *datapoint_data = &raw_data[data_offset + 3];
+    const uint8_t value_type = payload[payload_offset + 0];
+    const uint8_t *data = &payload[payload_offset + 3];
 
-    if (parse_xiaomi_data_byte(datapoint_type, datapoint_data, datapoint_length, result))
+    if (parse_xiaomi_value(value_type, data, value_length, result))
       success = true;
 
-    data_length -= data_offset + 3 + datapoint_length;
-    data_offset += 3 + datapoint_length;
+    payload_length -= 3 + value_length;
+    payload_offset += 3 + value_length;
   }
 
   return success;
 }
-optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) {
+
+optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) {
   XiaomiParseResult result;
-  bool success = false;
-  for (auto &service_data : device.get_service_datas()) {
-    if (parse_xiaomi_service_data(result, service_data))
-      success = true;
-  }
-  if (!success)
+  if (!service_data.uuid.contains(0x95, 0xFE)) {
+    ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes.");
     return {};
+  }
+
+  auto raw = service_data.data;
+  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.");
+    return {};
+  }
+
+  static uint8_t last_frame_count = 0;
+  if (last_frame_count == raw[4]) {
+    ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count));
+    result.is_duplicate = true;
+    return {};
+  }
+  last_frame_count = raw[4];
+  result.is_duplicate = false;
+  result.raw_offset = result.has_capability ? 12 : 11;
+
+  if ((raw[2] == 0x98) && (raw[3] == 0x00)) {  // MiFlora
+    result.type = XiaomiParseResult::TYPE_HHCCJCY01;
+    result.name = "HHCCJCY01";
+  } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) {  // round body, segment LCD
+    result.type = XiaomiParseResult::TYPE_LYWSDCGQ;
+    result.name = "LYWSDCGQ";
+  } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) {  // FlowerPot, RoPot
+    result.type = XiaomiParseResult::TYPE_HHCCPOT002;
+    result.name = "HHCCPOT002";
+  } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) {  // Xiaomi (Honeywell) formaldehyde sensor, OLED display
+    result.type = XiaomiParseResult::TYPE_JQJCY01YM;
+    result.name = "JQJCY01YM";
+  } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) {  // Philips/Xiaomi BLE nightlight
+    result.type = XiaomiParseResult::TYPE_MUE4094RT;
+    result.name = "MUE4094RT";
+    result.raw_offset -= 6;
+  } else if ((raw[2] == 0x47 && raw[3] == 0x03) ||  // ClearGrass-branded, round body, e-ink display
+             (raw[2] == 0x48 && raw[3] == 0x0B)) {  // Qingping-branded, round body, e-ink display — with bindkeys
+    result.type = XiaomiParseResult::TYPE_CGG1;
+    result.name = "CGG1";
+  } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) {  // VegTrug Grow Care Garden
+    result.type = XiaomiParseResult::TYPE_GCLS002;
+    result.name = "GCLS002";
+  } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) {  // rectangular body, e-ink display
+    result.type = XiaomiParseResult::TYPE_LYWSD02;
+    result.name = "LYWSD02";
+  } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) {  // Mosquito Repellent Smart Version
+    result.type = XiaomiParseResult::TYPE_WX08ZM;
+    result.name = "WX08ZM";
+  } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) {  // Cleargrass (Qingping) alarm clock, segment LCD
+    result.type = XiaomiParseResult::TYPE_CGD1;
+    result.name = "CGD1";
+  } else if ((raw[2] == 0x6F) && (raw[3] == 0x06)) {  // Cleargrass (Qingping) Temp & RH Lite
+    result.type = XiaomiParseResult::TYPE_CGDK2;
+    result.name = "CGDK2";
+  } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) {  // small square body, segment LCD, encrypted
+    result.type = XiaomiParseResult::TYPE_LYWSD03MMC;
+    result.name = "LYWSD03MMC";
+  } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) {  // Xiaomi-Yeelight BLE nightlight
+    result.type = XiaomiParseResult::TYPE_MJYD02YLA;
+    result.name = "MJYD02YLA";
+    if (raw.size() == 19)
+      result.raw_offset -= 6;
+  } 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 {};
+  }
+
   return result;
 }
 
-bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
-  auto res = parse_xiaomi(device);
-  if (!res.has_value())
+bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) {
+  if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) {
+    ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size());
+    ESP_LOGVV(TAG, "  Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
     return false;
-
-  const char *name = "HHCCJCY01";
-  if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) {
-    name = "LYWSDCGQ";
-  } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) {
-    name = "LYWSD02";
-  } else if (res->type == XiaomiParseResult::TYPE_CGG1) {
-    name = "CGG1";
   }
 
-  ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str());
+  uint8_t mac_reverse[6] = {0};
+  mac_reverse[5] = (uint8_t)(address >> 40);
+  mac_reverse[4] = (uint8_t)(address >> 32);
+  mac_reverse[3] = (uint8_t)(address >> 24);
+  mac_reverse[2] = (uint8_t)(address >> 16);
+  mac_reverse[1] = (uint8_t)(address >> 8);
+  mac_reverse[0] = (uint8_t)(address >> 0);
 
-  if (res->temperature.has_value()) {
-    ESP_LOGD(TAG, "  Temperature: %.1f°C", *res->temperature);
+  XiaomiAESVector vector{.key = {0},
+                         .plaintext = {0},
+                         .ciphertext = {0},
+                         .authdata = {0x11},
+                         .iv = {0},
+                         .tag = {0},
+                         .keysize = 16,
+                         .authsize = 1,
+                         .datasize = 0,
+                         .tagsize = 4,
+                         .ivsize = 12};
+
+  vector.datasize = (raw.size() == 19) ? raw.size() - 12 : raw.size() - 18;
+  int cipher_pos = (raw.size() == 19) ? 5 : 11;
+
+  const uint8_t *v = raw.data();
+
+  memcpy(vector.key, bindkey, vector.keysize);
+  memcpy(vector.ciphertext, v + cipher_pos, vector.datasize);
+  memcpy(vector.tag, v + raw.size() - vector.tagsize, vector.tagsize);
+  memcpy(vector.iv, mac_reverse, 6);             // MAC address reverse
+  memcpy(vector.iv + 6, v + 2, 3);               // sensor type (2) + packet id (1)
+  memcpy(vector.iv + 9, v + raw.size() - 7, 3);  // payload counter
+
+  mbedtls_ccm_context ctx;
+  mbedtls_ccm_init(&ctx);
+
+  int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8);
+  if (ret) {
+    ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed.");
+    mbedtls_ccm_free(&ctx);
+    return false;
   }
-  if (res->humidity.has_value()) {
-    ESP_LOGD(TAG, "  Humidity: %.1f%%", *res->humidity);
+
+  ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize,
+                                 vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize);
+  if (ret) {
+    uint8_t mac_address[6] = {0};
+    memcpy(mac_address, mac_reverse + 5, 1);
+    memcpy(mac_address + 1, mac_reverse + 4, 1);
+    memcpy(mac_address + 2, mac_reverse + 3, 1);
+    memcpy(mac_address + 3, mac_reverse + 2, 1);
+    memcpy(mac_address + 4, mac_reverse + 1, 1);
+    memcpy(mac_address + 5, mac_reverse, 1);
+    ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed.");
+    ESP_LOGVV(TAG, "  MAC address : %s", format_hex_pretty(mac_address, 6).c_str());
+    ESP_LOGVV(TAG, "       Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
+    ESP_LOGVV(TAG, "          Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str());
+    ESP_LOGVV(TAG, "           Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str());
+    ESP_LOGVV(TAG, "       Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str());
+    ESP_LOGVV(TAG, "          Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str());
+    mbedtls_ccm_free(&ctx);
+    return false;
   }
-  if (res->battery_level.has_value()) {
-    ESP_LOGD(TAG, "  Battery Level: %.0f%%", *res->battery_level);
+
+  // replace encrypted payload with plaintext
+  uint8_t *p = vector.plaintext;
+  for (std::vector::iterator it = raw.begin() + cipher_pos; it != raw.begin() + cipher_pos + vector.datasize;
+       ++it) {
+    *it = *(p++);
   }
-  if (res->conductivity.has_value()) {
-    ESP_LOGD(TAG, "  Conductivity: %.0fµS/cm", *res->conductivity);
+
+  // clear encrypted flag
+  raw[0] &= ~0x08;
+
+  ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed.");
+  ESP_LOGVV(TAG, "  Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(),
+            static_cast(raw[4]));
+
+  mbedtls_ccm_free(&ctx);
+  return true;
+}
+
+bool report_xiaomi_results(const optional &result, const std::string &address) {
+  if (!result.has_value()) {
+    ESP_LOGVV(TAG, "report_xiaomi_results(): no results available.");
+    return false;
   }
-  if (res->illuminance.has_value()) {
-    ESP_LOGD(TAG, "  Illuminance: %.0flx", *res->illuminance);
+
+  ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str());
+
+  if (result->temperature.has_value()) {
+    ESP_LOGD(TAG, "  Temperature: %.1f°C", *result->temperature);
   }
-  if (res->moisture.has_value()) {
-    ESP_LOGD(TAG, "  Moisture: %.0f%%", *res->moisture);
+  if (result->humidity.has_value()) {
+    ESP_LOGD(TAG, "  Humidity: %.1f%%", *result->humidity);
+  }
+  if (result->battery_level.has_value()) {
+    ESP_LOGD(TAG, "  Battery Level: %.0f%%", *result->battery_level);
+  }
+  if (result->conductivity.has_value()) {
+    ESP_LOGD(TAG, "  Conductivity: %.0fµS/cm", *result->conductivity);
+  }
+  if (result->illuminance.has_value()) {
+    ESP_LOGD(TAG, "  Illuminance: %.0flx", *result->illuminance);
+  }
+  if (result->moisture.has_value()) {
+    ESP_LOGD(TAG, "  Moisture: %.0f%%", *result->moisture);
+  }
+  if (result->tablet.has_value()) {
+    ESP_LOGD(TAG, "  Mosquito tablet: %.0f%%", *result->tablet);
+  }
+  if (result->is_active.has_value()) {
+    ESP_LOGD(TAG, "  Repellent: %s", (*result->is_active) ? "on" : "off");
+  }
+  if (result->has_motion.has_value()) {
+    ESP_LOGD(TAG, "  Motion: %s", (*result->has_motion) ? "yes" : "no");
+  }
+  if (result->is_light.has_value()) {
+    ESP_LOGD(TAG, "  Light: %s", (*result->is_light) ? "on" : "off");
   }
 
   return true;
 }
 
+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 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 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
+}
+
 }  // namespace xiaomi_ble
 }  // namespace esphome
 
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h
index 824ea80edf..54ab9a144f 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.h
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.h
@@ -3,24 +3,68 @@
 #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 {
 
 struct XiaomiParseResult {
-  enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type;
+  enum {
+    TYPE_HHCCJCY01,
+    TYPE_GCLS002,
+    TYPE_HHCCPOT002,
+    TYPE_LYWSDCGQ,
+    TYPE_LYWSD02,
+    TYPE_CGG1,
+    TYPE_LYWSD03MMC,
+    TYPE_CGD1,
+    TYPE_CGDK2,
+    TYPE_JQJCY01YM,
+    TYPE_MUE4094RT,
+    TYPE_WX08ZM,
+    TYPE_MJYD02YLA,
+    TYPE_MHOC401,
+    TYPE_CGPR1
+  } type;
+  std::string name;
   optional temperature;
   optional humidity;
-  optional battery_level;
+  optional moisture;
   optional conductivity;
   optional illuminance;
-  optional moisture;
+  optional formaldehyde;
+  optional battery_level;
+  optional tablet;
+  optional idle_time;
+  optional is_active;
+  optional has_motion;
+  optional is_light;
+  bool has_data;        // 0x40
+  bool has_capability;  // 0x20
+  bool has_encryption;  // 0x08
+  bool is_duplicate;
+  int raw_offset;
 };
 
-bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result);
+struct XiaomiAESVector {
+  uint8_t key[16];
+  uint8_t plaintext[16];
+  uint8_t ciphertext[16];
+  uint8_t authdata[16];
+  uint8_t iv[16];
+  uint8_t tag[16];
+  size_t keysize;
+  size_t authsize;
+  size_t datasize;
+  size_t tagsize;
+  size_t ivsize;
+};
 
-optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device);
+bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result);
+bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result);
+optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data);
+bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address);
+bool report_xiaomi_results(const optional &result, const std::string &address);
 
 class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener {
  public:
diff --git a/esphome/components/xiaomi_cgd1/__init__.py b/esphome/components/xiaomi_cgd1/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py
new file mode 100644
index 0000000000..5b88121d7c
--- /dev/null
+++ b/esphome/components/xiaomi_cgd1/sensor.py
@@ -0,0 +1,76 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    CONF_ID,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    CONF_BINDKEY,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_cgd1_ns = cg.esphome_ns.namespace("xiaomi_cgd1")
+XiaomiCGD1 = xiaomi_cgd1_ns.class_(
+    "XiaomiCGD1", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiCGD1),
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_bindkey(config[CONF_BINDKEY]))
+
+    if 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))
diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp
new file mode 100644
index 0000000000..baf9cb8075
--- /dev/null
+++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp
@@ -0,0 +1,73 @@
+#include "xiaomi_cgd1.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_cgd1 {
+
+static const char *const TAG = "xiaomi_cgd1";
+
+void XiaomiCGD1::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi CGD1");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
+  if (device.address_uint64() != this->address_) {
+    ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
+    return false;
+  }
+  ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
+
+  bool success = false;
+  for (auto &service_data : device.get_service_datas()) {
+    auto res = xiaomi_ble::parse_xiaomi_header(service_data);
+    if (!res.has_value()) {
+      continue;
+    }
+    if (res->is_duplicate) {
+      continue;
+    }
+    if (res->has_encryption &&
+        (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_,
+                                              this->address_)))) {
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiCGD1::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
+}  // namespace xiaomi_cgd1
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h
new file mode 100644
index 0000000000..d05cffc4d1
--- /dev/null
+++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_cgd1 {
+
+class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; };
+  void set_bindkey(const std::string &bindkey);
+
+  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; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_cgd1
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_cgdk2/__init__.py b/esphome/components/xiaomi_cgdk2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py
new file mode 100644
index 0000000000..ac487d87fc
--- /dev/null
+++ b/esphome/components/xiaomi_cgdk2/sensor.py
@@ -0,0 +1,76 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    CONF_ID,
+    CONF_BINDKEY,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_cgd1_ns = cg.esphome_ns.namespace("xiaomi_cgdk2")
+XiaomiCGD1 = xiaomi_cgd1_ns.class_(
+    "XiaomiCGDK2", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiCGD1),
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_bindkey(config[CONF_BINDKEY]))
+
+    if 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))
diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
new file mode 100644
index 0000000000..c74794f4f4
--- /dev/null
+++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
@@ -0,0 +1,73 @@
+#include "xiaomi_cgdk2.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_cgdk2 {
+
+static const char *const TAG = "xiaomi_cgdk2";
+
+void XiaomiCGDK2::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi CGDK2");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiCGDK2::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_LOGV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
+
+  bool success = false;
+  for (auto &service_data : device.get_service_datas()) {
+    auto res = xiaomi_ble::parse_xiaomi_header(service_data);
+    if (!res.has_value()) {
+      continue;
+    }
+    if (res->is_duplicate) {
+      continue;
+    }
+    if (res->has_encryption &&
+        (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_,
+                                              this->address_)))) {
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiCGDK2::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
+}  // namespace xiaomi_cgdk2
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
new file mode 100644
index 0000000000..8fd9946537
--- /dev/null
+++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_cgdk2 {
+
+class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; };
+  void set_bindkey(const std::string &bindkey);
+
+  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; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_cgdk2
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py
index 897687c68a..a4f9a39aff 100644
--- a/esphome/components/xiaomi_cgg1/sensor.py
+++ b/esphome/components/xiaomi_cgg1/sensor.py
@@ -1,38 +1,77 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor, esp32_ble_tracker
-from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
-    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_BINDKEY,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    CONF_ID,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+)
 
-DEPENDENCIES = ['esp32_ble_tracker']
-AUTO_LOAD = ['xiaomi_ble']
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
 
-xiaomi_cgg1_ns = cg.esphome_ns.namespace('xiaomi_cgg1')
+xiaomi_cgg1_ns = cg.esphome_ns.namespace("xiaomi_cgg1")
 XiaomiCGG1 = xiaomi_cgg1_ns.class_(
-    'XiaomiCGG1', esp32_ble_tracker.ESPBTDeviceListener, cg.Component)
+    "XiaomiCGG1", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
 
-CONFIG_SCHEMA = cv.Schema({
-    cv.GenerateID(): cv.declare_id(XiaomiCGG1),
-    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
-    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
-    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
-    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiCGG1),
+            cv.Optional(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
 
 
-def to_code(config):
+async 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)
+    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_BINDKEY in config:
+        cg.add(var.set_bindkey(config[CONF_BINDKEY]))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
index 6cc14f5a8e..c20c7578d0 100644
--- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
+++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
@@ -1,20 +1,72 @@
 #include "xiaomi_cgg1.h"
 #include "esphome/core/log.h"
 
-#ifdef ARDUINO_ARCH_ESP32
+#ifdef USE_ESP32
 
 namespace esphome {
 namespace xiaomi_cgg1 {
 
-static const char *TAG = "xiaomi_cgg1";
+static const char *const TAG = "xiaomi_cgg1";
 
 void XiaomiCGG1::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi CGG1");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
   LOG_SENSOR("  ", "Temperature", this->temperature_);
   LOG_SENSOR("  ", "Humidity", this->humidity_);
   LOG_SENSOR("  ", "Battery Level", this->battery_level_);
 }
 
+bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
+  if (device.address_uint64() != this->address_) {
+    ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
+    return false;
+  }
+  ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
+
+  bool success = false;
+  for (auto &service_data : device.get_service_datas()) {
+    auto res = xiaomi_ble::parse_xiaomi_header(service_data);
+    if (!res.has_value()) {
+      continue;
+    }
+    if (res->is_duplicate) {
+      continue;
+    }
+    if (res->has_encryption &&
+        (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_,
+                                              this->address_)))) {
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiCGG1::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
 }  // namespace xiaomi_cgg1
 }  // namespace esphome
 
diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
index 7f73011275..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 {
@@ -13,23 +13,9 @@ namespace xiaomi_cgg1 {
 class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
  public:
   void set_address(uint64_t address) { address_ = address; }
+  void set_bindkey(const std::string &bindkey);
 
-  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
-    if (device.address_uint64() != this->address_)
-      return false;
-
-    auto res = xiaomi_ble::parse_xiaomi(device);
-    if (!res.has_value())
-      return false;
-
-    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);
-    return true;
-  }
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
@@ -39,6 +25,7 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen
 
  protected:
   uint64_t address_;
+  uint8_t bindkey_[16];
   sensor::Sensor *temperature_{nullptr};
   sensor::Sensor *humidity_{nullptr};
   sensor::Sensor *battery_level_{nullptr};
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..7f0aac873d
--- /dev/null
+++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py
@@ -0,0 +1,85 @@
+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,
+    DEVICE_CLASS_MOTION,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    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=DEVICE_CLASS_MOTION,
+            ): binary_sensor.device_class,
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+            cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(
+                unit_of_measurement=UNIT_MINUTE,
+                icon=ICON_TIMELAPSE,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_EMPTY,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+            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/__init__.py b/esphome/components/xiaomi_gcls002/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py
new file mode 100644
index 0000000000..4154b64233
--- /dev/null
+++ b/esphome/components/xiaomi_gcls002/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_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_ILLUMINANCE,
+    DEVICE_CLASS_TEMPERATURE,
+    ICON_WATER_PERCENT,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    CONF_ID,
+    CONF_MOISTURE,
+    CONF_ILLUMINANCE,
+    UNIT_LUX,
+    CONF_CONDUCTIVITY,
+    UNIT_MICROSIEMENS_PER_CENTIMETER,
+    ICON_FLOWER,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_gcls002_ns = cg.esphome_ns.namespace("xiaomi_gcls002")
+XiaomiGCLS002 = xiaomi_gcls002_ns.class_(
+    "XiaomiGCLS002", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiGCLS002),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                icon=ICON_WATER_PERCENT,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_LUX,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_ILLUMINANCE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER,
+                icon=ICON_FLOWER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .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_MOISTURE in config:
+        sens = await sensor.new_sensor(config[CONF_MOISTURE])
+        cg.add(var.set_moisture(sens))
+    if CONF_ILLUMINANCE in config:
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
+        cg.add(var.set_illuminance(sens))
+    if CONF_CONDUCTIVITY in config:
+        sens = await sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        cg.add(var.set_conductivity(sens))
diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp
new file mode 100644
index 0000000000..990346e01e
--- /dev/null
+++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp
@@ -0,0 +1,62 @@
+#include "xiaomi_gcls002.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_gcls002 {
+
+static const char *const TAG = "xiaomi_gcls002";
+
+void XiaomiGCLS002::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi GCLS002");
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Moisture", this->moisture_);
+  LOG_SENSOR("  ", "Conductivity", this->conductivity_);
+  LOG_SENSOR("  ", "Illuminance", this->illuminance_);
+}
+
+bool XiaomiGCLS002::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->moisture.has_value() && this->moisture_ != nullptr)
+      this->moisture_->publish_state(*res->moisture);
+    if (res->conductivity.has_value() && this->conductivity_ != nullptr)
+      this->conductivity_->publish_state(*res->conductivity);
+    if (res->illuminance.has_value() && this->illuminance_ != nullptr)
+      this->illuminance_->publish_state(*res->illuminance);
+    success = true;
+  }
+
+  return success;
+}
+
+}  // namespace xiaomi_gcls002
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h
new file mode 100644
index 0000000000..08e1bd7e54
--- /dev/null
+++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_gcls002 {
+
+class XiaomiGCLS002 : 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_moisture(sensor::Sensor *moisture) { moisture_ = moisture; }
+  void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; }
+  void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *moisture_{nullptr};
+  sensor::Sensor *conductivity_{nullptr};
+  sensor::Sensor *illuminance_{nullptr};
+};
+
+}  // namespace xiaomi_gcls002
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py
index 495446ba11..535316e246 100644
--- a/esphome/components/xiaomi_hhccjcy01/sensor.py
+++ b/esphome/components/xiaomi_hhccjcy01/sensor.py
@@ -1,49 +1,97 @@
 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_MAC_ADDRESS, CONF_TEMPERATURE, \
-    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \
-    CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \
-    UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER
+from esphome.const import (
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_ILLUMINANCE,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    ICON_WATER_PERCENT,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    CONF_ID,
+    CONF_MOISTURE,
+    CONF_ILLUMINANCE,
+    UNIT_LUX,
+    CONF_CONDUCTIVITY,
+    UNIT_MICROSIEMENS_PER_CENTIMETER,
+    ICON_FLOWER,
+    DEVICE_CLASS_BATTERY,
+    CONF_BATTERY_LEVEL,
+)
 
-DEPENDENCIES = ['esp32_ble_tracker']
-AUTO_LOAD = ['xiaomi_ble']
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
 
-xiaomi_hhccjcy01_ns = cg.esphome_ns.namespace('xiaomi_hhccjcy01')
-XiaomiHHCCJCY01 = xiaomi_hhccjcy01_ns.class_('XiaomiHHCCJCY01',
-                                             esp32_ble_tracker.ESPBTDeviceListener, cg.Component)
+xiaomi_hhccjcy01_ns = cg.esphome_ns.namespace("xiaomi_hhccjcy01")
+XiaomiHHCCJCY01 = xiaomi_hhccjcy01_ns.class_(
+    "XiaomiHHCCJCY01", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
 
-CONFIG_SCHEMA = cv.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_THERMOMETER, 1),
-    cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0),
-    cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0),
-    cv.Optional(CONF_CONDUCTIVITY):
-        sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0),
-    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                icon=ICON_WATER_PERCENT,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_LUX,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_ILLUMINANCE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER,
+                icon=ICON_FLOWER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
 
 
-def to_code(config):
+async 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)
+    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 = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_MOISTURE in config:
-        sens = yield sensor.new_sensor(config[CONF_MOISTURE])
+        sens = await sensor.new_sensor(config[CONF_MOISTURE])
         cg.add(var.set_moisture(sens))
     if CONF_ILLUMINANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
         cg.add(var.set_illuminance(sens))
     if CONF_CONDUCTIVITY in config:
-        sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        sens = await sensor.new_sensor(config[CONF_CONDUCTIVITY])
         cg.add(var.set_conductivity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp
index 8c8152c54c..30990b121d 100644
--- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp
+++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp
@@ -1,12 +1,12 @@
 #include "xiaomi_hhccjcy01.h"
 #include "esphome/core/log.h"
 
-#ifdef ARDUINO_ARCH_ESP32
+#ifdef USE_ESP32
 
 namespace esphome {
 namespace xiaomi_hhccjcy01 {
 
-static const char *TAG = "xiaomi_hhccjcy01";
+static const char *const TAG = "xiaomi_hhccjcy01";
 
 void XiaomiHHCCJCY01::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi HHCCJCY01");
@@ -17,6 +17,48 @@ void XiaomiHHCCJCY01::dump_config() {
   LOG_SENSOR("  ", "Battery Level", this->battery_level_);
 }
 
+bool XiaomiHHCCJCY01::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->moisture.has_value() && this->moisture_ != nullptr)
+      this->moisture_->publish_state(*res->moisture);
+    if (res->conductivity.has_value() && this->conductivity_ != nullptr)
+      this->conductivity_->publish_state(*res->conductivity);
+    if (res->illuminance.has_value() && this->illuminance_ != nullptr)
+      this->illuminance_->publish_state(*res->illuminance);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
 }  // namespace xiaomi_hhccjcy01
 }  // namespace esphome
 
diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
index c1b8511bb8..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 {
@@ -14,26 +14,7 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL
  public:
   void set_address(uint64_t address) { address_ = address; }
 
-  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
-    if (device.address_uint64() != this->address_)
-      return false;
-
-    auto res = xiaomi_ble::parse_xiaomi(device);
-    if (!res.has_value())
-      return false;
-
-    if (res->temperature.has_value() && this->temperature_ != nullptr)
-      this->temperature_->publish_state(*res->temperature);
-    if (res->moisture.has_value() && this->moisture_ != nullptr)
-      this->moisture_->publish_state(*res->moisture);
-    if (res->conductivity.has_value() && this->conductivity_ != nullptr)
-      this->conductivity_->publish_state(*res->conductivity);
-    if (res->illuminance.has_value() && this->illuminance_ != nullptr)
-      this->illuminance_->publish_state(*res->illuminance);
-    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
-      this->battery_level_->publish_state(*res->battery_level);
-    return true;
-  }
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
diff --git a/esphome/components/xiaomi_hhccpot002/__init__.py b/esphome/components/xiaomi_hhccpot002/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py
new file mode 100644
index 0000000000..82ee12d8d1
--- /dev/null
+++ b/esphome/components/xiaomi_hhccpot002/sensor.py
@@ -0,0 +1,60 @@
+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,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PERCENT,
+    ICON_WATER_PERCENT,
+    CONF_ID,
+    CONF_MOISTURE,
+    CONF_CONDUCTIVITY,
+    UNIT_MICROSIEMENS_PER_CENTIMETER,
+    ICON_FLOWER,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_hhccpot002_ns = cg.esphome_ns.namespace("xiaomi_hhccpot002")
+XiaomiHHCCPOT002 = xiaomi_hhccpot002_ns.class_(
+    "XiaomiHHCCPOT002", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                icon=ICON_WATER_PERCENT,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER,
+                icon=ICON_FLOWER,
+                accuracy_decimals=0,
+                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_MOISTURE in config:
+        sens = await sensor.new_sensor(config[CONF_MOISTURE])
+        cg.add(var.set_moisture(sens))
+    if CONF_CONDUCTIVITY in config:
+        sens = await sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        cg.add(var.set_conductivity(sens))
diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp
new file mode 100644
index 0000000000..3ae29088bb
--- /dev/null
+++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp
@@ -0,0 +1,56 @@
+#include "xiaomi_hhccpot002.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_hhccpot002 {
+
+static const char *const TAG = "xiaomi_hhccpot002";
+
+void XiaomiHHCCPOT002 ::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002");
+  LOG_SENSOR("  ", "Moisture", this->moisture_);
+  LOG_SENSOR("  ", "Conductivity", this->conductivity_);
+}
+
+bool XiaomiHHCCPOT002::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      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->moisture.has_value() && this->moisture_ != nullptr)
+      this->moisture_->publish_state(*res->moisture);
+    if (res->conductivity.has_value() && this->conductivity_ != nullptr)
+      this->conductivity_->publish_state(*res->conductivity);
+    success = true;
+  }
+
+  return success;
+}
+
+}  // namespace xiaomi_hhccpot002
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h
new file mode 100644
index 0000000000..ce746b9ee0
--- /dev/null
+++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_hhccpot002 {
+
+class XiaomiHHCCPOT002 : 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_moisture(sensor::Sensor *moisture) { moisture_ = moisture; }
+  void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *moisture_{nullptr};
+  sensor::Sensor *conductivity_{nullptr};
+};
+
+}  // namespace xiaomi_hhccpot002
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_jqjcy01ym/__init__.py b/esphome/components/xiaomi_jqjcy01ym/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py
new file mode 100644
index 0000000000..f4d2b342fd
--- /dev/null
+++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py
@@ -0,0 +1,85 @@
+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_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    CONF_ID,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    CONF_HUMIDITY,
+    UNIT_MILLIGRAMS_PER_CUBIC_METER,
+    ICON_FLASK_OUTLINE,
+    CONF_FORMALDEHYDE,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_jqjcy01ym_ns = cg.esphome_ns.namespace("xiaomi_jqjcy01ym")
+XiaomiJQJCY01YM = xiaomi_jqjcy01ym_ns.class_(
+    "XiaomiJQJCY01YM", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
+                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_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if 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_FORMALDEHYDE in config:
+        sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
+        cg.add(var.set_formaldehyde(sens))
+    if CONF_BATTERY_LEVEL in config:
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp
new file mode 100644
index 0000000000..1efebc2849
--- /dev/null
+++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp
@@ -0,0 +1,62 @@
+#include "xiaomi_jqjcy01ym.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_jqjcy01ym {
+
+static const char *const TAG = "xiaomi_jqjcy01ym";
+
+void XiaomiJQJCY01YM::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi JQJCY01YM");
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Formaldehyde", this->formaldehyde_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiJQJCY01YM::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->formaldehyde.has_value() && this->formaldehyde_ != nullptr)
+      this->formaldehyde_->publish_state(*res->formaldehyde);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+}  // namespace xiaomi_jqjcy01ym
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h
new file mode 100644
index 0000000000..ca1ad0f27e
--- /dev/null
+++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_jqjcy01ym {
+
+class XiaomiJQJCY01YM : 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_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; }
+  void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *formaldehyde_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_jqjcy01ym
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py
index 8e4d59316b..20629a0a9c 100644
--- a/esphome/components/xiaomi_lywsd02/sensor.py
+++ b/esphome/components/xiaomi_lywsd02/sensor.py
@@ -1,34 +1,73 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor, esp32_ble_tracker
-from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
-    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_BATTERY,
+    CONF_ID,
+)
 
-DEPENDENCIES = ['esp32_ble_tracker']
-AUTO_LOAD = ['xiaomi_ble']
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
 
-xiaomi_lywsd02_ns = cg.esphome_ns.namespace('xiaomi_lywsd02')
-XiaomiLYWSD02 = xiaomi_lywsd02_ns.class_('XiaomiLYWSD02', esp32_ble_tracker.ESPBTDeviceListener,
-                                         cg.Component)
+xiaomi_lywsd02_ns = cg.esphome_ns.namespace("xiaomi_lywsd02")
+XiaomiLYWSD02 = xiaomi_lywsd02_ns.class_(
+    "XiaomiLYWSD02", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
 
-CONFIG_SCHEMA = cv.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_THERMOMETER, 1),
-    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiLYWSD02),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
 
 
-def to_code(config):
+async 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)
+    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 = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        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))
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
index cd77c133a5..a6f27c58b9 100644
--- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
+++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
@@ -1,17 +1,56 @@
 #include "xiaomi_lywsd02.h"
 #include "esphome/core/log.h"
 
-#ifdef ARDUINO_ARCH_ESP32
+#ifdef USE_ESP32
 
 namespace esphome {
 namespace xiaomi_lywsd02 {
 
-static const char *TAG = "xiaomi_lywsd02";
+static const char *const TAG = "xiaomi_lywsd02";
 
 void XiaomiLYWSD02::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02");
   LOG_SENSOR("  ", "Temperature", this->temperature_);
   LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiLYWSD02::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
 }
 
 }  // namespace xiaomi_lywsd02
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
index 9b8aba1bb0..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 {
@@ -14,30 +14,19 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis
  public:
   void set_address(uint64_t address) { address_ = address; }
 
-  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
-    if (device.address_uint64() != this->address_)
-      return false;
-
-    auto res = xiaomi_ble::parse_xiaomi(device);
-    if (!res.has_value())
-      return false;
-
-    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);
-    return true;
-  }
+  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; }
 
  protected:
   uint64_t address_;
   sensor::Sensor *temperature_{nullptr};
   sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
 };
 
 }  // namespace xiaomi_lywsd02
diff --git a/esphome/components/xiaomi_lywsd03mmc/__init__.py b/esphome/components/xiaomi_lywsd03mmc/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py
new file mode 100644
index 0000000000..b2784e58fc
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py
@@ -0,0 +1,78 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_HUMIDITY,
+    CONF_ID,
+    CONF_BINDKEY,
+    DEVICE_CLASS_BATTERY,
+)
+
+CODEOWNERS = ["@ahpohl"]
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_lywsd03mmc_ns = cg.esphome_ns.namespace("xiaomi_lywsd03mmc")
+XiaomiLYWSD03MMC = xiaomi_lywsd03mmc_ns.class_(
+    "XiaomiLYWSD03MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiLYWSD03MMC),
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_bindkey(config[CONF_BINDKEY]))
+
+    if 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))
diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp
new file mode 100644
index 0000000000..d0319c9474
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp
@@ -0,0 +1,77 @@
+#include "xiaomi_lywsd03mmc.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsd03mmc {
+
+static const char *const TAG = "xiaomi_lywsd03mmc";
+
+void XiaomiLYWSD03MMC::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiLYWSD03MMC::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 (res->humidity.has_value() && this->humidity_ != nullptr) {
+      // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254
+      *res->humidity = trunc(*res->humidity);
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
+}  // namespace xiaomi_lywsd03mmc
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h
new file mode 100644
index 0000000000..95710a1508
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsd03mmc {
+
+class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; };
+  void set_bindkey(const std::string &bindkey);
+
+  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; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_lywsd03mmc
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py
index e13c860464..80f24ac0ef 100644
--- a/esphome/components/xiaomi_lywsdcgq/sensor.py
+++ b/esphome/components/xiaomi_lywsdcgq/sensor.py
@@ -1,38 +1,73 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor, esp32_ble_tracker
-from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
-    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_BATTERY,
+    CONF_ID,
+)
 
-DEPENDENCIES = ['esp32_ble_tracker']
-AUTO_LOAD = ['xiaomi_ble']
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
 
-xiaomi_lywsdcgq_ns = cg.esphome_ns.namespace('xiaomi_lywsdcgq')
-XiaomiLYWSDCGQ = xiaomi_lywsdcgq_ns.class_('XiaomiLYWSDCGQ', esp32_ble_tracker.ESPBTDeviceListener,
-                                           cg.Component)
+xiaomi_lywsdcgq_ns = cg.esphome_ns.namespace("xiaomi_lywsdcgq")
+XiaomiLYWSDCGQ = xiaomi_lywsdcgq_ns.class_(
+    "XiaomiLYWSDCGQ", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
 
-CONFIG_SCHEMA = cv.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_THERMOMETER, 1),
-    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
-    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
 
 
-def to_code(config):
+async 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)
+    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 = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp
index 2dacff2876..749ca83afb 100644
--- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp
+++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp
@@ -1,12 +1,12 @@
 #include "xiaomi_lywsdcgq.h"
 #include "esphome/core/log.h"
 
-#ifdef ARDUINO_ARCH_ESP32
+#ifdef USE_ESP32
 
 namespace esphome {
 namespace xiaomi_lywsdcgq {
 
-static const char *TAG = "xiaomi_lywsdcgq";
+static const char *const TAG = "xiaomi_lywsdcgq";
 
 void XiaomiLYWSDCGQ::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi LYWSDCGQ");
@@ -15,6 +15,44 @@ void XiaomiLYWSDCGQ::dump_config() {
   LOG_SENSOR("  ", "Battery Level", this->battery_level_);
 }
 
+bool XiaomiLYWSDCGQ::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
 }  // namespace xiaomi_lywsdcgq
 }  // namespace esphome
 
diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h
index b6756eec61..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 {
@@ -14,22 +14,7 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi
  public:
   void set_address(uint64_t address) { address_ = address; }
 
-  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
-    if (device.address_uint64() != this->address_)
-      return false;
-
-    auto res = xiaomi_ble::parse_xiaomi(device);
-    if (!res.has_value())
-      return false;
-
-    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);
-    return true;
-  }
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
diff --git a/esphome/components/xiaomi_mhoc401/__init__.py b/esphome/components/xiaomi_mhoc401/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py
new file mode 100644
index 0000000000..9e92e34230
--- /dev/null
+++ b/esphome/components/xiaomi_mhoc401/sensor.py
@@ -0,0 +1,77 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_HUMIDITY,
+    CONF_ID,
+    CONF_BINDKEY,
+    DEVICE_CLASS_BATTERY,
+)
+
+CODEOWNERS = ["@vevsvevs"]
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_mhoc401_ns = cg.esphome_ns.namespace("xiaomi_mhoc401")
+XiaomiMHOC401 = xiaomi_mhoc401_ns.class_(
+    "XiaomiMHOC401", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiMHOC401),
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_bindkey(config[CONF_BINDKEY]))
+
+    if 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))
diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp
new file mode 100644
index 0000000000..9ec2b10e12
--- /dev/null
+++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp
@@ -0,0 +1,77 @@
+#include "xiaomi_mhoc401.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_mhoc401 {
+
+static const char *const TAG = "xiaomi_mhoc401";
+
+void XiaomiMHOC401::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi MHOC401");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiMHOC401::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 (res->humidity.has_value() && this->humidity_ != nullptr) {
+      // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254
+      *res->humidity = trunc(*res->humidity);
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiMHOC401::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
+}  // namespace xiaomi_mhoc401
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h
new file mode 100644
index 0000000000..4ab882b2af
--- /dev/null
+++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_mhoc401 {
+
+class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; };
+  void set_bindkey(const std::string &bindkey);
+
+  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; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_mhoc401
+}  // namespace esphome
+
+#endif
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/__init__.py b/esphome/components/xiaomi_miscale/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py
new file mode 100644
index 0000000000..517870cc01
--- /dev/null
+++ b/esphome/components/xiaomi_miscale/sensor.py
@@ -0,0 +1,59 @@
+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,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+
+xiaomi_miscale_ns = cg.esphome_ns.namespace("xiaomi_miscale")
+XiaomiMiscale = xiaomi_miscale_ns.class_(
+    "XiaomiMiscale", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiMiscale),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_WEIGHT): sensor.sensor_schema(
+                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,
+            ),
+        }
+    )
+    .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_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp
new file mode 100644
index 0000000000..de77e6146b
--- /dev/null
+++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp
@@ -0,0 +1,162 @@
+#include "xiaomi_miscale.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_miscale {
+
+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) {
+  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->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) {
+  ParseResult result;
+  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) {
+  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)
+  // 6 day (MISCALE 181D)
+  // 7 hour (MISCALE 181D)
+  // 8 minute (MISCALE 181D)
+  // 9 second (MISCALE 181D)
+
+  const uint8_t *data = message.data();
+
+  // 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.6f;  // unit 'jin'
+  else if (data[0] == 0x03 || data[0] == 0xb3)
+    result.weight = weight * 0.01f * 0.453592f;  // unit 'lbs'
+
+  return true;
+}
+
+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 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;
+}
+
+}  // namespace xiaomi_miscale
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h
new file mode 100644
index 0000000000..3e51405ddc
--- /dev/null
+++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_miscale {
+
+struct ParseResult {
+  int version;
+  optional weight;
+  optional impedance;
+};
+
+class XiaomiMiscale : 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 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
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_miscale2/__init__.py b/esphome/components/xiaomi_miscale2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_miscale2/sensor.py b/esphome/components/xiaomi_miscale2/sensor.py
new file mode 100644
index 0000000000..de04e8171e
--- /dev/null
+++ b/esphome/components/xiaomi_miscale2/sensor.py
@@ -0,0 +1,5 @@
+import esphome.config_validation as cv
+
+CONFIG_SCHEMA = cv.invalid(
+    "This platform has been combined into xiaomi_miscale. Use xiaomi_miscale instead."
+)
diff --git a/esphome/components/xiaomi_mjyd02yla/__init__.py b/esphome/components/xiaomi_mjyd02yla/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
new file mode 100644
index 0000000000..1bedae26cf
--- /dev/null
+++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
@@ -0,0 +1,97 @@
+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_MAC_ADDRESS,
+    CONF_ID,
+    CONF_BINDKEY,
+    CONF_DEVICE_CLASS,
+    CONF_LIGHT,
+    CONF_BATTERY_LEVEL,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_ILLUMINANCE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
+    UNIT_PERCENT,
+    CONF_IDLE_TIME,
+    CONF_ILLUMINANCE,
+    UNIT_MINUTE,
+    UNIT_LUX,
+    ICON_TIMELAPSE,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_mjyd02yla_ns = cg.esphome_ns.namespace("xiaomi_mjyd02yla")
+XiaomiMJYD02YLA = xiaomi_mjyd02yla_ns.class_(
+    "XiaomiMJYD02YLA",
+    binary_sensor.BinarySensor,
+    cg.Component,
+    esp32_ble_tracker.ESPBTDeviceListener,
+)
+
+CONFIG_SCHEMA = cv.All(
+    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiMJYD02YLA),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Optional(
+                CONF_DEVICE_CLASS, default="motion"
+            ): binary_sensor.device_class,
+            cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(
+                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_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+            cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_LUX,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_ILLUMINANCE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+                {
+                    cv.Optional(
+                        CONF_DEVICE_CLASS, default="light"
+                    ): binary_sensor.device_class,
+                }
+            ),
+        }
+    )
+    .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)
+    await 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 = await sensor.new_sensor(config[CONF_IDLE_TIME])
+        cg.add(var.set_idle_time(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_ILLUMINANCE in config:
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
+        cg.add(var.set_illuminance(sens))
+    if CONF_LIGHT in config:
+        sens = await binary_sensor.new_binary_sensor(config[CONF_LIGHT])
+        cg.add(var.set_light(sens))
diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp
new file mode 100644
index 0000000000..16c0b42279
--- /dev/null
+++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp
@@ -0,0 +1,78 @@
+#include "xiaomi_mjyd02yla.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_mjyd02yla {
+
+static const char *const TAG = "xiaomi_mjyd02yla";
+
+void XiaomiMJYD02YLA::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi MJYD02YL-A");
+  LOG_BINARY_SENSOR("  ", "Motion", this);
+  LOG_BINARY_SENSOR("  ", "Light", this->is_light_);
+  LOG_SENSOR("  ", "Idle Time", this->idle_time_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+  LOG_SENSOR("  ", "Illuminance", this->illuminance_);
+}
+
+bool XiaomiMJYD02YLA::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->is_light.has_value() && this->is_light_ != nullptr)
+      this->is_light_->publish_state(*res->is_light);
+    if (res->has_motion.has_value())
+      this->publish_state(*res->has_motion);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
+}  // namespace xiaomi_mjyd02yla
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h
new file mode 100644
index 0000000000..34b1fe4af0
--- /dev/null
+++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h
@@ -0,0 +1,42 @@
+#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_mjyd02yla {
+
+class XiaomiMJYD02YLA : 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_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; }
+  void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
+  void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
+  void set_light(binary_sensor::BinarySensor *light) { is_light_ = light; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *idle_time_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+  sensor::Sensor *illuminance_{nullptr};
+  binary_sensor::BinarySensor *is_light_{nullptr};
+};
+
+}  // namespace xiaomi_mjyd02yla
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_mue4094rt/__init__.py b/esphome/components/xiaomi_mue4094rt/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py
new file mode 100644
index 0000000000..5d19263c9c
--- /dev/null
+++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py
@@ -0,0 +1,43 @@
+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_DEVICE_CLASS, CONF_TIMEOUT, CONF_ID
+
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_mue4094rt_ns = cg.esphome_ns.namespace("xiaomi_mue4094rt")
+XiaomiMUE4094RT = xiaomi_mue4094rt_ns.class_(
+    "XiaomiMUE4094RT",
+    binary_sensor.BinarySensor,
+    cg.Component,
+    esp32_ble_tracker.ESPBTDeviceListener,
+)
+
+CONFIG_SCHEMA = cv.All(
+    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiMUE4094RT),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(
+                CONF_DEVICE_CLASS, default="motion"
+            ): binary_sensor.device_class,
+            cv.Optional(
+                CONF_TIMEOUT, default="5s"
+            ): cv.positive_time_period_milliseconds,
+        }
+    )
+    .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)
+    await binary_sensor.register_binary_sensor(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_time(config[CONF_TIMEOUT]))
diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp
new file mode 100644
index 0000000000..1a8e72bd2c
--- /dev/null
+++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp
@@ -0,0 +1,55 @@
+#include "xiaomi_mue4094rt.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_mue4094rt {
+
+static const char *const TAG = "xiaomi_mue4094rt";
+
+void XiaomiMUE4094RT::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi MUE4094RT");
+  LOG_BINARY_SENSOR("  ", "Motion", this);
+}
+
+bool XiaomiMUE4094RT::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      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->has_motion.has_value()) {
+      this->publish_state(*res->has_motion);
+      this->set_timeout("motion_timeout", timeout_, [this]() { this->publish_state(false); });
+    }
+    success = true;
+  }
+
+  return success;
+}
+
+}  // namespace xiaomi_mue4094rt
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h
new file mode 100644
index 0000000000..904c575ae6
--- /dev/null
+++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "esphome/core/component.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_mue4094rt {
+
+class XiaomiMUE4094RT : public Component,
+                        public binary_sensor::BinarySensorInitiallyOff,
+                        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_time(uint16_t timeout) { timeout_ = timeout; }
+
+ protected:
+  uint64_t address_;
+  uint16_t timeout_;
+};
+
+}  // namespace xiaomi_mue4094rt
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_wx08zm/__init__.py b/esphome/components/xiaomi_wx08zm/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py
new file mode 100644
index 0000000000..8667794923
--- /dev/null
+++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py
@@ -0,0 +1,66 @@
+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_MAC_ADDRESS,
+    CONF_TABLET,
+    DEVICE_CLASS_BATTERY,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PERCENT,
+    ICON_BUG,
+    CONF_ID,
+)
+
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_wx08zm_ns = cg.esphome_ns.namespace("xiaomi_wx08zm")
+XiaomiWX08ZM = xiaomi_wx08zm_ns.class_(
+    "XiaomiWX08ZM",
+    binary_sensor.BinarySensor,
+    esp32_ble_tracker.ESPBTDeviceListener,
+    cg.Component,
+)
+
+CONFIG_SCHEMA = cv.All(
+    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiWX08ZM),
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TABLET): sensor.sensor_schema(
+                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_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if CONF_TABLET in config:
+        sens = await sensor.new_sensor(config[CONF_TABLET])
+        cg.add(var.set_tablet(sens))
+    if CONF_BATTERY_LEVEL in config:
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp
new file mode 100644
index 0000000000..b57bf5cd05
--- /dev/null
+++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp
@@ -0,0 +1,60 @@
+#include "xiaomi_wx08zm.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_wx08zm {
+
+static const char *const TAG = "xiaomi_wx08zm";
+
+void XiaomiWX08ZM::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi WX08ZM");
+  LOG_BINARY_SENSOR("  ", "Mosquito Repellent", this);
+  LOG_SENSOR("  ", "Tablet Resource", this->tablet_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiWX08ZM::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) {
+      ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
+      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->is_active.has_value()) {
+      this->publish_state(*res->is_active);
+    }
+    if (res->tablet.has_value() && this->tablet_ != nullptr)
+      this->tablet_->publish_state(*res->tablet);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+}  // namespace xiaomi_wx08zm
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h
new file mode 100644
index 0000000000..297c7ab47d
--- /dev/null
+++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h
@@ -0,0 +1,36 @@
+#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_wx08zm {
+
+class XiaomiWX08ZM : public Component,
+                     public binary_sensor::BinarySensorInitiallyOff,
+                     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_tablet(sensor::Sensor *tablet) { tablet_ = tablet; }
+  void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *tablet_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_wx08zm
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xpt2046/__init__.py b/esphome/components/xpt2046/__init__.py
new file mode 100644
index 0000000000..3de89a6425
--- /dev/null
+++ b/esphome/components/xpt2046/__init__.py
@@ -0,0 +1,129 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome import pins
+from esphome.components import spi
+from esphome.const import CONF_ID, CONF_ON_STATE, CONF_THRESHOLD, CONF_TRIGGER_ID
+
+CODEOWNERS = ["@numo68"]
+AUTO_LOAD = ["binary_sensor"]
+DEPENDENCIES = ["spi"]
+MULTI_CONF = True
+
+CONF_REPORT_INTERVAL = "report_interval"
+CONF_CALIBRATION_X_MIN = "calibration_x_min"
+CONF_CALIBRATION_X_MAX = "calibration_x_max"
+CONF_CALIBRATION_Y_MIN = "calibration_y_min"
+CONF_CALIBRATION_Y_MAX = "calibration_y_max"
+CONF_DIMENSION_X = "dimension_x"
+CONF_DIMENSION_Y = "dimension_y"
+CONF_SWAP_X_Y = "swap_x_y"
+CONF_IRQ_PIN = "irq_pin"
+
+xpt2046_ns = cg.esphome_ns.namespace("xpt2046")
+CONF_XPT2046_ID = "xpt2046_id"
+
+XPT2046Component = xpt2046_ns.class_(
+    "XPT2046Component", cg.PollingComponent, spi.SPIDevice
+)
+
+XPT2046OnStateTrigger = xpt2046_ns.class_(
+    "XPT2046OnStateTrigger", automation.Trigger.template(cg.int_, cg.int_, cg.bool_)
+)
+
+
+def validate_xpt2046(config):
+    if (
+        abs(
+            cv.int_(config[CONF_CALIBRATION_X_MAX])
+            - cv.int_(config[CONF_CALIBRATION_X_MIN])
+        )
+        < 1000
+    ):
+        raise cv.Invalid("Calibration X values difference < 1000")
+
+    if (
+        abs(
+            cv.int_(config[CONF_CALIBRATION_Y_MAX])
+            - cv.int_(config[CONF_CALIBRATION_Y_MIN])
+        )
+        < 1000
+    ):
+        raise cv.Invalid("Calibration Y values difference < 1000")
+
+    return config
+
+
+def report_interval(value):
+    if value == "never":
+        return 4294967295  # uint32_t max
+    return cv.positive_time_period_milliseconds(value)
+
+
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XPT2046Component),
+            cv.Optional(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
+            cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_DIMENSION_X, default=100): cv.positive_not_null_int,
+            cv.Optional(CONF_DIMENSION_Y, default=100): cv.positive_not_null_int,
+            cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095),
+            cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval,
+            cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean,
+            cv.Optional(CONF_ON_STATE): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                        XPT2046OnStateTrigger
+                    ),
+                }
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("50ms"))
+    .extend(spi.spi_device_schema()),
+    validate_xpt2046,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+
+    cg.add(var.set_threshold(config[CONF_THRESHOLD]))
+    cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL]))
+    cg.add(var.set_dimensions(config[CONF_DIMENSION_X], config[CONF_DIMENSION_Y]))
+    cg.add(
+        var.set_calibration(
+            config[CONF_CALIBRATION_X_MIN],
+            config[CONF_CALIBRATION_X_MAX],
+            config[CONF_CALIBRATION_Y_MIN],
+            config[CONF_CALIBRATION_Y_MAX],
+        )
+    )
+
+    if CONF_SWAP_X_Y in config:
+        cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y]))
+
+    if CONF_IRQ_PIN in config:
+        pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
+        cg.add(var.set_irq_pin(pin))
+
+    for conf in config.get(CONF_ON_STATE, []):
+        await automation.build_automation(
+            var.get_on_state_trigger(),
+            [(cg.int_, "x"), (cg.int_, "y"), (cg.bool_, "touched")],
+            conf,
+        )
diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py
new file mode 100644
index 0000000000..6959d6c342
--- /dev/null
+++ b/esphome/components/xpt2046/binary_sensor.py
@@ -0,0 +1,57 @@
+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 (
+    xpt2046_ns,
+    XPT2046Component,
+    CONF_XPT2046_ID,
+)
+
+CONF_X_MIN = "x_min"
+CONF_X_MAX = "x_max"
+CONF_Y_MIN = "y_min"
+CONF_Y_MAX = "y_max"
+
+DEPENDENCIES = ["xpt2046"]
+XPT2046Button = xpt2046_ns.class_("XPT2046Button", binary_sensor.BinarySensor)
+
+
+def validate_xpt2046_button(config):
+    if cv.int_(config[CONF_X_MAX]) < cv.int_(config[CONF_X_MIN]) or cv.int_(
+        config[CONF_Y_MAX]
+    ) < cv.int_(config[CONF_Y_MIN]):
+        raise cv.Invalid("x_max is less than x_min or y_max is less than y_min")
+
+    return config
+
+
+CONFIG_SCHEMA = cv.All(
+    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(XPT2046Button),
+            cv.GenerateID(CONF_XPT2046_ID): cv.use_id(XPT2046Component),
+            cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095),
+        }
+    ),
+    validate_xpt2046_button,
+)
+
+
+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_XPT2046_ID])
+    cg.add(
+        var.set_area(
+            config[CONF_X_MIN],
+            config[CONF_X_MAX],
+            config[CONF_Y_MIN],
+            config[CONF_Y_MAX],
+        )
+    )
+
+    cg.add(hub.register_button(var))
diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp
new file mode 100644
index 0000000000..aaadeea52e
--- /dev/null
+++ b/esphome/components/xpt2046/xpt2046.cpp
@@ -0,0 +1,217 @@
+#include "xpt2046.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+#include 
+
+namespace esphome {
+namespace xpt2046 {
+
+static const char *const TAG = "xpt2046";
+
+void XPT2046Component::setup() {
+  if (this->irq_pin_ != nullptr) {
+    // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state
+    // while the channels are read and wiring it as an interrupt is not straightforward and would
+    // need careful masking. A GPIO poll is cheap so we'll just use that.
+    this->irq_pin_->setup();  // INPUT
+  }
+  spi_setup();
+  read_adc_(0xD0);  // ADC powerdown, enable PENIRQ pin
+}
+
+void XPT2046Component::loop() {
+  if (this->irq_pin_ != nullptr) {
+    // Force immediate update if a falling edge (= touched is seen) Ignore if still active
+    // (that would mean that we missed the release because of a too long update interval)
+    bool val = this->irq_pin_->digital_read();
+    if (!val && this->last_irq_ && !this->touched) {
+      ESP_LOGD(TAG, "Falling penirq edge, forcing update");
+      update();
+    }
+    this->last_irq_ = val;
+  }
+}
+
+void XPT2046Component::update() {
+  int16_t data[6];
+  bool touch = false;
+  uint32_t now = millis();
+
+  this->z_raw = 0;
+
+  // In case the penirq pin is present only do the SPI transaction if it reports a touch (is low).
+  // The touch has to be also confirmed with checking the pressure over threshold
+  if (this->irq_pin_ == nullptr || !this->irq_pin_->digital_read()) {
+    enable();
+
+    int16_t z1 = read_adc_(0xB1 /* Z1 */);
+    int16_t z2 = read_adc_(0xC1 /* Z2 */);
+
+    this->z_raw = z1 + 4095 - z2;
+
+    touch = (this->z_raw >= this->threshold_);
+    if (touch) {
+      read_adc_(0x91 /* Y */);  // dummy Y measure, 1st is always noisy
+      data[0] = read_adc_(0xD1 /* X */);
+      data[1] = read_adc_(0x91 /* Y */);  // make 3 x-y measurements
+      data[2] = read_adc_(0xD1 /* X */);
+      data[3] = read_adc_(0x91 /* Y */);
+      data[4] = read_adc_(0xD1 /* X */);
+    }
+
+    data[5] = read_adc_(0x90 /* Y */);  // Last Y touch power down
+
+    disable();
+  }
+
+  if (touch) {
+    this->x_raw = best_two_avg(data[0], data[2], data[4]);
+    this->y_raw = best_two_avg(data[1], data[3], data[5]);
+  } else {
+    this->x_raw = this->y_raw = 0;
+  }
+
+  ESP_LOGV(TAG, "Update [x, y] = [%d, %d], z = %d%s", this->x_raw, this->y_raw, this->z_raw, (touch ? " touched" : ""));
+
+  if (touch) {
+    // Normalize raw data according to calibration min and max
+
+    int16_t x_val = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_);
+    int16_t y_val = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_);
+
+    if (this->swap_x_y_) {
+      std::swap(x_val, y_val);
+    }
+
+    if (this->invert_x_) {
+      x_val = 0x7fff - x_val;
+    }
+
+    if (this->invert_y_) {
+      y_val = 0x7fff - y_val;
+    }
+
+    x_val = (int16_t)((int) x_val * this->x_dim_ / 0x7fff);
+    y_val = (int16_t)((int) y_val * this->y_dim_ / 0x7fff);
+
+    if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) {
+      ESP_LOGD(TAG, "Raw [x, y] = [%d, %d], transformed = [%d, %d]", this->x_raw, this->y_raw, x_val, y_val);
+
+      this->x = x_val;
+      this->y = y_val;
+      this->touched = true;
+      this->last_pos_ms_ = now;
+
+      this->on_state_trigger_->process(this->x, this->y, true);
+      for (auto *button : this->buttons_)
+        button->touch(this->x, this->y);
+    }
+  } else {
+    if (this->touched) {
+      ESP_LOGD(TAG, "Released [%d, %d]", this->x, this->y);
+
+      this->touched = false;
+
+      this->on_state_trigger_->process(this->x, this->y, false);
+      for (auto *button : this->buttons_)
+        button->release();
+    }
+  }
+}
+
+void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) {
+  this->x_raw_min_ = std::min(x_min, x_max);
+  this->x_raw_max_ = std::max(x_min, x_max);
+  this->y_raw_min_ = std::min(y_min, y_max);
+  this->y_raw_max_ = std::max(y_min, y_max);
+  this->invert_x_ = (x_min > x_max);
+  this->invert_y_ = (y_min > y_max);
+}
+
+void XPT2046Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "XPT2046:");
+
+  LOG_PIN("  IRQ Pin: ", this->irq_pin_);
+  ESP_LOGCONFIG(TAG, "  X min: %d", this->x_raw_min_);
+  ESP_LOGCONFIG(TAG, "  X max: %d", this->x_raw_max_);
+  ESP_LOGCONFIG(TAG, "  Y min: %d", this->y_raw_min_);
+  ESP_LOGCONFIG(TAG, "  Y max: %d", this->y_raw_max_);
+  ESP_LOGCONFIG(TAG, "  X dim: %d", this->x_dim_);
+  ESP_LOGCONFIG(TAG, "  Y dim: %d", this->y_dim_);
+  if (this->swap_x_y_) {
+    ESP_LOGCONFIG(TAG, "  Swap X/Y");
+  }
+  ESP_LOGCONFIG(TAG, "  threshold: %d", this->threshold_);
+  ESP_LOGCONFIG(TAG, "  Report interval: %u", this->report_millis_);
+
+  LOG_UPDATE_INTERVAL(this);
+}
+
+float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; }
+
+int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) {
+  int16_t da, db, dc;
+  int16_t reta = 0;
+
+  da = (x > y) ? x - y : y - x;
+  db = (x > z) ? x - z : z - x;
+  dc = (z > y) ? z - y : y - z;
+
+  if (da <= db && da <= dc) {
+    reta = (x + y) >> 1;
+  } else if (db <= da && db <= dc) {
+    reta = (x + z) >> 1;
+  } else {
+    reta = (y + z) >> 1;
+  }
+
+  return reta;
+}
+
+int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) {
+  int16_t ret;
+
+  if (val <= min_val) {
+    ret = 0;
+  } else if (val >= max_val) {
+    ret = 0x7fff;
+  } else {
+    ret = (int16_t)((int) 0x7fff * (val - min_val) / (max_val - min_val));
+  }
+
+  return ret;
+}
+
+int16_t XPT2046Component::read_adc_(uint8_t ctrl) {
+  uint8_t data[2];
+
+  write_byte(ctrl);
+  data[0] = read_byte();
+  data[1] = read_byte();
+
+  return ((data[0] << 8) | data[1]) >> 3;
+}
+
+void XPT2046OnStateTrigger::process(int x, int y, bool touched) { this->trigger(x, y, touched); }
+
+void XPT2046Button::touch(int16_t x, int16_t y) {
+  bool touched = (x >= this->x_min_ && x <= this->x_max_ && y >= this->y_min_ && y <= this->y_max_);
+
+  if (touched) {
+    this->publish_state(true);
+    this->state_ = true;
+  } else {
+    release();
+  }
+}
+
+void XPT2046Button::release() {
+  if (this->state_) {
+    this->publish_state(false);
+    this->state_ = false;
+  }
+}
+
+}  // namespace xpt2046
+}  // namespace esphome
diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h
new file mode 100644
index 0000000000..e7270f7d7d
--- /dev/null
+++ b/esphome/components/xpt2046/xpt2046.h
@@ -0,0 +1,124 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/spi/spi.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace xpt2046 {
+
+class XPT2046OnStateTrigger : public Trigger {
+ public:
+  void process(int x, int y, bool touched);
+};
+
+class XPT2046Button : public binary_sensor::BinarySensor {
+ public:
+  /// Set the touch screen area where the button will detect the touch.
+  void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) {
+    this->x_min_ = x_min;
+    this->x_max_ = x_max;
+    this->y_min_ = y_min;
+    this->y_max_ = y_max;
+  }
+
+  void touch(int16_t x, int16_t y);
+  void release();
+
+ protected:
+  int16_t x_min_, x_max_, y_min_, y_max_;
+  bool state_{false};
+};
+
+class XPT2046Component : public PollingComponent,
+                         public spi::SPIDevice {
+ public:
+  /// Set the logical touch screen dimensions.
+  void set_dimensions(int16_t x, int16_t y) {
+    this->x_dim_ = x;
+    this->y_dim_ = y;
+  }
+  /// Set the coordinates for the touch screen edges.
+  void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max);
+  /// If true the x and y axes will be swapped
+  void set_swap_x_y(bool val) { this->swap_x_y_ = val; }
+
+  /// Set the interval to report the touch point perodically.
+  void set_report_interval(uint32_t interval) { this->report_millis_ = interval; }
+  /// Set the threshold for the touch detection.
+  void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
+  /// Set the pin used to detect the touch.
+  void set_irq_pin(GPIOPin *pin) { this->irq_pin_ = pin; }
+  /// Get an access to the on_state automation trigger
+  XPT2046OnStateTrigger *get_on_state_trigger() const { return this->on_state_trigger_; }
+  /// Register a virtual button to the component.
+  void register_button(XPT2046Button *button) { this->buttons_.push_back(button); }
+
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+
+  /** Detect the touch if the irq pin is specified.
+   *
+   * If the touch is detected and the component does not already know about it
+   * the update() is called immediately. If the irq pin is not specified
+   * the loop() is a no-op.
+   */
+  void loop() override;
+
+  /** Read and process the values from the hardware.
+   *
+   * Read the raw x, y and touch pressure values from the chip, detect the touch,
+   * and if touched, transform to the user x and y coordinates. If the state has
+   * changed or if the value should be reported again due to the
+   * report interval, run the action and inform the virtual buttons.
+   */
+  void update() override;
+
+  /**@{*/
+  /** Coordinates of the touch position.
+   *
+   * The values are set immediately before the on_state action with touched == true
+   * is triggered. The action with touched == false sends the coordinates of the last
+   * reported touch.
+   */
+  int16_t x{0}, y{0};
+  /**@}*/
+
+  /// True if the component currently detects the touch
+  bool touched{false};
+
+  /**@{*/
+  /** Raw sensor values of the coordinates and the pressure.
+   *
+   * The values are set each time the update() method is called.
+   */
+  int16_t x_raw{0}, y_raw{0}, z_raw{0};
+  /**@}*/
+
+ protected:
+  static int16_t best_two_avg(int16_t x, int16_t y, int16_t z);
+  static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val);
+
+  int16_t read_adc_(uint8_t ctrl);
+
+  int16_t threshold_;
+  int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_;
+  int16_t x_dim_, y_dim_;
+  bool invert_x_, invert_y_;
+  bool swap_x_y_;
+
+  uint32_t report_millis_;
+  uint32_t last_pos_ms_{0};
+
+  GPIOPin *irq_pin_{nullptr};
+  bool last_irq_{true};
+
+  XPT2046OnStateTrigger *on_state_trigger_{new XPT2046OnStateTrigger()};
+  std::vector buttons_{};
+};
+
+}  // namespace xpt2046
+}  // namespace esphome
diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py
index 4c4b98d9e7..8cafd468ac 100644
--- a/esphome/components/yashima/climate.py
+++ b/esphome/components/yashima/climate.py
@@ -4,30 +4,36 @@ from esphome.components import climate, remote_transmitter, sensor
 from esphome.components.remote_base import CONF_TRANSMITTER_ID
 from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 
-AUTO_LOAD = ['sensor']
+AUTO_LOAD = ["sensor"]
 
-yashima_ns = cg.esphome_ns.namespace('yashima')
-YashimaClimate = yashima_ns.class_('YashimaClimate', climate.Climate, cg.Component)
+yashima_ns = cg.esphome_ns.namespace("yashima")
+YashimaClimate = yashima_ns.class_("YashimaClimate", climate.Climate, cg.Component)
 
-CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
-    cv.GenerateID(): cv.declare_id(YashimaClimate),
-    cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
-    cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
-    cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
-    cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
-}).extend(cv.COMPONENT_SCHEMA))
+CONFIG_SCHEMA = cv.All(
+    climate.CLIMATE_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(YashimaClimate),
+            cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(
+                remote_transmitter.RemoteTransmitterComponent
+            ),
+            cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
+            cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
+            cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
+        }
+    ).extend(cv.COMPONENT_SCHEMA)
+)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
     cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
     cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
     if CONF_SENSOR in config:
-        sens = yield cg.get_variable(config[CONF_SENSOR])
+        sens = await cg.get_variable(config[CONF_SENSOR])
         cg.add(var.set_sensor(sens))
 
-    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
+    transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
     cg.add(var.set_transmitter(transmitter))
diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp
index e3c0a33127..8d588127b0 100644
--- a/esphome/components/yashima/yashima.cpp
+++ b/esphome/components/yashima/yashima.cpp
@@ -4,7 +4,7 @@
 namespace esphome {
 namespace yashima {
 
-static const char *TAG = "yashima.climate";
+static const char *const TAG = "yashima.climate";
 
 const uint16_t YASHIMA_STATE_LENGTH = 9;
 const uint16_t YASHIMA_BITS = YASHIMA_STATE_LENGTH * 8;
@@ -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 649b80b444..28a708b866 100644
--- a/esphome/components/zyaura/sensor.py
+++ b/esphome/components/zyaura/sensor.py
@@ -2,42 +2,70 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import sensor
-from esphome.const import CONF_ID, CONF_CLOCK_PIN, CONF_DATA_PIN, \
-    CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, \
-    UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, UNIT_PERCENT, \
-    ICON_PERIODIC_TABLE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT
+from esphome.const import (
+    CONF_ID,
+    CONF_CLOCK_PIN,
+    CONF_DATA_PIN,
+    CONF_CO2,
+    CONF_TEMPERATURE,
+    CONF_HUMIDITY,
+    DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PARTS_PER_MILLION,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    ICON_MOLECULE_CO2,
+)
 from esphome.cpp_helpers import gpio_pin_expression
 
-zyaura_ns = cg.esphome_ns.namespace('zyaura')
-ZyAuraSensor = zyaura_ns.class_('ZyAuraSensor', cg.PollingComponent)
+zyaura_ns = cg.esphome_ns.namespace("zyaura")
+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.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0),
-    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
-    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
-}).extend(cv.polling_component_schema('60s'))
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(ZyAuraSensor),
+        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_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,
+            accuracy_decimals=1,
+            device_class=DEVICE_CLASS_TEMPERATURE,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+            unit_of_measurement=UNIT_PERCENT,
+            accuracy_decimals=1,
+            device_class=DEVICE_CLASS_HUMIDITY,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+    }
+).extend(cv.polling_component_schema("60s"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    pin_clock = yield gpio_pin_expression(config[CONF_CLOCK_PIN])
+    pin_clock = await gpio_pin_expression(config[CONF_CLOCK_PIN])
     cg.add(var.set_pin_clock(pin_clock))
-    pin_data = yield gpio_pin_expression(config[CONF_DATA_PIN])
+    pin_data = await gpio_pin_expression(config[CONF_DATA_PIN])
     cg.add(var.set_pin_data(pin_data))
 
     if CONF_CO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_CO2])
+        sens = await sensor.new_sensor(config[CONF_CO2])
         cg.add(var.set_co2_sensor(sens))
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp
index 3b1a2a5069..621439aa0c 100644
--- a/esphome/components/zyaura/zyaura.cpp
+++ b/esphome/components/zyaura/zyaura.cpp
@@ -4,9 +4,9 @@
 namespace esphome {
 namespace zyaura {
 
-static const char *TAG = "zyaura";
+static const char *const TAG = "zyaura";
 
-bool ICACHE_RAM_ATTR ZaDataProcessor::decode(unsigned long 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,58 +37,66 @@ bool ICACHE_RAM_ATTR ZaDataProcessor::decode(unsigned long 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);
+      this->humidity = message->value;
       break;
-
     case TEMPERATURE:
-      this->temperature = (message->value > 5970) ? NAN : (message->value / 16.0f - 273.15f);
+      this->temperature = message->value;
       break;
-
     case CO2:
-      this->co2 = (message->value > 10000) ? NAN : message->value;
-      break;
-
-    default:
+      this->co2 = message->value;
       break;
   }
 }
 
-bool ZyAuraSensor::publish_state_(sensor::Sensor *sensor, float *value) {
-  // Sensor doesn't added to configuration
+bool ZyAuraSensor::publish_state_(ZaDataType data_type, sensor::Sensor *sensor, uint16_t *data_value) {
+  // Sensor wasn't added to configuration
   if (sensor == nullptr) {
     return true;
   }
 
-  sensor->publish_state(*value);
+  float value = NAN;
+  switch (data_type) {
+    case HUMIDITY:
+      value = (*data_value > 10000) ? NAN : (*data_value / 100.0f);
+      break;
+    case TEMPERATURE:
+      value = (*data_value > 5970) ? NAN : (*data_value / 16.0f - 273.15f);
+      break;
+    case CO2:
+      value = (*data_value > 10000) ? NAN : *data_value;
+      break;
+  }
+
+  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;
   }
 
-  *value = NAN;
+  *data_value = -1;
   return true;
 }
 
@@ -104,9 +112,9 @@ void ZyAuraSensor::dump_config() {
 }
 
 void ZyAuraSensor::update() {
-  bool co2_result = this->publish_state_(this->co2_sensor_, &this->store_.co2);
-  bool temperature_result = this->publish_state_(this->temperature_sensor_, &this->store_.temperature);
-  bool humidity_result = this->publish_state_(this->humidity_sensor_, &this->store_.humidity);
+  bool co2_result = this->publish_state_(CO2, this->co2_sensor_, &this->store_.co2);
+  bool temperature_result = this->publish_state_(TEMPERATURE, this->temperature_sensor_, &this->store_.temperature);
+  bool humidity_result = this->publish_state_(HUMIDITY, this->humidity_sensor_, &this->store_.humidity);
 
   if (co2_result && temperature_result && humidity_result) {
     this->status_clear_warning();
diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h
index fd26947e28..85c31ec75a 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 {
@@ -31,27 +31,27 @@ struct ZaMessage {
 
 class ZaDataProcessor {
  public:
-  bool decode(unsigned long ms, bool data);
+  bool decode(uint32_t ms, bool data);
   ZaMessage *message = new ZaMessage;
 
  protected:
   uint8_t buffer_[ZA_MSG_LEN];
   int num_bits_ = 0;
-  unsigned long prev_ms_;
+  uint32_t prev_ms_;
 };
 
 class ZaSensorStore {
  public:
-  float co2 = NAN;
-  float temperature = NAN;
-  float humidity = NAN;
+  uint16_t co2 = -1;
+  uint16_t temperature = -1;
+  uint16_t humidity = -1;
 
-  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,13 +73,13 @@ 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};
 
-  bool publish_state_(sensor::Sensor *sensor, float *value);
+  bool publish_state_(ZaDataType data_type, sensor::Sensor *sensor, uint16_t *data_value);
 };
 
 }  // namespace zyaura
diff --git a/esphome/config.py b/esphome/config.py
index 8d7c622a27..af6c5b0b64 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -1,189 +1,51 @@
-import collections
-import importlib
+import abc
+import functools
+import heapq
 import logging
 import re
-import os.path
 
 # pylint: disable=unused-import, wrong-import-order
-import sys
 from contextlib import contextmanager
 
 import voluptuous as vol
 
-from esphome import core, core_config, yaml_util
-from esphome.components import substitutions
-from esphome.components.substitutions import CONF_SUBSTITUTIONS
-from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS
-from esphome.core import CORE, EsphomeError  # noqa
-from esphome.helpers import color, indent
+from esphome import core, yaml_util, loader
+import esphome.core.config as core_config
+from esphome.const import (
+    CONF_ESPHOME,
+    CONF_PLATFORM,
+    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  # noqa
-from esphome.core import ConfigType  # noqa
+from typing import List, Optional, Tuple, Union
+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__)
 
-_COMPONENT_CACHE = {}
-
-
-class ComponentManifest:
-    def __init__(self, module, base_components_path, is_core=False, is_platform=False):
-        self.module = module
-        self._is_core = is_core
-        self.is_platform = is_platform
-        self.base_components_path = base_components_path
-
-    @property
-    def is_platform_component(self):
-        return getattr(self.module, 'IS_PLATFORM_COMPONENT', False)
-
-    @property
-    def config_schema(self):
-        return getattr(self.module, 'CONFIG_SCHEMA', None)
-
-    @property
-    def is_multi_conf(self):
-        return getattr(self.module, 'MULTI_CONF', False)
-
-    @property
-    def to_code(self):
-        return getattr(self.module, 'to_code', None)
-
-    @property
-    def esp_platforms(self):
-        return getattr(self.module, 'ESP_PLATFORMS', ESP_PLATFORMS)
-
-    @property
-    def dependencies(self):
-        return getattr(self.module, 'DEPENDENCIES', [])
-
-    @property
-    def conflicts_with(self):
-        return getattr(self.module, 'CONFLICTS_WITH', [])
-
-    @property
-    def auto_load(self):
-        return getattr(self.module, 'AUTO_LOAD', [])
-
-    def _get_flags_set(self, name, config):
-        if not hasattr(self.module, name):
-            return set()
-        obj = getattr(self.module, name)
-        if callable(obj):
-            obj = obj(config)
-        if obj is None:
-            return set()
-        if not isinstance(obj, (list, tuple, set)):
-            obj = [obj]
-        return set(obj)
-
-    @property
-    def source_files(self):
-        if self._is_core:
-            core_p = os.path.abspath(os.path.join(os.path.dirname(__file__), 'core'))
-            source_files = core.find_source_files(os.path.join(core_p, 'dummy'))
-            ret = {}
-            for f in source_files:
-                ret[f'esphome/core/{f}'] = os.path.join(core_p, f)
-            return ret
-
-        source_files = core.find_source_files(self.module.__file__)
-        ret = {}
-        # Make paths absolute
-        directory = os.path.abspath(os.path.dirname(self.module.__file__))
-        for x in source_files:
-            full_file = os.path.join(directory, x)
-            rel = os.path.relpath(full_file, self.base_components_path)
-            # Always use / for C++ include names
-            rel = rel.replace(os.sep, '/')
-            target_file = f'esphome/components/{rel}'
-            ret[target_file] = full_file
-        return ret
-
-
-CORE_COMPONENTS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'components'))
-_UNDEF = object()
-CUSTOM_COMPONENTS_PATH = _UNDEF
-
-
-def _mount_config_dir():
-    global CUSTOM_COMPONENTS_PATH
-    if CUSTOM_COMPONENTS_PATH is not _UNDEF:
-        return
-    custom_path = os.path.abspath(os.path.join(CORE.config_dir, 'custom_components'))
-    if not os.path.isdir(custom_path):
-        CUSTOM_COMPONENTS_PATH = None
-        return
-    if CORE.config_dir not in sys.path:
-        sys.path.insert(0, CORE.config_dir)
-    CUSTOM_COMPONENTS_PATH = custom_path
-
-
-def _lookup_module(domain, is_platform):
-    if domain in _COMPONENT_CACHE:
-        return _COMPONENT_CACHE[domain]
-
-    _mount_config_dir()
-    # First look for custom_components
-    try:
-        module = importlib.import_module(f'custom_components.{domain}')
-    except ImportError as e:
-        # ImportError when no such module
-        if 'No module named' not in str(e):
-            _LOGGER.warning("Unable to import custom component %s:", domain, exc_info=True)
-    except Exception:  # pylint: disable=broad-except
-        # Other error means component has an issue
-        _LOGGER.error("Unable to load custom component %s:", domain, exc_info=True)
-        return None
-    else:
-        # Found in custom components
-        manif = ComponentManifest(module, CUSTOM_COMPONENTS_PATH, is_platform=is_platform)
-        _COMPONENT_CACHE[domain] = manif
-        return manif
-
-    try:
-        module = importlib.import_module(f'esphome.components.{domain}')
-    except ImportError as e:
-        if 'No module named' not in str(e):
-            _LOGGER.error("Unable to import component %s:", domain, exc_info=True)
-        return None
-    except Exception:  # pylint: disable=broad-except
-        _LOGGER.error("Unable to load component %s:", domain, exc_info=True)
-        return None
-    else:
-        manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform)
-        _COMPONENT_CACHE[domain] = manif
-        return manif
-
-
-def get_component(domain):
-    assert '.' not in domain
-    return _lookup_module(domain, False)
-
-
-def get_platform(domain, platform):
-    full = f'{platform}.{domain}'
-    return _lookup_module(full, True)
-
-
-_COMPONENT_CACHE['esphome'] = ComponentManifest(
-    core_config, CORE_COMPONENTS_PATH, is_core=True, is_platform=False,
-)
-
 
 def iter_components(config):
     for domain, conf in config.items():
         component = get_component(domain)
-        if component.is_multi_conf:
+        if component.multi_conf:
             for conf_ in conf:
                 yield domain, component, conf_
         else:
             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
 
@@ -194,10 +56,31 @@ ConfigPath = List[Union[str, int]]
 def _path_begins_with(path, other):  # type: (ConfigPath, ConfigPath) -> bool
     if len(path) < len(other):
         return False
-    return path[:len(other)] == other
+    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
@@ -206,6 +89,13 @@ class Config(OrderedDict):
         # The values will be the paths to all "domain", for example (['logger'], 'logger')
         # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic')
         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
@@ -213,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 []
@@ -262,21 +170,35 @@ class Config(OrderedDict):
         doc_range = None
         for item_index in path:
             try:
+                if item_index in data:
+                    doc_range = [x for x in data.keys() if x == item_index][0].esp_range
                 data = data[item_index]
-            except (KeyError, IndexError, TypeError):
+            except (KeyError, IndexError, TypeError, AttributeError):
                 return doc_range
+            if isinstance(data, core.ID):
+                data = data.id
             if isinstance(data, ESPHomeDataBase) and data.esp_range is not None:
                 doc_range = data.esp_range
+            elif isinstance(data, dict):
+                platform_item = data.get("platform")
+                if (
+                    isinstance(platform_item, ESPHomeDataBase)
+                    and platform_item.esp_range is not None
+                ):
+                    doc_range = platform_item.esp_range
 
         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
 
@@ -293,6 +215,21 @@ class Config(OrderedDict):
             part.append(item_index)
         return part
 
+    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 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):
     path = path or []
@@ -309,194 +246,101 @@ 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 = []  # 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:
-                # 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 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:
-            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:
-                    id.id = v[0].id
-                    break
-            else:
-                result.add_str_error(f"Couldn't resolve ID for type '{id.type}'", 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):
         return cv.Schema({cv.valid: recursive_check_replaceme})(value)
     if isinstance(value, ESPForceValue):
         pass
-    if isinstance(value, str) and value == 'REPLACEME':
-        raise cv.Invalid("Found 'REPLACEME' in configuration, this is most likely an error. "
-                         "Please make sure you have replaced all fields from the sample "
-                         "configuration.\n"
-                         "If you want to use the literal REPLACEME string, "
-                         "please use \"!force REPLACEME\"")
+    if isinstance(value, str) and value == "REPLACEME":
+        raise cv.Invalid(
+            "Found 'REPLACEME' in configuration, this is most likely an error. "
+            "Please make sure you have replaced all fields from the sample "
+            "configuration.\n"
+            "If you want to use the literal REPLACEME string, "
+            'please use "!force REPLACEME"'
+        )
     return value
 
 
-def validate_config(config):
-    result = Config()
+class ConfigValidationStep(abc.ABC):
+    """A step to for the validation phase."""
 
-    # 1. Load substitutions
-    if CONF_SUBSTITUTIONS in config:
-        result[CONF_SUBSTITUTIONS] = config[CONF_SUBSTITUTIONS]
-        result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
-        try:
-            substitutions.do_substitution_pass(config)
-        except vol.Invalid as err:
-            result.add_error(err)
-            return result
+    # Priority of this step, higher means run earlier
+    priority: float = 0.0
 
-    # 1.1. Check for REPLACEME special value
-    try:
-        recursive_check_replaceme(config)
-    except vol.Invalid as err:
-        result.add_error(err)
+    @abc.abstractmethod
+    def run(self, result: Config) -> None:
+        ...
 
-    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:'.")
-        config[CONF_ESPHOME] = config.pop('esphomeyaml')
 
-    if CONF_ESPHOME not in config:
-        result.add_str_error("'esphome' section missing from configuration. Please make sure "
-                             "your configuration has an 'esphome:' line in it.", [])
-        return result
+class LoadValidationStep(ConfigValidationStep):
+    """Load step, this step is called once for each domain config fragment.
 
-    # 2. Load partial core config
-    result[CONF_ESPHOME] = config[CONF_ESPHOME]
-    result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
-    try:
-        core_config.preload_core_config(config)
-    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)
+    Responsibilties:
+    - Load component code
+    - Ensure all AUTO_LOADs are added
+    - Set output paths of result
+    """
 
-    # 3. Load components.
-    # Load components (also AUTO_LOAD) and set output paths of result
-    # Queue of items to load, FIFO
-    load_queue = collections.deque()
-    for domain, conf in config.items():
-        load_queue.append((domain, conf))
+    def __init__(self, domain: str, conf: ConfigType):
+        self.domain = domain
+        self.conf = conf
 
-    # 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()
-        domain = str(domain)
-        if domain.startswith('.'):
+    def run(self, result: Config) -> None:
+        if self.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]
+            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: {domain}", path)
-            continue
-        CORE.loaded_integrations.add(domain)
+            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 config:
-                load_conf = core.AutoLoad()
-                config[load] = load_conf
-                load_queue.append((load, load_conf))
+            if load not in result:
+                result.add_validation_step(AutoLoadValidationStep(load))
 
         if not component.is_platform_component:
-            check_queue.append(([domain], domain, conf, component))
-            continue
+            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([domain], domain)
+        result.remove_output_path([self.domain], self.domain)
 
         # Ensure conf is a list
-        if not conf:
-            result[domain] = conf = []
-        elif not isinstance(conf, list):
-            result[domain] = conf = [conf]
+        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(conf):
-            path = [domain, i]
+        for i, p_config in enumerate(self.conf):
+            path = [self.domain, i]
             # Construct temporary unknown output path
-            p_domain = f'{domain}.unknown'
+            p_domain = f"{self.domain}.unknown"
             result.add_output_path(path, p_domain)
-            result[domain][i] = p_config
+            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')
+            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}'
+            p_domain = f"{self.domain}.{p_name}"
             result.add_output_path(path, p_domain)
             # Try Load platform
-            platform = get_platform(domain, p_name)
+            platform = get_platform(self.domain, p_name)
             if platform is None:
                 result.add_str_error(f"Platform not found: '{p_domain}'", path)
                 continue
@@ -504,115 +348,420 @@ def validate_config(config):
 
             # 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))
+                if load not in result:
+                    result.add_validation_step(AutoLoadValidationStep(load))
 
-            check_queue.append((path, p_domain, p_config, platform))
+            result.add_validation_step(
+                MetadataValidationStep(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 = {}
+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 comp.dependencies:
-            if dependency not in config:
-                result.add_str_error("Component {} requires component {}"
-                                     "".format(domain, dependency), path)
+        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:
-            continue
+            return
 
         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)
+        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:
-            continue
+            return
 
-        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 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 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 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
 
-        if comp.is_multi_conf:
-            if not isinstance(conf, list):
-                result[domain] = conf = [conf]
-            for i, part_conf in enumerate(conf):
-                validate_queue.append((path + [i], part_conf, comp))
-            continue
+        result.add_validation_step(
+            SchemaValidationStep(self.domain, self.path, self.conf, self.comp)
+        )
 
-        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:
+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(conf)
-                platform_val = input_conf.pop('platform')
-                validated = comp.config_schema(input_conf)
+                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(path, validated)
+                validated["platform"] = platform_val
+                validated.move_to_end("platform", last=False)
+                result.set_by_path(self.path, validated)
             else:
-                validated = comp.config_schema(conf)
-                result.set_by_path(path, validated)
+                schema = cv.Schema(self.comp.config_schema)
+                validated = schema(self.conf)
+                result.set_by_path(self.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)
-    return result
+        result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
 
 
-def _nested_getitem(data, path):
-    for item_index in path:
+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()
+
+    loader.clear_component_meta_finders()
+    loader.install_custom_components_meta_finder()
+
+    # 0. Load packages
+    if CONF_PACKAGES in config:
+        from esphome.components.packages import do_packages_pass
+
+        result.add_output_path([CONF_PACKAGES], CONF_PACKAGES)
         try:
-            data = data[item_index]
-        except (KeyError, IndexError, TypeError):
-            return None
-    return data
+            config = do_packages_pass(config)
+        except vol.Invalid as err:
+            result.update(config)
+            result.add_error(err)
+            return result
+
+    CORE.raw_config = config
+
+    # 1. Load substitutions
+    if CONF_SUBSTITUTIONS in config:
+        from esphome.components import substitutions
+
+        result[CONF_SUBSTITUTIONS] = {
+            **config[CONF_SUBSTITUTIONS],
+            **command_line_substitutions,
+        }
+        result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
+        try:
+            substitutions.do_substitution_pass(config, command_line_substitutions)
+        except vol.Invalid as err:
+            result.add_error(err)
+            return result
+
+    CORE.raw_config = config
+
+    # 1.1. Check for REPLACEME special value
+    try:
+        recursive_check_replaceme(config)
+    except vol.Invalid as err:
+        result.add_error(err)
+
+    # 1.2. Load external_components
+    if CONF_EXTERNAL_COMPONENTS in config:
+        from esphome.components.external_components import do_external_components_pass
+
+        result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS)
+        try:
+            do_external_components_pass(config)
+        except vol.Invalid as err:
+            result.update(config)
+            result.add_error(err)
+            return result
+
+    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:'."
+        )
+        config[CONF_ESPHOME] = config.pop("esphomeyaml")
+
+    if CONF_ESPHOME not in config:
+        result.add_str_error(
+            "'esphome' section missing from configuration. Please make sure "
+            "your configuration has an 'esphome:' line in it.",
+            [],
+        )
+        return result
+
+    # 2. Load partial core config
+    result[CONF_ESPHOME] = config[CONF_ESPHOME]
+    result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
+    try:
+        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)
+
+    # 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():
+        result.add_validation_step(LoadValidationStep(domain, conf))
+    result.add_validation_step(IDPassValidationStep())
+
+    result.run_validation_steps()
+
+    return result
 
 
 def humanize_error(config, validation_error):
     validation_error = str(validation_error)
-    m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error, re.DOTALL)
+    m = re.match(
+        r"^(.*?)\s*(?:for dictionary value )?@ data\[.*$", validation_error, re.DOTALL
+    )
     if m is not None:
         validation_error = m.group(1)
     validation_error = validation_error.strip()
-    if not validation_error.endswith('.'):
-        validation_error += '.'
+    if not validation_error.endswith("."):
+        validation_error += "."
     return validation_error
 
 
 def _get_parent_name(path, config):
     if not path:
-        return ''
+        return ""
     for domain_path, domain in config.output_paths:
         if _path_begins_with(path, domain_path):
             if len(path) > len(domain_path):
@@ -624,21 +773,23 @@ def _get_parent_name(path, config):
 
 def _format_vol_invalid(ex, config):
     # type: (vol.Invalid, Config) -> str
-    message = ''
+    message = ""
 
     paren = _get_parent_name(ex.path[:-1], 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)
-    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}]. Please check the indentation."
+    elif "extra keys not allowed" in str(ex):
+        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)
 
@@ -656,15 +807,14 @@ class InvalidYAMLError(EsphomeError):
         self.base_exc = base_exc
 
 
-def _load_config():
+def _load_config(command_line_substitutions):
     try:
         config = yaml_util.load_yaml(CORE.config_path)
     except EsphomeError as e:
-        raise InvalidYAMLError(e)
-    CORE.raw_config = config
+        raise InvalidYAMLError(e) from e
 
     try:
-        result = validate_config(config)
+        result = validate_config(config, command_line_substitutions)
     except EsphomeError:
         raise
     except Exception:
@@ -674,22 +824,23 @@ def _load_config():
     return result
 
 
-def load_config():
+def load_config(command_line_substitutions):
     try:
-        return _load_config()
+        return _load_config(command_line_substitutions)
     except vol.Invalid as err:
-        raise EsphomeError(f"Error while parsing config: {err}")
+        raise EsphomeError(f"Error while parsing config: {err}") from err
 
 
-def line_info(obj, highlight=True):
+def line_info(config, path, highlight=True):
     """Display line config source."""
     if not highlight:
         return None
-    if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None:
-        mark = obj.esp_range.start_mark
-        source = "[source {}:{}]".format(mark.document, mark.line + 1)
-        return color('cyan', source)
-    return None
+    obj = config.get_deepest_document_range_for_path(path)
+    if obj:
+        mark = obj.start_mark
+        source = f"[source {mark.document}:{mark.line + 1}]"
+        return color(Fore.CYAN, source)
+    return "None"
 
 
 def _print_on_next_line(obj):
@@ -705,90 +856,90 @@ def _print_on_next_line(obj):
 def dump_dict(config, path, at_root=True):
     # type: (Config, ConfigPath, bool) -> Tuple[str, bool]
     conf = config.get_nested_item(path)
-    ret = ''
+    ret = ""
     multiline = False
 
     if at_root:
         error = config.get_error_for_path(path)
         if error is not None:
-            ret += '\n' + color('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
         if not conf:
-            ret += '[]'
+            ret += "[]"
             multiline = False
 
         for i in range(len(conf)):
             path_ = path + [i]
             error = config.get_error_for_path(path_)
             if error is not None:
-                ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n'
+                ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
 
-            sep = '- '
+            sep = "- "
             if config.is_in_error_path(path_):
-                sep = color('red', sep)
+                sep = color(Fore.RED, sep)
             msg, _ = dump_dict(config, path_, at_root=False)
             msg = indent(msg)
-            inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_))
+            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:
-            ret += '{}'
+            ret += "{}"
             multiline = False
 
         for k in conf.keys():
             path_ = path + [k]
             error = config.get_error_for_path(path_)
             if error is not None:
-                ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n'
+                ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
 
-            st = f'{k}: '
+            st = f"{k}: "
             if config.is_in_error_path(path_):
-                st = color('red', st)
+                st = color(Fore.RED, st)
             msg, m = dump_dict(config, path_, at_root=False)
 
-            inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_))
+            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 = 'bold_red' if error else 'white'
+        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 = 'bold_red' if error else 'white'
+        col = Fore.BOLD_RED if error else Fore.KEEP
         ret += color(col, conf)
     elif conf is None:
         pass
     else:
         error = config.get_error_for_path(path)
-        col = 'bold_red' if error else 'white'
+        col = Fore.BOLD_RED if error else Fore.KEEP
         ret += color(col, str(conf))
-        multiline = '\n' in ret
+        multiline = "\n" in ret
 
     return ret, multiline
 
@@ -798,7 +949,9 @@ def strip_default_ids(config):
         to_remove = []
         for i, x in enumerate(config):
             x = config[i] = strip_default_ids(x)
-            if (isinstance(x, core.ID) and not x.is_manual) or isinstance(x, core.AutoLoad):
+            if (isinstance(x, core.ID) and not x.is_manual) or isinstance(
+                x, core.AutoLoad
+            ):
                 to_remove.append(x)
         for x in to_remove:
             config.remove(x)
@@ -806,17 +959,19 @@ def strip_default_ids(config):
         to_remove = []
         for k, v in config.items():
             v = config[k] = strip_default_ids(v)
-            if (isinstance(v, core.ID) and not v.is_manual) or isinstance(v, core.AutoLoad):
+            if (isinstance(v, core.ID) and not v.is_manual) or isinstance(
+                v, core.AutoLoad
+            ):
                 to_remove.append(k)
         for k in to_remove:
             config.pop(k)
     return config
 
 
-def read_config():
+def read_config(command_line_substitutions):
     _LOGGER.info("Reading configuration %s...", CORE.config_path)
     try:
-        res = load_config()
+        res = load_config(command_line_substitutions)
     except EsphomeError as err:
         _LOGGER.error("Error while reading config: %s", err)
         return None
@@ -824,14 +979,17 @@ def read_config():
         if not CORE.verbose:
             res = strip_default_ids(res)
 
-        safe_print(color('bold_red', "Failed config"))
-        safe_print('')
+        safe_print(color(Fore.BOLD_RED, "Failed config"))
+        safe_print("")
         for path, domain in res.output_paths:
             if not res.is_in_error_path(path):
                 continue
 
-            safe_print(color('bold_red', f'{domain}:') + ' ' +
-                       (line_info(res.get_nested_item(path)) or ''))
+            errstr = color(Fore.BOLD_RED, f"{domain}:")
+            errline = line_info(res, path)
+            if errline:
+                errstr += f" {errline}"
+            safe_print(errstr)
             safe_print(indent(dump_dict(res, path)[0]))
         return None
     return OrderedDict(res)
diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py
index dcbcb70efe..a88c5983b5 100644
--- a/esphome/config_helpers.py
+++ b/esphome/config_helpers.py
@@ -7,14 +7,19 @@ from esphome.helpers import read_file
 
 def read_config_file(path):
     # type: (str) -> str
-    if CORE.vscode and (not CORE.ace or
-                        os.path.abspath(path) == os.path.abspath(CORE.config_path)):
-        print(json.dumps({
-            'type': 'read_file',
-            'path': path,
-        }))
+    if CORE.vscode and (
+        not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
+    ):
+        print(
+            json.dumps(
+                {
+                    "type": "read_file",
+                    "path": path,
+                }
+            )
+        )
         data = json.loads(input())
-        assert data['type'] == 'file_response'
-        return data['content']
+        assert data["type"] == "file_response"
+        return data["content"]
 
     return read_file(path)
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index 55199e6647..8df74ba861 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
@@ -11,14 +12,59 @@ from string import ascii_letters, digits
 import voluptuous as vol
 
 from esphome import core
-from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \
-    CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \
-    CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \
-    CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE
-from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \
-    TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes
+import esphome.codegen as cg
+from esphome.const import (
+    ALLOWED_NAME_CHARS,
+    CONF_AVAILABILITY,
+    CONF_COMMAND_TOPIC,
+    CONF_DISABLED_BY_DEFAULT,
+    CONF_DISCOVERY,
+    CONF_ENTITY_CATEGORY,
+    CONF_ICON,
+    CONF_ID,
+    CONF_INTERNAL,
+    CONF_NAME,
+    CONF_PAYLOAD_AVAILABLE,
+    CONF_PAYLOAD_NOT_AVAILABLE,
+    CONF_RETAIN,
+    CONF_SETUP_PRIORITY,
+    CONF_STATE_TOPIC,
+    CONF_TOPIC,
+    CONF_HOUR,
+    CONF_MINUTE,
+    CONF_SECOND,
+    CONF_VALUE,
+    CONF_UPDATE_INTERVAL,
+    CONF_TYPE_ID,
+    CONF_TYPE,
+    ENTITY_CATEGORY_CONFIG,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    ENTITY_CATEGORY_NONE,
+    KEY_CORE,
+    KEY_FRAMEWORK_VERSION,
+    KEY_TARGET_FRAMEWORK,
+)
+from esphome.core import (
+    CORE,
+    HexInt,
+    IPAddress,
+    Lambda,
+    TimePeriod,
+    TimePeriodMicroseconds,
+    TimePeriodMilliseconds,
+    TimePeriodSeconds,
+    TimePeriodMinutes,
+)
 from esphome.helpers import list_starts_with, add_class_to_obj
+from esphome.jsonschema import (
+    jschema_composite,
+    jschema_extractor,
+    jschema_registry,
+    jschema_typed,
+)
+
 from esphome.voluptuous_schema import _Schema
+from esphome.yaml_util import make_data_base
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -39,26 +85,121 @@ Inclusive = vol.Inclusive
 ALLOW_EXTRA = vol.ALLOW_EXTRA
 UNDEFINED = vol.UNDEFINED
 RequiredFieldInvalid = vol.RequiredFieldInvalid
-
-ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
+# 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
-    'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break',
-    'case', 'catch', 'char', 'char16_t', 'char32_t', 'class', 'compl', 'concept', 'const',
-    'constexpr', 'const_cast', 'continue', 'decltype', 'default', 'delete', 'do', 'double',
-    'dynamic_cast', 'else', 'enum', 'explicit', 'export', 'export', 'extern', 'false', 'float',
-    'for', 'friend', 'goto', 'if', 'inline', 'int', 'long', 'mutable', 'namespace', 'new',
-    'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private', 'protected',
-    'public', 'register', 'reinterpret_cast', 'requires', 'return', 'short', 'signed', 'sizeof',
-    'static', 'static_assert', 'static_cast', 'struct', 'switch', 'template', 'this',
-    'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned',
-    'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq',
-
-    'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT',
-    'OUTPUT',
-    'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t',
-    'close', 'pause', 'sleep', 'open', 'setup', 'loop',
+    "alignas",
+    "alignof",
+    "and",
+    "and_eq",
+    "asm",
+    "auto",
+    "bitand",
+    "bitor",
+    "bool",
+    "break",
+    "case",
+    "catch",
+    "char",
+    "char16_t",
+    "char32_t",
+    "class",
+    "compl",
+    "concept",
+    "const",
+    "constexpr",
+    "const_cast",
+    "continue",
+    "decltype",
+    "default",
+    "delete",
+    "do",
+    "double",
+    "dynamic_cast",
+    "else",
+    "enum",
+    "explicit",
+    "export",
+    "export",
+    "extern",
+    "false",
+    "float",
+    "for",
+    "friend",
+    "goto",
+    "if",
+    "inline",
+    "int",
+    "long",
+    "mutable",
+    "namespace",
+    "new",
+    "noexcept",
+    "not",
+    "not_eq",
+    "nullptr",
+    "operator",
+    "or",
+    "or_eq",
+    "private",
+    "protected",
+    "public",
+    "register",
+    "reinterpret_cast",
+    "requires",
+    "return",
+    "short",
+    "signed",
+    "sizeof",
+    "static",
+    "static_assert",
+    "static_cast",
+    "struct",
+    "switch",
+    "template",
+    "this",
+    "thread_local",
+    "throw",
+    "true",
+    "try",
+    "typedef",
+    "typeid",
+    "typename",
+    "union",
+    "unsigned",
+    "using",
+    "virtual",
+    "void",
+    "volatile",
+    "wchar_t",
+    "while",
+    "xor",
+    "xor_eq",
+    "App",
+    "pinMode",
+    "delay",
+    "delayMicroseconds",
+    "digitalRead",
+    "digitalWrite",
+    "INPUT",
+    "OUTPUT",
+    "uint8_t",
+    "uint16_t",
+    "uint32_t",
+    "uint64_t",
+    "int8_t",
+    "int16_t",
+    "int32_t",
+    "int64_t",
+    "close",
+    "pause",
+    "sleep",
+    "open",
+    "setup",
+    "loop",
 ]
 
 
@@ -90,8 +231,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):
@@ -104,7 +245,7 @@ def alphanumeric(value):
         raise Invalid("string value is None")
     value = str(value)
     if not value.isalnum():
-        raise Invalid("string value is not alphanumeric")
+        raise Invalid(f"{value} is not alphanumeric")
     return value
 
 
@@ -112,8 +253,10 @@ def valid_name(value):
     value = string_strict(value)
     for c in value:
         if c not in ALLOWED_NAME_CHARS:
-            raise Invalid(f"'{c}' is an invalid character for names. Valid characters are: "
-                          f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)")
+            raise Invalid(
+                f"'{c}' is an invalid character for names. Valid characters are: "
+                f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)"
+            )
     return value
 
 
@@ -127,7 +270,9 @@ def string(value):
     if isinstance(value, (dict, list)):
         raise Invalid("string value cannot be dictionary or list.")
     if isinstance(value, bool):
-        raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.")
+        raise Invalid(
+            "Auto-converted this value to boolean, please wrap the value in quotes."
+        )
     if isinstance(value, str):
         return value
     if value is not None:
@@ -141,8 +286,9 @@ def string_strict(value):
     check_not_templatable(value)
     if isinstance(value, str):
         return value
-    raise Invalid("Must be string, got {}. did you forget putting quotes "
-                  "around the value?".format(type(value)))
+    raise Invalid(
+        f"Must be string, got {type(value)}. did you forget putting quotes around the value?"
+    )
 
 
 def icon(value):
@@ -150,9 +296,11 @@ def icon(value):
     value = string_strict(value)
     if not value:
         return value
-    if value.startswith('mdi:'):
+    if re.match("^[\\w\\-]+:[\\w\\-]+$", value):
         return value
-    raise Invalid('Icons should start with prefix "mdi:"')
+    raise Invalid(
+        'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"'
+    )
 
 
 def boolean(value):
@@ -169,14 +317,16 @@ def boolean(value):
         return value
     if isinstance(value, str):
         value = value.lower()
-        if value in ('true', 'yes', 'on', 'enable'):
+        if value in ("true", "yes", "on", "enable"):
             return True
-        if value in ('false', 'no', 'off', 'disable'):
+        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))
+    raise Invalid(
+        f"Expected boolean value, but cannot convert {value} to a boolean. Please use 'true' or 'false'"
+    )
 
 
+@jschema_composite
 def ensure_list(*validators):
     """Validate this configuration option to be a list.
 
@@ -186,6 +336,7 @@ def ensure_list(*validators):
     None and empty dictionaries are converted to empty lists.
     """
     user = All(*validators)
+    list_schema = Schema([user])
 
     def validator(value):
         check_not_templatable(value)
@@ -193,19 +344,7 @@ def ensure_list(*validators):
             return []
         if not isinstance(value, list):
             return [user(value)]
-        ret = []
-        errs = []
-        for i, val in enumerate(value):
-            try:
-                with prepend_path([i]):
-                    ret.append(user(val))
-            except MultipleInvalid as err:
-                errs.extend(err.errors)
-            except Invalid as err:
-                errs.append(err)
-        if errs:
-            raise MultipleInvalid(errs)
-        return ret
+        return list_schema(value)
 
     return validator
 
@@ -228,15 +367,17 @@ def int_(value):
     if isinstance(value, float):
         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))
+        raise Invalid(
+            f"This option only accepts integers with no fractional part. Please remove the fractional part from {value}"
+        )
     value = string_strict(value).lower()
     base = 10
-    if value.startswith('0x'):
+    if value.startswith("0x"):
         base = 16
     try:
         return int(value, base)
     except ValueError:
+        # pylint: disable=raise-missing-from
         raise Invalid(f"Expected integer, but cannot parse {value} as an integer")
 
 
@@ -246,13 +387,18 @@ def int_range(min=None, max=None, min_included=True, max_included=True):
         assert isinstance(min, int)
     if max is not None:
         assert isinstance(max, int)
-    return All(int_, Range(min=min, max=max, min_included=min_included, max_included=max_included))
+    return All(
+        int_,
+        Range(min=min, max=max, min_included=min_included, max_included=max_included),
+    )
 
 
 def hex_int_range(min=None, max=None, min_included=True, max_included=True):
     """Validate that the config option is an integer in the given range."""
-    return All(hex_int,
-               Range(min=min, max=max, min_included=min_included, max_included=max_included))
+    return All(
+        hex_int,
+        Range(min=min, max=max, min_included=min_included, max_included=max_included),
+    )
 
 
 def float_range(min=None, max=None, min_included=True, max_included=True):
@@ -261,8 +407,10 @@ def float_range(min=None, max=None, min_included=True, max_included=True):
         assert isinstance(min, (int, float))
     if max is not None:
         assert isinstance(max, (int, float))
-    return All(float_, Range(min=min, max=max, min_included=min_included,
-                             max_included=max_included))
+    return All(
+        float_,
+        Range(min=min, max=max, min_included=min_included, max_included=max_included),
+    )
 
 
 port = int_range(min=1, max=65535)
@@ -281,29 +429,37 @@ def validate_id_name(value):
         raise Invalid("ID must not be empty")
     if value[0].isdigit():
         raise Invalid("First character in ID cannot be a digit.")
-    if '-' in value:
-        raise Invalid("Dashes are not supported in IDs, please use underscores instead.")
-    valid_chars = ascii_letters + digits + '_'
+    if "-" in value:
+        raise Invalid(
+            "Dashes are not supported in IDs, please use underscores instead."
+        )
+    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))
+            raise Invalid(
+                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))
+        raise Invalid(
+            f"ID '{value}' conflicts with the name of an esphome integration, please use another ID name."
+        )
     return value
 
 
 def use_id(type):
     """Declare that this configuration option should point to an ID with the given type."""
+
     def validator(value):
         check_not_templatable(value)
         if value is None:
             return core.ID(None, is_declaration=False, type=type)
-        if isinstance(value, core.ID) and value.is_declaration is False and value.type is type:
+        if (
+            isinstance(value, core.ID)
+            and value.is_declaration is False
+            and value.type is type
+        ):
             return value
 
         return core.ID(validate_id_name(value), is_declaration=False, type=type)
@@ -317,6 +473,7 @@ def declare_id(type):
 
     If two IDs with the same name exist, a validation error is thrown.
     """
+
     def validator(value):
         check_not_templatable(value)
         if value is None:
@@ -347,20 +504,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:
@@ -371,10 +545,10 @@ def has_at_least_one_key(*keys):
     def validate(obj):
         """Test keys exist in dict."""
         if not isinstance(obj, dict):
-            raise Invalid('expected dictionary')
+            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
@@ -382,15 +556,16 @@ def has_at_least_one_key(*keys):
 
 def has_exactly_one_key(*keys):
     """Validate that exactly one of the given keys exist in the config."""
+
     def validate(obj):
         if not isinstance(obj, dict):
-            raise Invalid('expected dictionary')
+            raise Invalid("expected dictionary")
 
         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
@@ -398,44 +573,67 @@ def has_exactly_one_key(*keys):
 
 def has_at_most_one_key(*keys):
     """Validate that at most one of the given keys exist in the config."""
+
     def validate(obj):
         if not isinstance(obj, dict):
-            raise Invalid('expected dictionary')
+            raise Invalid("expected dictionary")
 
         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
 
 
-TIME_PERIOD_ERROR = "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h"
+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
+
+
+TIME_PERIOD_ERROR = (
+    "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h"
+)
 
 time_period_dict = All(
-    Schema({
-        Optional('days'): float_,
-        Optional('hours'): float_,
-        Optional('minutes'): float_,
-        Optional('seconds'): float_,
-        Optional('milliseconds'): float_,
-        Optional('microseconds'): float_,
-    }),
-    has_at_least_one_key('days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds'),
-    lambda value: TimePeriod(**value)
+    Schema(
+        {
+            Optional("days"): float_,
+            Optional("hours"): float_,
+            Optional("minutes"): float_,
+            Optional("seconds"): float_,
+            Optional("milliseconds"): float_,
+            Optional("microseconds"): float_,
+        }
+    ),
+    has_at_least_one_key(
+        "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"
+    ),
+    lambda value: TimePeriod(**value),
 )
 
 
 def time_period_str_colon(value):
     """Validate and transform time offset with format HH:MM[:SS]."""
     if isinstance(value, int):
-        raise Invalid('Make sure you wrap time values in quotes')
+        raise Invalid("Make sure you wrap time values in quotes")
     if not isinstance(value, str):
         raise Invalid(TIME_PERIOD_ERROR.format(value))
 
     try:
-        parsed = [int(x) for x in value.split(':')]
+        parsed = [int(x) for x in value.split(":")]
     except ValueError:
+        # pylint: disable=raise-missing-from
         raise Invalid(TIME_PERIOD_ERROR.format(value))
 
     if len(parsed) == 2:
@@ -454,34 +652,34 @@ def time_period_str_unit(value):
     check_not_templatable(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))
+        raise Invalid(
+            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)
     if not isinstance(value, str):
         raise Invalid("Expected string for time period with unit.")
 
     unit_to_kwarg = {
-        'us': 'microseconds',
-        'microseconds': 'microseconds',
-        'ms': 'milliseconds',
-        'milliseconds': 'milliseconds',
-        's': 'seconds',
-        'sec': 'seconds',
-        'seconds': 'seconds',
-        'min': 'minutes',
-        'minutes': 'minutes',
-        'h': 'hours',
-        'hours': 'hours',
-        'd': 'days',
-        'days': 'days',
+        "us": "microseconds",
+        "microseconds": "microseconds",
+        "ms": "milliseconds",
+        "milliseconds": "milliseconds",
+        "s": "seconds",
+        "sec": "seconds",
+        "seconds": "seconds",
+        "min": "minutes",
+        "minutes": "minutes",
+        "h": "hours",
+        "hours": "hours",
+        "d": "days",
+        "days": "days",
     }
 
     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))})
@@ -516,30 +714,36 @@ def time_period_in_minutes_(value):
 
 
 def update_interval(value):
-    if value == 'never':
+    if value == "never":
         return 4294967295  # uint32_t max
     return positive_time_period_milliseconds(value)
 
 
 time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
 positive_time_period = All(time_period, Range(min=TimePeriod()))
-positive_time_period_milliseconds = All(positive_time_period, time_period_in_milliseconds_)
+positive_time_period_milliseconds = All(
+    positive_time_period, time_period_in_milliseconds_
+)
 positive_time_period_seconds = All(positive_time_period, time_period_in_seconds_)
 positive_time_period_minutes = All(positive_time_period, time_period_in_minutes_)
 time_period_microseconds = All(time_period, time_period_in_microseconds_)
-positive_time_period_microseconds = All(positive_time_period, time_period_in_microseconds_)
-positive_not_null_time_period = All(time_period,
-                                    Range(min=TimePeriod(), min_included=False))
+positive_time_period_microseconds = All(
+    positive_time_period, time_period_in_microseconds_
+)
+positive_not_null_time_period = All(
+    time_period, Range(min=TimePeriod(), min_included=False)
+)
 
 
 def time_of_day(value):
     value = string(value)
     try:
-        date = datetime.strptime(value, '%H:%M:%S')
+        date = datetime.strptime(value, "%H:%M:%S")
     except ValueError as err:
         try:
-            date = datetime.strptime(value, '%H:%M:%S %p')
+            date = datetime.strptime(value, "%H:%M:%S %p")
         except ValueError:
+            # pylint: disable=raise-missing-from
             raise Invalid(f"Invalid time of day: {err}")
 
     return {
@@ -551,7 +755,7 @@ def time_of_day(value):
 
 def mac_address(value):
     value = string_strict(value)
-    parts = value.split(':')
+    parts = value.split(":")
     if len(parts) != 6:
         raise Invalid("MAC Address must consist of 6 : (colon) separated parts")
     parts_int = []
@@ -561,24 +765,59 @@ def mac_address(value):
         try:
             parts_int.append(int(part, 16))
         except ValueError:
+            # pylint: disable=raise-missing-from
             raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF")
 
     return core.MACAddress(*parts_int)
 
 
+def bind_key(value):
+    value = string_strict(value)
+    parts = [value[i : i + 2] for i in range(0, len(value), 2)]
+    if len(parts) != 16:
+        raise Invalid("Bind key must consist of 16 hexadecimal numbers")
+    parts_int = []
+    if any(len(part) != 2 for part in parts):
+        raise Invalid("Bind key must be format XX")
+    for part in parts:
+        try:
+            parts_int.append(int(part, 16))
+        except ValueError:
+            # pylint: disable=raise-missing-from
+            raise Invalid("Bind key must be hex values from 00 to FF")
+
+    return "".join(f"{part:02X}" for part in parts_int)
+
+
 def uuid(value):
     return Coerce(uuid_.UUID)(value)
 
 
 METRIC_SUFFIXES = {
-    'E': 1e18, 'P': 1e15, 'T': 1e12, 'G': 1e9, 'M': 1e6, 'k': 1e3, 'da': 10, 'd': 1e-1,
-    'c': 1e-2, 'm': 0.001, 'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18,
-    '': 1
+    "E": 1e18,
+    "P": 1e15,
+    "T": 1e12,
+    "G": 1e9,
+    "M": 1e6,
+    "k": 1e3,
+    "da": 10,
+    "d": 1e-1,
+    "c": 1e-2,
+    "m": 0.001,
+    "µ": 1e-6,
+    "u": 1e-6,
+    "n": 1e-9,
+    "p": 1e-12,
+    "f": 1e-15,
+    "a": 1e-18,
+    "": 1,
 }
 
 
 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)
+    pattern = re.compile(
+        f"^([-+]?[0-9]*\\.?[0-9]*)\\s*(\\w*?){regex_suffix}$", re.UNICODE
+    )
 
     def validator(value):
         if optional_unit:
@@ -593,7 +832,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
@@ -612,13 +851,15 @@ _temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?")
 _temperature_k = float_with_unit("temperature", "(° K|° K|K)?")
 _temperature_f = float_with_unit("temperature", "(°F|° F|F)?")
 decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True)
+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)
@@ -632,11 +873,11 @@ def temperature(value):
     except Invalid:
         pass
 
-    raise orig_err  # noqa
+    raise err
 
 
-_color_temperature_mireds = float_with_unit('Color Temperature', r'(mireds|Mireds)')
-_color_temperature_kelvin = float_with_unit('Color Temperature', r'(K|Kelvin)')
+_color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)")
+_color_temperature_kelvin = float_with_unit("Color Temperature", r"(K|Kelvin)")
 
 
 def color_temperature(value):
@@ -658,22 +899,20 @@ 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)))
+        raise Invalid(
+            f"Only suffixes with positive exponents are supported. Got {match.group(2)}"
+        )
     return int(mantissa * multiplier)
 
 
 def hostname(value):
     value = string(value)
-    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 -")
-    return value
+    if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None:
+        return value
+    raise Invalid(f"Invalid hostname: {value}")
 
 
 def domain(value):
@@ -682,21 +921,23 @@ def domain(value):
         return value
     try:
         return str(ipv4(value))
-    except Invalid:
-        raise Invalid(f"Invalid domain: {value}")
+    except Invalid as err:
+        raise Invalid(f"Invalid domain: {value}") from err
 
 
 def domain_name(value):
     value = string_strict(value)
     if not value:
         return value
-    if not value.startswith('.'):
+    if not value.startswith("."):
         raise Invalid("Domain name must start with .")
-    if value.startswith('..'):
+    if value.startswith(".."):
         raise Invalid("Domain name must start with single .")
     for c in value:
-        if not (c.isalnum() or c in '._-'):
-            raise Invalid("Domain name can only have alphanumeric characters and _ or -")
+        if not (c.isalnum() or c in "._-"):
+            raise Invalid(
+                "Domain name can only have alphanumeric characters and _ or -"
+            )
     return value
 
 
@@ -713,15 +954,13 @@ def ipv4(value):
     if isinstance(value, list):
         parts = value
     elif isinstance(value, str):
-        parts = value.split('.')
+        parts = value.split(".")
     elif isinstance(value, IPAddress):
         return value
     else:
-        raise Invalid("IPv4 address must consist of either string or "
-                      "integer list")
+        raise Invalid("IPv4 address must consist of either string or " "integer list")
     if len(parts) != 4:
-        raise Invalid("IPv4 address must consist of four point-separated "
-                      "integers")
+        raise Invalid("IPv4 address must consist of four point-separated " "integers")
     parts_ = list(map(int, parts))
     if not all(0 <= x < 256 for x in parts_):
         raise Invalid("IPv4 address parts must be in range from 0 to 255")
@@ -734,38 +973,43 @@ def _valid_topic(value):
         raise Invalid("Can't use dictionary with topic")
     value = string(value)
     try:
-        raw_value = value.encode('utf-8')
-    except UnicodeError:
-        raise Invalid("MQTT topic name/filter must be valid UTF-8 string.")
+        raw_value = value.encode("utf-8")
+    except UnicodeError as err:
+        raise Invalid("MQTT topic name/filter must be valid UTF-8 string.") from err
     if not raw_value:
         raise Invalid("MQTT topic name/filter must not be empty.")
     if len(raw_value) > 65535:
-        raise Invalid("MQTT topic name/filter must not be longer than "
-                      "65535 encoded bytes.")
-    if '\0' in value:
-        raise Invalid("MQTT topic name/filter must not contain null "
-                      "character.")
+        raise Invalid(
+            "MQTT topic name/filter must not be longer than " "65535 encoded bytes."
+        )
+    if "\0" in value:
+        raise Invalid("MQTT topic name/filter must not contain null " "character.")
     return value
 
 
 def subscribe_topic(value):
     """Validate that we can subscribe using this MQTT topic."""
     value = _valid_topic(value)
-    for i in (i for i, c in enumerate(value) if c == '+'):
-        if (i > 0 and value[i - 1] != '/') or \
-                (i < len(value) - 1 and value[i + 1] != '/'):
-            raise Invalid("Single-level wildcard must occupy an entire "
-                          "level of the filter")
+    for i in (i for i, c in enumerate(value) if c == "+"):
+        if (i > 0 and value[i - 1] != "/") or (
+            i < len(value) - 1 and value[i + 1] != "/"
+        ):
+            raise Invalid(
+                "Single-level wildcard must occupy an entire " "level of the filter"
+            )
 
-    index = value.find('#')
+    index = value.find("#")
     if index != -1:
         if index != len(value) - 1:
             # If there are multiple wildcards, this will also trigger
-            raise Invalid("Multi-level wildcard must be the last "
-                          "character in the topic filter.")
-        if len(value) > 1 and value[index - 1] != '/':
-            raise Invalid("Multi-level wildcard must be after a topic "
-                          "level separator.")
+            raise Invalid(
+                "Multi-level wildcard must be the last "
+                "character in the topic filter."
+            )
+        if len(value) > 1 and value[index - 1] != "/":
+            raise Invalid(
+                "Multi-level wildcard must be after a topic " "level separator."
+            )
 
     return value
 
@@ -773,14 +1017,14 @@ def subscribe_topic(value):
 def publish_topic(value):
     """Validate that we can publish using this MQTT topic."""
     value = _valid_topic(value)
-    if '+' in value or '#' in value:
+    if "+" in value or "#" in value:
         raise Invalid("Wildcards can not be used in topic names")
     return value
 
 
 def mqtt_payload(value):
     if value is None:
-        return ''
+        return ""
     return string(value)
 
 
@@ -788,14 +1032,17 @@ def mqtt_qos(value):
     try:
         value = int(value)
     except (TypeError, ValueError):
+        # pylint: disable=raise-missing-from
         raise Invalid(f"MQTT Quality of Service must be integer, got {value}")
     return one_of(0, 1, 2)(value)
 
 
 def requires_component(comp):
     """Validate that this option can only be specified when the component `comp` is loaded."""
+    # pylint: disable=unsupported-membership-test
     def validator(value):
-        if comp not in CORE.raw_config:
+        # pylint: disable=unsupported-membership-test
+        if comp not in CORE.loaded_integrations:
             raise Invalid(f"This option requires component {comp}")
         return value
 
@@ -805,9 +1052,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
 
 
@@ -821,9 +1070,17 @@ def percentage(value):
 
 
 def possibly_negative_percentage(value):
-    has_percent_sign = isinstance(value, str) and value.endswith('%')
-    if has_percent_sign:
-        value = float(value[:-1].rstrip()) / 100.0
+    has_percent_sign = False
+    if isinstance(value, str):
+        try:
+            if value.endswith("%"):
+                has_percent_sign = False
+                value = float(value[:-1].rstrip()) / 100.0
+            else:
+                value = float(value)
+        except ValueError:
+            # pylint: disable=raise-missing-from
+            raise Invalid("invalid number")
     if value > 1:
         msg = "Percentage must not be higher than 100%."
         if not has_percent_sign:
@@ -838,7 +1095,7 @@ def possibly_negative_percentage(value):
 
 
 def percentage_int(value):
-    if isinstance(value, str) and value.endswith('%'):
+    if isinstance(value, str) and value.endswith("%"):
         value = int(value[:-1].rstrip())
     return value
 
@@ -847,6 +1104,7 @@ def invalid(message):
     """Mark this value as invalid. Each time *any* value is passed here it will result in a
     validation error with the given message.
     """
+
     def validator(value):
         raise Invalid(message)
 
@@ -854,6 +1112,7 @@ def invalid(message):
 
 
 def valid(value):
+    """A validator that is always valid and returns the value as-is."""
     return value
 
 
@@ -898,20 +1157,25 @@ def one_of(*values, **kwargs):
       - *float* (``bool``, default=False): Whether to convert the incoming values to floats.
       - *space* (``str``, default=' '): What to convert spaces in the input string to.
     """
-    options = ', '.join(f"'{x}'" for x in values)
-    lower = kwargs.pop('lower', False)
-    upper = kwargs.pop('upper', False)
-    string_ = kwargs.pop('string', False) or lower or upper
-    to_int = kwargs.pop('int', False)
-    to_float = kwargs.pop('float', False)
-    space = kwargs.pop('space', ' ')
+    options = ", ".join(f"'{x}'" for x in values)
+    lower = kwargs.pop("lower", False)
+    upper = kwargs.pop("upper", False)
+    string_ = kwargs.pop("string", False) or lower or upper
+    to_int = kwargs.pop("int", False)
+    to_float = kwargs.pop("float", False)
+    space = kwargs.pop("space", " ")
     if kwargs:
         raise ValueError
 
+    @jschema_extractor("one_of")
     def validator(value):
+        # pylint: disable=comparison-with-callable
+        if value == jschema_extractor:
+            return values
+
         if string_:
             value = string(value)
-            value = value.replace(' ', space)
+            value = value.replace(" ", space)
         if to_int:
             value = int_(value)
         if to_float:
@@ -922,12 +1186,13 @@ def one_of(*values, **kwargs):
             value = Upper(value)
         if value not in values:
             import difflib
+
             options_ = [str(x) for x in values]
             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
 
@@ -945,7 +1210,12 @@ def enum(mapping, **kwargs):
     assert isinstance(mapping, dict)
     one_of_validator = one_of(*mapping, **kwargs)
 
+    @jschema_extractor("enum")
     def validator(value):
+        # pylint: disable=comparison-with-callable
+        if value == jschema_extractor:
+            return mapping
+
         value = one_of_validator(value)
         value = add_class_to_obj(value, core.EnumValue)
         value.enum_value = mapping[value]
@@ -954,21 +1224,21 @@ def enum(mapping, **kwargs):
     return validator
 
 
-LAMBDA_ENTITY_ID_PROG = re.compile(r'id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)')
+LAMBDA_ENTITY_ID_PROG = re.compile(r"id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)")
 
 
 def lambda_(value):
     """Coerce this configuration option to a lambda."""
     if not isinstance(value, Lambda):
-        value = Lambda(string_strict(value))
+        value = make_data_base(Lambda(string_strict(value)), 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))
-        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))
+        entity_ids = " ".join(
+            f"'{entity_id_parts[i]}'" for i in range(1, len(entity_id_parts), 2)
+        )
+        raise Invalid(
+            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
 
 
@@ -978,21 +1248,24 @@ def returning_lambda(value):
     Additionally, make sure the lambda returns something.
     """
     value = lambda_(value)
-    if 'return' not in value.value:
-        raise Invalid("Lambda doesn't contain a 'return' statement, but the lambda "
-                      "is expected to return a value. \n"
-                      "Please make sure the lambda contains at least one "
-                      "return statement.")
+    if "return" not in value.value:
+        raise Invalid(
+            "Lambda doesn't contain a 'return' statement, but the lambda "
+            "is expected to return a value. \n"
+            "Please make sure the lambda contains at least one "
+            "return statement."
+        )
     return 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:
+            # pylint: disable=raise-missing-from
             raise Invalid("Width and height dimensions must be integers")
         if width <= 0 or height <= 0:
             raise Invalid("Width and height must at least be 1")
@@ -1000,65 +1273,85 @@ def dimensions(value):
     value = string(value)
     match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value)
     if not match:
-        raise Invalid("Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.")
+        raise Invalid(
+            "Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed."
+        )
     return dimensions([match.group(1), match.group(2)])
 
 
 def directory(value):
     import json
+
     value = string(value)
     path = CORE.relative_config_path(value)
 
-    if CORE.vscode and (not CORE.ace or
-                        os.path.abspath(path) == os.path.abspath(CORE.config_path)):
-        print(json.dumps({
-            'type': 'check_directory_exists',
-            'path': path,
-        }))
+    if CORE.vscode and (
+        not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
+    ):
+        print(
+            json.dumps(
+                {
+                    "type": "check_directory_exists",
+                    "path": path,
+                }
+            )
+        )
         data = json.loads(input())
-        assert data['type'] == 'directory_exists_response'
-        if data['content']:
+        assert data["type"] == "directory_exists_response"
+        if data["content"]:
             return value
-        raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})."
-                      "".format(path, os.path.abspath(path)))
+        raise Invalid(
+            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)))
+        raise Invalid(
+            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)))
+        raise Invalid(
+            f"Path '{path}' is not a directory (full path: {os.path.abspath(path)})."
+        )
     return value
 
 
 def file_(value):
     import json
+
     value = string(value)
     path = CORE.relative_config_path(value)
 
-    if CORE.vscode and (not CORE.ace or
-                        os.path.abspath(path) == os.path.abspath(CORE.config_path)):
-        print(json.dumps({
-            'type': 'check_file_exists',
-            'path': path,
-        }))
+    if CORE.vscode and (
+        not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
+    ):
+        print(
+            json.dumps(
+                {
+                    "type": "check_file_exists",
+                    "path": path,
+                }
+            )
+        )
         data = json.loads(input())
-        assert data['type'] == 'file_exists_response'
-        if data['content']:
+        assert data["type"] == "file_exists_response"
+        if data["content"]:
             return value
-        raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})."
-                      "".format(path, os.path.abspath(path)))
+        raise Invalid(
+            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)))
+        raise Invalid(
+            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)))
+        raise Invalid(
+            f"Path '{path}' is not a file (full path: {os.path.abspath(path)})."
+        )
     return value
 
 
-ENTITY_ID_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
+ENTITY_ID_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_"
 
 
 def entity_id(value):
@@ -1067,9 +1360,9 @@ def entity_id(value):
     Should only be used for 'homeassistant' platforms.
     """
     value = string_strict(value).lower()
-    if value.count('.') != 1:
+    if value.count(".") != 1:
         raise Invalid("Entity ID must have exactly one dot in it")
-    for x in value.split('.'):
+    for x in value.split("."):
         for c in x:
             if c not in ENTITY_ID_CHARACTERS:
                 raise Invalid(f"Invalid character for entity ID: {c}")
@@ -1093,19 +1386,22 @@ def extract_keys(schema):
     return keys
 
 
+@jschema_typed
 def typed_schema(schemas, **kwargs):
     """Create a schema that has a key to distinguish between schemas"""
-    key = kwargs.pop('key', CONF_TYPE)
+    key = kwargs.pop("key", CONF_TYPE)
+    default_schema_option = kwargs.pop("default_type", None)
     key_validator = one_of(*schemas, **kwargs)
 
     def validator(value):
         if not isinstance(value, dict):
             raise Invalid("Value must be dict")
-        if CONF_TYPE not in value:
-            raise Invalid("type not specified!")
         value = value.copy()
-        key_v = key_validator(value.pop(key))
-        value = schemas[key_v](value)
+        schema_option = value.pop(key, default_schema_option)
+        if schema_option is None:
+            raise Invalid(f"{key} not specified!")
+        key_v = key_validator(schema_option)
+        value = Schema(schemas[key_v])(value)
         value[key] = key_v
         return value
 
@@ -1122,18 +1418,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):
@@ -1151,9 +1461,10 @@ class OnlyWith(Optional):
 
     @property
     def default(self):
-        if self._component not in CORE.raw_config:
-            return vol.UNDEFINED
-        return self._default
+        # pylint: disable=unsupported-membership-test
+        if self._component in CORE.loaded_integrations:
+            return self._default
+        return vol.UNDEFINED
 
     @default.setter
     def default(self, value):
@@ -1161,7 +1472,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:
@@ -1181,17 +1492,22 @@ def ensure_schema(schema):
 
 
 def validate_registry_entry(name, registry):
-    base_schema = ensure_schema(registry.base_schema).extend({
-        Optional(CONF_TYPE_ID): valid,
-    }, extra=ALLOW_EXTRA)
+    base_schema = ensure_schema(registry.base_schema).extend(
+        {
+            Optional(CONF_TYPE_ID): valid,
+        },
+        extra=ALLOW_EXTRA,
+    )
     ignore_keys = extract_keys(base_schema)
 
+    @jschema_registry(registry)
     def validator(value):
         if isinstance(value, str):
             value = {value: {}}
         if not isinstance(value, dict):
-            raise Invalid("{} must consist of key-value mapping! Got {}"
-                          "".format(name.title(), value))
+            raise Invalid(
+                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:
             raise Invalid(f"Key missing from {name}! Got {value}")
@@ -1199,9 +1515,10 @@ def validate_registry_entry(name, registry):
             raise Invalid(f"Unable to find {name} with the name '{key}'", [key])
         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))
+            raise Invalid(
+                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:
             value[key] = {}
@@ -1214,9 +1531,9 @@ def validate_registry_entry(name, registry):
             value[key] = registry_entry.schema(value[key])
 
         if registry_entry.type_id is not None:
-            my_base_schema = base_schema.extend({
-                GenerateID(CONF_TYPE_ID): declare_id(registry_entry.type_id)
-            })
+            my_base_schema = base_schema.extend(
+                {GenerateID(CONF_TYPE_ID): declare_id(registry_entry.type_id)}
+            )
             value = my_base_schema(value)
 
         return value
@@ -1228,8 +1545,9 @@ def validate_registry(name, registry):
     return ensure_list(validate_registry_entry(name, registry))
 
 
+@jschema_composite
 def maybe_simple_value(*validators, **kwargs):
-    key = kwargs.pop('key', CONF_VALUE)
+    key = kwargs.pop("key", CONF_VALUE)
     validator = All(*validators)
 
     def validate(value):
@@ -1240,30 +1558,55 @@ def maybe_simple_value(*validators, **kwargs):
     return validate
 
 
-MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema({
-    Required(CONF_TOPIC): subscribe_topic,
-    Optional(CONF_PAYLOAD_AVAILABLE, default='online'): mqtt_payload,
-    Optional(CONF_PAYLOAD_NOT_AVAILABLE, default='offline'): mqtt_payload,
-})
+_ENTITY_CATEGORIES = {
+    ENTITY_CATEGORY_NONE: cg.EntityCategory.ENTITY_CATEGORY_NONE,
+    ENTITY_CATEGORY_CONFIG: cg.EntityCategory.ENTITY_CATEGORY_CONFIG,
+    ENTITY_CATEGORY_DIAGNOSTIC: cg.EntityCategory.ENTITY_CATEGORY_DIAGNOSTIC,
+}
 
-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({
-    Optional(CONF_COMMAND_TOPIC): All(requires_component('mqtt'), subscribe_topic),
-})
+def entity_category(value):
+    return enum(_ENTITY_CATEGORIES, lower=True)(value)
 
-COMPONENT_SCHEMA = Schema({
-    Optional(CONF_SETUP_PRIORITY): float_
-})
+
+MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema(
+    {
+        Required(CONF_TOPIC): subscribe_topic,
+        Optional(CONF_PAYLOAD_AVAILABLE, default="online"): mqtt_payload,
+        Optional(CONF_PAYLOAD_NOT_AVAILABLE, default="offline"): mqtt_payload,
+    }
+)
+
+MQTT_COMPONENT_SCHEMA = Schema(
+    {
+        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)
+        ),
+    }
+)
+
+MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend(
+    {
+        Optional(CONF_COMMAND_TOPIC): All(requires_component("mqtt"), subscribe_topic),
+    }
+)
+
+ENTITY_BASE_SCHEMA = Schema(
+    {
+        Optional(CONF_NAME): string,
+        Optional(CONF_INTERNAL): boolean,
+        Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
+        Optional(CONF_ICON): icon,
+        Optional(CONF_ENTITY_CATEGORY): entity_category,
+    }
+)
+
+ENTITY_BASE_SCHEMA.add_extra(_entity_base_validator)
+
+COMPONENT_SCHEMA = Schema({Optional(CONF_SETUP_PRIORITY): float_})
 
 
 def polling_component_schema(default_update_interval):
@@ -1273,10 +1616,133 @@ def polling_component_schema(default_update_interval):
     :param default_update_interval: The default update interval to set for the integration.
     """
     if default_update_interval is None:
-        return COMPONENT_SCHEMA.extend({
-            Required(CONF_UPDATE_INTERVAL): default_update_interval,
-        })
+        return COMPONENT_SCHEMA.extend(
+            {
+                Required(CONF_UPDATE_INTERVAL): default_update_interval,
+            }
+        )
     assert isinstance(default_update_interval, str)
-    return COMPONENT_SCHEMA.extend({
-        Optional(CONF_UPDATE_INTERVAL, default=default_update_interval): update_interval,
-    })
+    return COMPONENT_SCHEMA.extend(
+        {
+            Optional(
+                CONF_UPDATE_INTERVAL, default=default_update_interval
+            ): update_interval,
+        }
+    )
+
+
+def url(value):
+    import urllib.parse
+
+    value = string_strict(value)
+    try:
+        parsed = urllib.parse.urlparse(value)
+    except ValueError as e:
+        raise Invalid("Not a valid URL") from e
+
+    if not parsed.scheme or not parsed.netloc:
+        raise Invalid("Expected a URL scheme and host")
+    return parsed.geturl()
+
+
+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 platformio_version_constraint(value):
+    # for documentation on valid version constraints:
+    # https://docs.platformio.org/en/latest/core/userguide/platforms/cmd_install.html#cmd-platform-install
+
+    value = string_strict(value)
+    constraints = []
+    for item in value.split(","):
+        # find and strip prefix operator
+        op = None
+        for test_op in ("^", "~", ">=", ">", "<=", "<", "!="):
+            if item.startswith(test_op):
+                op = test_op
+                item = item[len(test_op) :]
+                break
+
+        constraints.append((op, version_number(item)))
+    return constraints
+
+
+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 31359e610d..970b3c6578 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,592 +1,930 @@
 """Constants used by esphome."""
 
-MAJOR_VERSION = 1
-MINOR_VERSION = 15
-PATCH_VERSION = '0-dev'
-__short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}'
-__version__ = f'{__short_version__}.{PATCH_VERSION}'
+__version__ = "2022.1.0-dev"
 
-ESP_PLATFORM_ESP32 = 'ESP32'
-ESP_PLATFORM_ESP8266 = 'ESP8266'
-ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266]
+ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 
-ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
-ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage'
-ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0'
-ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0'
-ARDUINO_VERSION_ESP32_1_0_2 = 'espressif32@1.9.0'
-ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0'
-ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0'
-ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \
-                              '/stage'
-ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.1'
-ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0'
-ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3'
-ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0'
-SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'}
-HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'}
+PLATFORM_ESP32 = "esp32"
+PLATFORM_ESP8266 = "esp8266"
 
-CONF_ABOVE = 'above'
-CONF_ACCELERATION = 'acceleration'
-CONF_ACCELERATION_X = 'acceleration_x'
-CONF_ACCELERATION_Y = 'acceleration_y'
-CONF_ACCELERATION_Z = 'acceleration_z'
-CONF_ACCURACY = 'accuracy'
-CONF_ACCURACY_DECIMALS = 'accuracy_decimals'
-CONF_ACTION_ID = 'action_id'
-CONF_ADDRESS = 'address'
-CONF_ALPHA = 'alpha'
-CONF_AND = 'and'
-CONF_AP = 'ap'
-CONF_ARDUINO_VERSION = 'arduino_version'
-CONF_ARGS = 'args'
-CONF_ASSUMED_STATE = 'assumed_state'
-CONF_AT = 'at'
-CONF_ATTENUATION = 'attenuation'
-CONF_AUTH = 'auth'
-CONF_AUTOMATION_ID = 'automation_id'
-CONF_AVAILABILITY = 'availability'
-CONF_AWAY = 'away'
-CONF_AWAY_CONFIG = 'away_config'
-CONF_BATTERY_LEVEL = 'battery_level'
-CONF_BATTERY_VOLTAGE = 'battery_voltage'
-CONF_BAUD_RATE = 'baud_rate'
-CONF_BELOW = 'below'
-CONF_BINARY = 'binary'
-CONF_BINARY_SENSOR = 'binary_sensor'
-CONF_BINARY_SENSORS = 'binary_sensors'
-CONF_BIRTH_MESSAGE = 'birth_message'
-CONF_BIT_DEPTH = 'bit_depth'
-CONF_BLUE = 'blue'
-CONF_BOARD = 'board'
-CONF_BOARD_FLASH_MODE = 'board_flash_mode'
-CONF_BRANCH = 'branch'
-CONF_BRIGHTNESS = 'brightness'
-CONF_BROKER = 'broker'
-CONF_BSSID = 'bssid'
-CONF_BUFFER_SIZE = 'buffer_size'
-CONF_BUILD_PATH = 'build_path'
-CONF_BUS_VOLTAGE = 'bus_voltage'
-CONF_BUSY_PIN = 'busy_pin'
-CONF_CALIBRATE_LINEAR = 'calibrate_linear'
-CONF_CALIBRATION = 'calibration'
-CONF_CAPACITANCE = 'capacitance'
-CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent'
-CONF_CARRIER_FREQUENCY = 'carrier_frequency'
-CONF_CHANGE_MODE_EVERY = 'change_mode_every'
-CONF_CHANNEL = 'channel'
-CONF_CHANNELS = 'channels'
-CONF_CHIPSET = 'chipset'
-CONF_CLIENT_ID = 'client_id'
-CONF_CLK_PIN = 'clk_pin'
-CONF_CLOCK_PIN = 'clock_pin'
-CONF_CLOSE_ACTION = 'close_action'
-CONF_CLOSE_DURATION = 'close_duration'
-CONF_CLOSE_ENDSTOP = 'close_endstop'
-CONF_CO2 = 'co2'
-CONF_CODE = 'code'
-CONF_COLD_WHITE = 'cold_white'
-CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature'
-CONF_COLOR_CORRECT = 'color_correct'
-CONF_COLOR_TEMPERATURE = 'color_temperature'
-CONF_COLORS = 'colors'
-CONF_COMMAND = 'command'
-CONF_COMMAND_TOPIC = 'command_topic'
-CONF_COMMENT = 'comment'
-CONF_COMMIT = 'commit'
-CONF_COMPONENT_ID = 'component_id'
-CONF_COMPONENTS = 'components'
-CONF_CONDITION = 'condition'
-CONF_CONDITION_ID = 'condition_id'
-CONF_CONDUCTIVITY = 'conductivity'
-CONF_COOL_ACTION = 'cool_action'
-CONF_COUNT_MODE = 'count_mode'
-CONF_CRON = 'cron'
-CONF_CS_PIN = 'cs_pin'
-CONF_CSS_INCLUDE = 'css_include'
-CONF_CSS_URL = 'css_url'
-CONF_CURRENT = 'current'
-CONF_CURRENT_OPERATION = 'current_operation'
-CONF_CURRENT_RESISTOR = 'current_resistor'
-CONF_DALLAS_ID = 'dallas_id'
-CONF_DATA = 'data'
-CONF_DATA_PIN = 'data_pin'
-CONF_DATA_PINS = 'data_pins'
-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_DEBOUNCE = 'debounce'
-CONF_DECELERATION = 'deceleration'
-CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = 'default_target_temperature_high'
-CONF_DEFAULT_TARGET_TEMPERATURE_LOW = 'default_target_temperature_low'
-CONF_DEFAULT_TRANSITION_LENGTH = 'default_transition_length'
-CONF_DELAY = 'delay'
-CONF_DELTA = 'delta'
-CONF_DEVICE = 'device'
-CONF_DEVICE_CLASS = 'device_class'
-CONF_DIMENSIONS = 'dimensions'
-CONF_DIO_PIN = 'dio_pin'
-CONF_DIR_PIN = 'dir_pin'
-CONF_DIRECTION = 'direction'
-CONF_DISCOVERY = 'discovery'
-CONF_DISCOVERY_PREFIX = 'discovery_prefix'
-CONF_DISCOVERY_RETAIN = 'discovery_retain'
-CONF_DISTANCE = 'distance'
-CONF_DIV_RATIO = 'div_ratio'
-CONF_DNS1 = 'dns1'
-CONF_DNS2 = 'dns2'
-CONF_DOMAIN = 'domain'
-CONF_DUMP = 'dump'
-CONF_DURATION = 'duration'
-CONF_ECHO_PIN = 'echo_pin'
-CONF_EFFECT = 'effect'
-CONF_EFFECTS = 'effects'
-CONF_ELSE = 'else'
-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_ESPHOME = 'esphome'
-CONF_ESPHOME_CORE_VERSION = 'esphome_core_version'
-CONF_EVENT = 'event'
-CONF_EXPIRE_AFTER = 'expire_after'
-CONF_EXTERNAL_VCC = 'external_vcc'
-CONF_FALLING_EDGE = 'falling_edge'
-CONF_FAMILY = 'family'
-CONF_FAN_MODE = 'fan_mode'
-CONF_FAST_CONNECT = 'fast_connect'
-CONF_FILE = 'file'
-CONF_FILTER = 'filter'
-CONF_FILTER_OUT = 'filter_out'
-CONF_FILTERS = 'filters'
-CONF_FLASH_LENGTH = 'flash_length'
-CONF_FOR = 'for'
-CONF_FORCE_UPDATE = 'force_update'
-CONF_FORMALDEHYDE = 'formaldehyde'
-CONF_FORMAT = 'format'
-CONF_FREQUENCY = 'frequency'
-CONF_FROM = 'from'
-CONF_FULL_UPDATE_EVERY = 'full_update_every'
-CONF_GAIN = 'gain'
-CONF_GAMMA_CORRECT = 'gamma_correct'
-CONF_GAS_RESISTANCE = 'gas_resistance'
-CONF_GATEWAY = 'gateway'
-CONF_GLYPHS = 'glyphs'
-CONF_GPIO = 'gpio'
-CONF_GREEN = 'green'
-CONF_GROUP = 'group'
-CONF_HARDWARE_UART = 'hardware_uart'
-CONF_HEARTBEAT = 'heartbeat'
-CONF_HEAT_ACTION = 'heat_action'
-CONF_HEATER = 'heater'
-CONF_HIDDEN = 'hidden'
-CONF_HIGH = 'high'
-CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference'
-CONF_HOUR = 'hour'
-CONF_HOURS = 'hours'
-CONF_HUMIDITY = 'humidity'
-CONF_I2C = 'i2c'
-CONF_I2C_ID = 'i2c_id'
-CONF_ICON = 'icon'
-CONF_ID = 'id'
-CONF_IDLE = 'idle'
-CONF_IDLE_ACTION = 'idle_action'
-CONF_IDLE_LEVEL = 'idle_level'
-CONF_IF = 'if'
-CONF_IIR_FILTER = 'iir_filter'
-CONF_ILLUMINANCE = 'illuminance'
-CONF_INCLUDES = 'includes'
-CONF_INDEX = 'index'
-CONF_INDOOR = 'indoor'
-CONF_INITIAL_MODE = 'initial_mode'
-CONF_INITIAL_VALUE = 'initial_value'
-CONF_INTEGRATION_TIME = 'integration_time'
-CONF_INTENSITY = 'intensity'
-CONF_INTERLOCK = 'interlock'
-CONF_INTERNAL = 'internal'
-CONF_INTERNAL_FILTER = 'internal_filter'
-CONF_INTERVAL = 'interval'
-CONF_INVALID_COOLDOWN = 'invalid_cooldown'
-CONF_INVERT = 'invert'
-CONF_INVERTED = 'inverted'
-CONF_IP_ADDRESS = 'ip_address'
-CONF_JS_INCLUDE = 'js_include'
-CONF_JS_URL = 'js_url'
-CONF_JVC = 'jvc'
-CONF_KEEP_ON_TIME = 'keep_on_time'
-CONF_KEEPALIVE = 'keepalive'
-CONF_LAMBDA = 'lambda'
-CONF_LEVEL = 'level'
-CONF_LG = 'lg'
-CONF_LIBRARIES = 'libraries'
-CONF_LIGHT = 'light'
-CONF_LIGHTNING_ENERGY = 'lightning_energy'
-CONF_LIGHTNING_THRESHOLD = 'lightning_threshold'
-CONF_LOADED_INTEGRATIONS = 'loaded_integrations'
-CONF_LOCAL = 'local'
-CONF_LOG_TOPIC = 'log_topic'
-CONF_LOGGER = 'logger'
-CONF_LOGS = 'logs'
-CONF_LOW = 'low'
-CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference'
-CONF_MAC_ADDRESS = 'mac_address'
-CONF_MAINS_FILTER = 'mains_filter'
-CONF_MAKE_ID = 'make_id'
-CONF_MANUAL_IP = 'manual_ip'
-CONF_MANUFACTURER_ID = 'manufacturer_id'
-CONF_MASK_DISTURBER = 'mask_disturber'
-CONF_MAX_CURRENT = 'max_current'
-CONF_MAX_DURATION = 'max_duration'
-CONF_MAX_LENGTH = 'max_length'
-CONF_MAX_LEVEL = 'max_level'
-CONF_MAX_POWER = 'max_power'
-CONF_MAX_REFRESH_RATE = 'max_refresh_rate'
-CONF_MAX_SPEED = 'max_speed'
-CONF_MAX_TEMPERATURE = 'max_temperature'
-CONF_MAX_VALUE = 'max_value'
-CONF_MAX_VOLTAGE = 'max_voltage'
-CONF_MEASUREMENT_DURATION = 'measurement_duration'
-CONF_MEASUREMENT_SEQUENCE_NUMBER = 'measurement_sequence_number'
-CONF_MEDIUM = 'medium'
-CONF_MEMORY_BLOCKS = 'memory_blocks'
-CONF_METHOD = 'method'
-CONF_MIN_LENGTH = 'min_length'
-CONF_MIN_LEVEL = 'min_level'
-CONF_MIN_POWER = 'min_power'
-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_MODEL = 'model'
-CONF_MOISTURE = 'moisture'
-CONF_MONTHS = 'months'
-CONF_MOSI_PIN = 'mosi_pin'
-CONF_MOVEMENT_COUNTER = 'movement_counter'
-CONF_MQTT = 'mqtt'
-CONF_MQTT_ID = 'mqtt_id'
-CONF_MULTIPLEXER = 'multiplexer'
-CONF_MULTIPLY = 'multiply'
-CONF_NAME = 'name'
-CONF_NBITS = 'nbits'
-CONF_NEC = 'nec'
-CONF_NETWORKS = 'networks'
-CONF_NOISE_LEVEL = 'noise_level'
-CONF_NUM_ATTEMPTS = 'num_attempts'
-CONF_NUM_CHANNELS = 'num_channels'
-CONF_NUM_CHIPS = 'num_chips'
-CONF_NUM_LEDS = 'num_leds'
-CONF_NUMBER = 'number'
-CONF_OFFSET = 'offset'
-CONF_ON = 'on'
-CONF_ON_BLE_ADVERTISE = 'on_ble_advertise'
-CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = 'on_ble_manufacturer_data_advertise'
-CONF_ON_BLE_SERVICE_DATA_ADVERTISE = 'on_ble_service_data_advertise'
-CONF_ON_BOOT = 'on_boot'
-CONF_ON_CLICK = 'on_click'
-CONF_ON_DOUBLE_CLICK = 'on_double_click'
-CONF_ON_JSON_MESSAGE = 'on_json_message'
-CONF_ON_LOOP = 'on_loop'
-CONF_ON_MESSAGE = 'on_message'
-CONF_ON_MULTI_CLICK = 'on_multi_click'
-CONF_ON_PRESS = 'on_press'
-CONF_ON_RAW_VALUE = 'on_raw_value'
-CONF_ON_RELEASE = 'on_release'
-CONF_ON_SHUTDOWN = 'on_shutdown'
-CONF_ON_STATE = 'on_state'
-CONF_ON_TAG = 'on_tag'
-CONF_ON_TIME = 'on_time'
-CONF_ON_TURN_OFF = 'on_turn_off'
-CONF_ON_TURN_ON = 'on_turn_on'
-CONF_ON_VALUE = 'on_value'
-CONF_ON_VALUE_RANGE = 'on_value_range'
-CONF_ONE = 'one'
-CONF_OPEN_ACTION = 'open_action'
-CONF_OPEN_DURATION = 'open_duration'
-CONF_OPEN_ENDSTOP = 'open_endstop'
-CONF_OPTIMISTIC = 'optimistic'
-CONF_OR = 'or'
-CONF_OSCILLATING = 'oscillating'
-CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic'
-CONF_OSCILLATION_OUTPUT = 'oscillation_output'
-CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic'
-CONF_OTA = 'ota'
-CONF_OUTPUT = 'output'
-CONF_OUTPUT_ID = 'output_id'
-CONF_OUTPUTS = 'outputs'
-CONF_OVERSAMPLING = 'oversampling'
-CONF_PAGE_ID = 'page_id'
-CONF_PAGES = 'pages'
-CONF_PANASONIC = 'panasonic'
-CONF_PASSWORD = 'password'
-CONF_PAYLOAD = 'payload'
-CONF_PAYLOAD_AVAILABLE = 'payload_available'
-CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
-CONF_PERIOD = 'period'
-CONF_PHASE_BALANCER = 'phase_balancer'
-CONF_PIN = 'pin'
-CONF_PIN_A = 'pin_a'
-CONF_PIN_B = 'pin_b'
-CONF_PIN_C = 'pin_c'
-CONF_PIN_D = 'pin_d'
-CONF_PINS = 'pins'
-CONF_PLATFORM = 'platform'
-CONF_PLATFORMIO_OPTIONS = 'platformio_options'
-CONF_PM_1_0 = 'pm_1_0'
-CONF_PM_10_0 = 'pm_10_0'
-CONF_PM_2_5 = 'pm_2_5'
-CONF_PM_4_0 = 'pm_4_0'
-CONF_PM_SIZE = 'pm_size'
-CONF_PMC_0_5 = 'pmc_0_5'
-CONF_PMC_1_0 = 'pmc_1_0'
-CONF_PMC_10_0 = 'pmc_10_0'
-CONF_PMC_2_5 = 'pmc_2_5'
-CONF_PMC_4_0 = 'pmc_4_0'
-CONF_PORT = 'port'
-CONF_POSITION = 'position'
-CONF_POSITION_ACTION = 'position_action'
-CONF_POWER = 'power'
-CONF_POWER_FACTOR = 'power_factor'
-CONF_POWER_ON_VALUE = 'power_on_value'
-CONF_POWER_SAVE_MODE = 'power_save_mode'
-CONF_POWER_SUPPLY = 'power_supply'
-CONF_PRESSURE = 'pressure'
-CONF_PRIORITY = 'priority'
-CONF_PROTOCOL = 'protocol'
-CONF_PULL_MODE = 'pull_mode'
-CONF_PULSE_LENGTH = 'pulse_length'
-CONF_QOS = 'qos'
-CONF_RANDOM = 'random'
-CONF_RANGE = 'range'
-CONF_RANGE_FROM = 'range_from'
-CONF_RANGE_TO = 'range_to'
-CONF_RATE = 'rate'
-CONF_RAW = 'raw'
-CONF_RC_CODE_1 = 'rc_code_1'
-CONF_RC_CODE_2 = 'rc_code_2'
-CONF_REBOOT_TIMEOUT = 'reboot_timeout'
-CONF_RECEIVE_TIMEOUT = 'receive_timeout'
-CONF_RED = 'red'
-CONF_REFERENCE_RESISTANCE = 'reference_resistance'
-CONF_REFERENCE_TEMPERATURE = 'reference_temperature'
-CONF_REPEAT = 'repeat'
-CONF_REPOSITORY = 'repository'
-CONF_RESET_PIN = 'reset_pin'
-CONF_RESIZE = 'resize'
-CONF_RESOLUTION = 'resolution'
-CONF_RESTORE = 'restore'
-CONF_RESTORE_MODE = 'restore_mode'
-CONF_RESTORE_STATE = 'restore_state'
-CONF_RESTORE_VALUE = 'restore_value'
-CONF_RETAIN = 'retain'
-CONF_RGB_ORDER = 'rgb_order'
-CONF_RGBW = 'rgbw'
-CONF_RISING_EDGE = 'rising_edge'
-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_ONLY = 'rx_only'
-CONF_RX_PIN = 'rx_pin'
-CONF_SAFE_MODE = 'safe_mode'
-CONF_SAMSUNG = 'samsung'
-CONF_SCAN = 'scan'
-CONF_SCL = 'scl'
-CONF_SCL_PIN = 'scl_pin'
-CONF_SDA = 'sda'
-CONF_SDO_PIN = 'sdo_pin'
-CONF_SECOND = 'second'
-CONF_SECONDS = 'seconds'
-CONF_SEGMENTS = 'segments'
-CONF_SEL_PIN = 'sel_pin'
-CONF_SEND_EVERY = 'send_every'
-CONF_SEND_FIRST_AT = 'send_first_at'
-CONF_SENSOR = 'sensor'
-CONF_SENSOR_ID = 'sensor_id'
-CONF_SENSORS = 'sensors'
-CONF_SEQUENCE = 'sequence'
-CONF_SERVERS = 'servers'
-CONF_SERVICE = 'service'
-CONF_SERVICE_UUID = 'service_uuid'
-CONF_SERVICES = 'services'
-CONF_SETUP_MODE = 'setup_mode'
-CONF_SETUP_PRIORITY = 'setup_priority'
-CONF_SHUNT_RESISTANCE = 'shunt_resistance'
-CONF_SHUNT_VOLTAGE = 'shunt_voltage'
-CONF_SHUTDOWN_MESSAGE = 'shutdown_message'
-CONF_SIZE = 'size'
-CONF_SLEEP_DURATION = 'sleep_duration'
-CONF_SLEEP_PIN = 'sleep_pin'
-CONF_SLEEP_WHEN_DONE = 'sleep_when_done'
-CONF_SONY = 'sony'
-CONF_SPEED = 'speed'
-CONF_SPEED_COMMAND_TOPIC = 'speed_command_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_STATE = 'state'
-CONF_STATE_TOPIC = 'state_topic'
-CONF_STATIC_IP = 'static_ip'
-CONF_STEP_MODE = 'step_mode'
-CONF_STEP_PIN = 'step_pin'
-CONF_STOP = 'stop'
-CONF_STOP_ACTION = 'stop_action'
-CONF_SUBNET = 'subnet'
-CONF_SUPPORTS_COOL = 'supports_cool'
-CONF_SUPPORTS_HEAT = 'supports_heat'
-CONF_SWING_MODE = 'swing_mode'
-CONF_SWITCHES = 'switches'
-CONF_SYNC = 'sync'
-CONF_TAG = 'tag'
-CONF_TARGET = 'target'
-CONF_TARGET_TEMPERATURE = 'target_temperature'
-CONF_TARGET_TEMPERATURE_HIGH = 'target_temperature_high'
-CONF_TARGET_TEMPERATURE_LOW = 'target_temperature_low'
-CONF_TEMPERATURE = 'temperature'
-CONF_TEMPERATURE_STEP = 'temperature_step'
-CONF_TEXT_SENSORS = 'text_sensors'
-CONF_THEN = 'then'
-CONF_THRESHOLD = 'threshold'
-CONF_THROTTLE = 'throttle'
-CONF_TILT = 'tilt'
-CONF_TILT_ACTION = 'tilt_action'
-CONF_TILT_LAMBDA = 'tilt_lambda'
-CONF_TIME = 'time'
-CONF_TIME_ID = 'time_id'
-CONF_TIMEOUT = 'timeout'
-CONF_TIMES = 'times'
-CONF_TIMEZONE = 'timezone'
-CONF_TIMING = 'timing'
-CONF_TO = 'to'
-CONF_TOLERANCE = 'tolerance'
-CONF_TOPIC = 'topic'
-CONF_TOPIC_PREFIX = 'topic_prefix'
-CONF_TRANSITION_LENGTH = 'transition_length'
-CONF_TRIGGER_ID = 'trigger_id'
-CONF_TRIGGER_PIN = 'trigger_pin'
-CONF_TURN_OFF_ACTION = 'turn_off_action'
-CONF_TURN_ON_ACTION = 'turn_on_action'
-CONF_TX_BUFFER_SIZE = 'tx_buffer_size'
-CONF_TX_PIN = 'tx_pin'
-CONF_TX_POWER = 'tx_power'
-CONF_TYPE = 'type'
-CONF_TYPE_ID = 'type_id'
-CONF_UART_ID = 'uart_id'
-CONF_UID = 'uid'
-CONF_UNIQUE = 'unique'
-CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement'
-CONF_UPDATE_INTERVAL = 'update_interval'
-CONF_UPDATE_ON_BOOT = 'update_on_boot'
-CONF_URL = 'url'
-CONF_USE_ADDRESS = 'use_address'
-CONF_USERNAME = 'username'
-CONF_UUID = 'uuid'
-CONF_VALUE = 'value'
-CONF_VARIABLES = 'variables'
-CONF_VARIANT = 'variant'
-CONF_VISUAL = 'visual'
-CONF_VOLTAGE = 'voltage'
-CONF_VOLTAGE_ATTENUATION = 'voltage_attenuation'
-CONF_VOLTAGE_DIVIDER = 'voltage_divider'
-CONF_WAIT_TIME = 'wait_time'
-CONF_WAIT_UNTIL = 'wait_until'
-CONF_WAKEUP_PIN = 'wakeup_pin'
-CONF_WARM_WHITE = 'warm_white'
-CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature'
-CONF_WATCHDOG_THRESHOLD = 'watchdog_threshold'
-CONF_WHILE = 'while'
-CONF_WHITE = 'white'
-CONF_WIDTH = 'width'
-CONF_WIFI = 'wifi'
-CONF_WILL_MESSAGE = 'will_message'
-CONF_WIND_DIRECTION_DEGREES = 'wind_direction_degrees'
-CONF_WIND_SPEED = 'wind_speed'
-CONF_WINDOW_SIZE = 'window_size'
-CONF_ZERO = 'zero'
+TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266]
 
-ICON_ACCELERATION = 'mdi:axis-arrow'
-ICON_ACCELERATION_X = 'mdi:axis-x-arrow'
-ICON_ACCELERATION_Y = 'mdi:axis-y-arrow'
-ICON_ACCELERATION_Z = 'mdi:axis-z-arrow'
-ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical'
-ICON_BATTERY = 'mdi:battery'
-ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download'
-ICON_BRIGHTNESS_5 = 'mdi:brightness-5'
-ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline'
-ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon'
-ICON_COUNTER = 'mdi:counter'
-ICON_CURRENT_AC = 'mdi:current-ac'
-ICON_EMPTY = ''
-ICON_FLASH = 'mdi:flash'
-ICON_FLOWER = 'mdi:flower'
-ICON_GAS_CYLINDER = 'mdi:gas-cylinder'
-ICON_GAUGE = 'mdi:gauge'
-ICON_LIGHTBULB = 'mdi:lightbulb'
-ICON_MAGNET = 'mdi:magnet'
-ICON_NEW_BOX = 'mdi:new-box'
-ICON_PERCENT = 'mdi:percent'
-ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2'
-ICON_POWER = 'mdi:power'
-ICON_PULSE = 'mdi:pulse'
-ICON_RADIATOR = 'mdi:radiator'
-ICON_RESTART = 'mdi:restart'
-ICON_ROTATE_RIGHT = 'mdi:rotate-right'
-ICON_RULER = 'mdi:ruler'
-ICON_SCALE = 'mdi:scale'
-ICON_SCREEN_ROTATION = 'mdi:screen-rotation'
-ICON_SIGN_DIRECTION = 'mdi:sign-direction'
-ICON_SIGNAL = 'mdi:signal-distance-variant'
-ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal'
-ICON_THERMOMETER = 'mdi:thermometer'
-ICON_TIMER = 'mdi:timer'
-ICON_WATER_PERCENT = 'mdi:water-percent'
-ICON_WEATHER_SUNSET = 'mdi:weather-sunset'
-ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down'
-ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up'
-ICON_WEATHER_WINDY = 'mdi:weather-windy'
-ICON_WIFI = 'mdi:wifi'
+SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
+HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
+SECRETS_FILES = {"secrets.yaml", "secrets.yml"}
 
-UNIT_AMPERE = 'A'
-UNIT_CELSIUS = '°C'
-UNIT_COUNTS_PER_CUBIC_METER = '#/m³'
-UNIT_DECIBEL = 'dB'
-UNIT_DECIBEL_MILLIWATT = 'dBm'
-UNIT_DEGREE_PER_SECOND = '°/s'
-UNIT_DEGREES = '°'
-UNIT_EMPTY = ''
-UNIT_G = 'G'
-UNIT_HECTOPASCAL = 'hPa'
-UNIT_HERTZ = 'hz'
-UNIT_KELVIN = 'K'
-UNIT_KILOMETER = 'km'
-UNIT_KILOMETER_PER_HOUR = 'km/h'
-UNIT_LUX = 'lx'
-UNIT_METER = 'm'
-UNIT_METER_PER_SECOND_SQUARED = 'm/s²'
-UNIT_MICROGRAMS_PER_CUBIC_METER = 'µg/m³'
-UNIT_MICROMETER = 'µm'
-UNIT_MICROSIEMENS_PER_CENTIMETER = 'µS/cm'
-UNIT_MICROTESLA = 'µT'
-UNIT_OHM = 'Ω'
-UNIT_PARTS_PER_BILLION = 'ppb'
-UNIT_PARTS_PER_MILLION = 'ppm'
-UNIT_PERCENT = '%'
-UNIT_PULSES_PER_MINUTE = 'pulses/min'
-UNIT_SECOND = 's'
-UNIT_STEPS = 'steps'
-UNIT_VOLT = 'V'
-UNIT_VOLT_AMPS = 'VA'
-UNIT_VOLT_AMPS_REACTIVE = 'VAR'
-UNIT_WATT = 'W'
-UNIT_WATT_HOURS = 'Wh'
 
-DEVICE_CLASS_CONNECTIVITY = 'connectivity'
-DEVICE_CLASS_MOVING = 'moving'
+CONF_ABOVE = "above"
+CONF_ACCELERATION = "acceleration"
+CONF_ACCELERATION_X = "acceleration_x"
+CONF_ACCELERATION_Y = "acceleration_y"
+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_AFTER = "after"
+CONF_ALPHA = "alpha"
+CONF_ALTITUDE = "altitude"
+CONF_AND = "and"
+CONF_AP = "ap"
+CONF_APPARENT_POWER = "apparent_power"
+CONF_ARDUINO_VERSION = "arduino_version"
+CONF_ARGS = "args"
+CONF_ASSUMED_STATE = "assumed_state"
+CONF_AT = "at"
+CONF_ATTENUATION = "attenuation"
+CONF_ATTRIBUTE = "attribute"
+CONF_AUTH = "auth"
+CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled"
+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"
+CONF_BINARY_SENSORS = "binary_sensors"
+CONF_BINDKEY = "bindkey"
+CONF_BIRTH_MESSAGE = "birth_message"
+CONF_BIT_DEPTH = "bit_depth"
+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"
+CONF_BSSID = "bssid"
+CONF_BUFFER_SIZE = "buffer_size"
+CONF_BUILD_PATH = "build_path"
+CONF_BUS_VOLTAGE = "bus_voltage"
+CONF_BUSY_PIN = "busy_pin"
+CONF_BYTES = "bytes"
+CONF_CALCULATED_LUX = "calculated_lux"
+CONF_CALIBRATE_LINEAR = "calibrate_linear"
+CONF_CALIBRATION = "calibration"
+CONF_CAPACITANCE = "capacitance"
+CONF_CAPACITY = "capacity"
+CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent"
+CONF_CARRIER_FREQUENCY = "carrier_frequency"
+CONF_CERTIFICATE = "certificate"
+CONF_CERTIFICATE_AUTHORITY = "certificate_authority"
+CONF_CHANGE_MODE_EVERY = "change_mode_every"
+CONF_CHANNEL = "channel"
+CONF_CHANNELS = "channels"
+CONF_CHIPSET = "chipset"
+CONF_CLIENT_ID = "client_id"
+CONF_CLK_PIN = "clk_pin"
+CONF_CLOCK_PIN = "clock_pin"
+CONF_CLOSE_ACTION = "close_action"
+CONF_CLOSE_DURATION = "close_duration"
+CONF_CLOSE_ENDSTOP = "close_endstop"
+CONF_CO2 = "co2"
+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"
+CONF_COMMAND_TOPIC = "command_topic"
+CONF_COMMENT = "comment"
+CONF_COMMIT = "commit"
+CONF_COMPONENT_ID = "component_id"
+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"
+CONF_CRON = "cron"
+CONF_CS_PIN = "cs_pin"
+CONF_CSS_INCLUDE = "css_include"
+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"
+CONF_CUSTOM_PRESETS = "custom_presets"
+CONF_DALLAS_ID = "dallas_id"
+CONF_DATA = "data"
+CONF_DATA_PIN = "data_pin"
+CONF_DATA_PINS = "data_pins"
+CONF_DATA_RATE = "data_rate"
+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_DEBUG = "debug"
+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"
+CONF_DELAY = "delay"
+CONF_DELIMITER = "delimiter"
+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"
+CONF_DISCOVERY_UNIQUE_ID_GENERATOR = "discovery_unique_id_generator"
+CONF_DISTANCE = "distance"
+CONF_DITHER = "dither"
+CONF_DIV_RATIO = "div_ratio"
+CONF_DNS1 = "dns1"
+CONF_DNS2 = "dns2"
+CONF_DOMAIN = "domain"
+CONF_DRY_ACTION = "dry_action"
+CONF_DRY_MODE = "dry_mode"
+CONF_DUMMY_RECEIVER = "dummy_receiver"
+CONF_DUMMY_RECEIVER_ID = "dummy_receiver_id"
+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_PIN = "enable_pin"
+CONF_ENABLE_TIME = "enable_time"
+CONF_ENERGY = "energy"
+CONF_ENTITY_CATEGORY = "entity_category"
+CONF_ENTITY_ID = "entity_id"
+CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
+CONF_ESPHOME = "esphome"
+CONF_ETHERNET = "ethernet"
+CONF_EVENT = "event"
+CONF_EXPIRE_AFTER = "expire_after"
+CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy"
+CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy"
+CONF_EXTERNAL_COMPONENTS = "external_components"
+CONF_EXTERNAL_VCC = "external_vcc"
+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"
+CONF_FAN_MODE_LOW_ACTION = "fan_mode_low_action"
+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"
+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"
+CONF_HIDE_TIMESTAMP = "hide_timestamp"
+CONF_HIGH = "high"
+CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference"
+CONF_HOUR = "hour"
+CONF_HOURS = "hours"
+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"
+CONF_IDLE = "idle"
+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"
+CONF_IMPORT_ACTIVE_ENERGY = "import_active_energy"
+CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy"
+CONF_INCLUDE_INTERNAL = "include_internal"
+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"
+CONF_INTERNAL = "internal"
+CONF_INTERNAL_FILTER = "internal_filter"
+CONF_INTERRUPT = "interrupt"
+CONF_INTERVAL = "interval"
+CONF_INVALID_COOLDOWN = "invalid_cooldown"
+CONF_INVERT = "invert"
+CONF_INVERTED = "inverted"
+CONF_IP_ADDRESS = "ip_address"
+CONF_JS_INCLUDE = "js_include"
+CONF_JS_URL = "js_url"
+CONF_JVC = "jvc"
+CONF_KEEP_ON_TIME = "keep_on_time"
+CONF_KEEPALIVE = "keepalive"
+CONF_KEY = "key"
+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"
+CONF_LOGGER = "logger"
+CONF_LOGS = "logs"
+CONF_LONGITUDE = "longitude"
+CONF_LOW = "low"
+CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference"
+CONF_MAC_ADDRESS = "mac_address"
+CONF_MAINS_FILTER = "mains_filter"
+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"
+CONF_MAX_VALUE = "max_value"
+CONF_MAX_VOLTAGE = "max_voltage"
+CONF_MEASUREMENT_DURATION = "measurement_duration"
+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"
+CONF_MOSI_PIN = "mosi_pin"
+CONF_MOTION = "motion"
+CONF_MOVEMENT_COUNTER = "movement_counter"
+CONF_MQTT = "mqtt"
+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"
+CONF_NEW_PASSWORD = "new_password"
+CONF_NOISE_LEVEL = "noise_level"
+CONF_NUM_ATTEMPTS = "num_attempts"
+CONF_NUM_CHANNELS = "num_channels"
+CONF_NUM_CHIPS = "num_chips"
+CONF_NUM_LEDS = "num_leds"
+CONF_NUM_SCANS = "num_scans"
+CONF_NUMBER = "number"
+CONF_NUMBER_DATAPOINT = "number_datapoint"
+CONF_OFF_MODE = "off_mode"
+CONF_OFFSET = "offset"
+CONF_ON = "on"
+CONF_ON_BLE_ADVERTISE = "on_ble_advertise"
+CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise"
+CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise"
+CONF_ON_BOOT = "on_boot"
+CONF_ON_CLICK = "on_click"
+CONF_ON_CONNECT = "on_connect"
+CONF_ON_DISCONNECT = "on_disconnect"
+CONF_ON_DOUBLE_CLICK = "on_double_click"
+CONF_ON_ENROLLMENT_DONE = "on_enrollment_done"
+CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed"
+CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan"
+CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched"
+CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched"
+CONF_ON_JSON_MESSAGE = "on_json_message"
+CONF_ON_LOOP = "on_loop"
+CONF_ON_MESSAGE = "on_message"
+CONF_ON_MULTI_CLICK = "on_multi_click"
+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"
+CONF_ON_TIME = "on_time"
+CONF_ON_TIME_SYNC = "on_time_sync"
+CONF_ON_TURN_OFF = "on_turn_off"
+CONF_ON_TURN_ON = "on_turn_on"
+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"
+CONF_OSCILLATION_OUTPUT = "oscillation_output"
+CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
+CONF_OTA = "ota"
+CONF_OUTPUT = "output"
+CONF_OUTPUT_ID = "output_id"
+CONF_OUTPUTS = "outputs"
+CONF_OVERSAMPLING = "oversampling"
+CONF_PACKAGES = "packages"
+CONF_PAGE_ID = "page_id"
+CONF_PAGES = "pages"
+CONF_PANASONIC = "panasonic"
+CONF_PASSWORD = "password"
+CONF_PATH = "path"
+CONF_PAYLOAD = "payload"
+CONF_PAYLOAD_AVAILABLE = "payload_available"
+CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
+CONF_PERIOD = "period"
+CONF_PHASE_ANGLE = "phase_angle"
+CONF_PHASE_BALANCER = "phase_balancer"
+CONF_PIN = "pin"
+CONF_PIN_A = "pin_a"
+CONF_PIN_B = "pin_b"
+CONF_PIN_C = "pin_c"
+CONF_PIN_D = "pin_d"
+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"
+CONF_PMC_10_0 = "pmc_10_0"
+CONF_PMC_2_5 = "pmc_2_5"
+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"
+CONF_POWER_SAVE_MODE = "power_save_mode"
+CONF_POWER_SUPPLY = "power_supply"
+CONF_PRESET = "preset"
+CONF_PRESET_BOOST = "preset_boost"
+CONF_PRESET_ECO = "preset_eco"
+CONF_PRESET_SLEEP = "preset_sleep"
+CONF_PRESSURE = "pressure"
+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_QUANTILE = "quantile"
+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_DURATION = "reset_duration"
+CONF_RESET_PIN = "reset_pin"
+CONF_RESIZE = "resize"
+CONF_RESOLUTION = "resolution"
+CONF_RESTORE = "restore"
+CONF_RESTORE_MODE = "restore_mode"
+CONF_RESTORE_STATE = "restore_state"
+CONF_RESTORE_VALUE = "restore_value"
+CONF_RETAIN = "retain"
+CONF_REVERSE_ACTIVE_ENERGY = "reverse_active_energy"
+CONF_REVERSED = "reversed"
+CONF_RGB_ORDER = "rgb_order"
+CONF_RGBW = "rgbw"
+CONF_RISING_EDGE = "rising_edge"
+CONF_ROTATION = "rotation"
+CONF_RS_PIN = "rs_pin"
+CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance"
+CONF_RTD_WIRES = "rtd_wires"
+CONF_RUN_DURATION = "run_duration"
+CONF_RW_PIN = "rw_pin"
+CONF_RX_BUFFER_SIZE = "rx_buffer_size"
+CONF_RX_ONLY = "rx_only"
+CONF_RX_PIN = "rx_pin"
+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"
+CONF_SDO_PIN = "sdo_pin"
+CONF_SECOND = "second"
+CONF_SECONDS = "seconds"
+CONF_SECURITY_LEVEL = "security_level"
+CONF_SEGMENTS = "segments"
+CONF_SEL_PIN = "sel_pin"
+CONF_SEND_EVERY = "send_every"
+CONF_SEND_FIRST_AT = "send_first_at"
+CONF_SENSING_PIN = "sensing_pin"
+CONF_SENSOR = "sensor"
+CONF_SENSOR_DATAPOINT = "sensor_datapoint"
+CONF_SENSOR_ID = "sensor_id"
+CONF_SENSORS = "sensors"
+CONF_SEQUENCE = "sequence"
+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"
+CONF_SLEEP_WHEN_DONE = "sleep_when_done"
+CONF_SONY = "sony"
+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"
+CONF_SWITCHES = "switches"
+CONF_SYNC = "sync"
+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"
+CONF_THEN = "then"
+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"
+CONF_TIMES = "times"
+CONF_TIMEZONE = "timezone"
+CONF_TIMING = "timing"
+CONF_TO = "to"
+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"
+CONF_TURN_OFF_ACTION = "turn_off_action"
+CONF_TURN_ON_ACTION = "turn_on_action"
+CONF_TVOC = "tvoc"
+CONF_TX_BUFFER_SIZE = "tx_buffer_size"
+CONF_TX_PIN = "tx_pin"
+CONF_TX_POWER = "tx_power"
+CONF_TYPE = "type"
+CONF_TYPE_ID = "type_id"
+CONF_UART_ID = "uart_id"
+CONF_UID = "uid"
+CONF_UNIQUE = "unique"
+CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement"
+CONF_UPDATE_INTERVAL = "update_interval"
+CONF_UPDATE_ON_BOOT = "update_on_boot"
+CONF_URL = "url"
+CONF_USE_ABBREVIATIONS = "use_abbreviations"
+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"
+CONF_VOLTAGE_DIVIDER = "voltage_divider"
+CONF_WAIT_TIME = "wait_time"
+CONF_WAIT_UNTIL = "wait_until"
+CONF_WAKEUP_PIN = "wakeup_pin"
+CONF_WARM_WHITE = "warm_white"
+CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature"
+CONF_WATCHDOG_THRESHOLD = "watchdog_threshold"
+CONF_WEIGHT = "weight"
+CONF_WHILE = "while"
+CONF_WHITE = "white"
+CONF_WIDTH = "width"
+CONF_WIFI = "wifi"
+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"
+ENV_QUICKWIZARD = "ESPHOME_QUICKWIZARD"
+
+ICON_ACCELERATION = "mdi:axis-arrow"
+ICON_ACCELERATION_X = "mdi:axis-x-arrow"
+ICON_ACCELERATION_Y = "mdi:axis-y-arrow"
+ICON_ACCELERATION_Z = "mdi:axis-z-arrow"
+ICON_ACCOUNT = "mdi:account"
+ICON_ACCOUNT_CHECK = "mdi:account-check"
+ICON_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"
+ICON_COUNTER = "mdi:counter"
+ICON_CURRENT_AC = "mdi:current-ac"
+ICON_DATABASE = "mdi:database"
+ICON_EMPTY = ""
+ICON_FINGERPRINT = "mdi:fingerprint"
+ICON_FLASH = "mdi:flash"
+ICON_FLASK = "mdi:flask"
+ICON_FLASK_OUTLINE = "mdi:flask-outline"
+ICON_FLOWER = "mdi:flower"
+ICON_GAS_CYLINDER = "mdi:gas-cylinder"
+ICON_GAUGE = "mdi:gauge"
+ICON_GRAIN = "mdi:grain"
+ICON_KEY_PLUS = "mdi:key-plus"
+ICON_LIGHTBULB = "mdi:lightbulb"
+ICON_MAGNET = "mdi:magnet"
+ICON_MOLECULE_CO2 = "mdi:molecule-co2"
+ICON_MOTION_SENSOR = "mdi:motion-sensor"
+ICON_NEW_BOX = "mdi:new-box"
+ICON_OMEGA = "mdi:omega"
+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"
+ICON_SCALE_BATHROOM = "mdi:scale-bathroom"
+ICON_SCREEN_ROTATION = "mdi:screen-rotation"
+ICON_SECURITY = "mdi:security"
+ICON_SIGN_DIRECTION = "mdi:sign-direction"
+ICON_SIGNAL = "mdi:signal-distance-variant"
+ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal"
+ICON_THERMOMETER = "mdi:thermometer"
+ICON_TIMELAPSE = "mdi:timelapse"
+ICON_TIMER = "mdi:timer-outline"
+ICON_WATER_PERCENT = "mdi:water-percent"
+ICON_WEATHER_SUNSET = "mdi:weather-sunset"
+ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down"
+ICON_WEATHER_SUNSET_UP = "mdi:weather-sunset-up"
+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"
+UNIT_DECIBEL_MILLIWATT = "dBm"
+UNIT_DEGREE_PER_SECOND = "°/s"
+UNIT_DEGREES = "°"
+UNIT_EMPTY = ""
+UNIT_G = "G"
+UNIT_HECTOPASCAL = "hPa"
+UNIT_HERTZ = "Hz"
+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²"
+UNIT_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
+UNIT_MICROMETER = "µm"
+UNIT_MICROSIEMENS_PER_CENTIMETER = "µS/cm"
+UNIT_MICROTESLA = "µT"
+UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³"
+UNIT_MINUTE = "min"
+UNIT_OHM = "Ω"
+UNIT_PARTS_PER_BILLION = "ppb"
+UNIT_PARTS_PER_MILLION = "ppm"
+UNIT_PASCAL = "Pa"
+UNIT_PERCENT = "%"
+UNIT_PULSES = "pulses"
+UNIT_PULSES_PER_MINUTE = "pulses/min"
+UNIT_SECOND = "s"
+UNIT_STEPS = "steps"
+UNIT_VOLT = "V"
+UNIT_VOLT_AMPS = "VA"
+UNIT_VOLT_AMPS_REACTIVE = "VAR"
+UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh"
+UNIT_WATT = "W"
+UNIT_WATT_HOURS = "Wh"
+
+# device classes of binary_sensor component
+DEVICE_CLASS_BATTERY_CHARGING = "battery_charging"
+DEVICE_CLASS_COLD = "cold"
+DEVICE_CLASS_CONNECTIVITY = "connectivity"
+DEVICE_CLASS_DOOR = "door"
+DEVICE_CLASS_GARAGE_DOOR = "garage_door"
+DEVICE_CLASS_HEAT = "heat"
+DEVICE_CLASS_LIGHT = "light"
+DEVICE_CLASS_LOCK = "lock"
+DEVICE_CLASS_MOISTURE = "moisture"
+DEVICE_CLASS_MOTION = "motion"
+DEVICE_CLASS_MOVING = "moving"
+DEVICE_CLASS_OCCUPANCY = "occupancy"
+DEVICE_CLASS_OPENING = "opening"
+DEVICE_CLASS_PLUG = "plug"
+DEVICE_CLASS_PRESENCE = "presence"
+DEVICE_CLASS_PROBLEM = "problem"
+DEVICE_CLASS_RUNNING = "running"
+DEVICE_CLASS_SAFETY = "safety"
+DEVICE_CLASS_SMOKE = "smoke"
+DEVICE_CLASS_SOUND = "sound"
+DEVICE_CLASS_TAMPER = "tamper"
+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_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_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"
+# device classes of both binary_sensor and button component
+DEVICE_CLASS_UPDATE = "update"
+# device classes of button component
+DEVICE_CLASS_RESTART = "restart"
+
+
+# state classes
+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"
+
+# Entity categories
+ENTITY_CATEGORY_NONE = ""
+
+# The entity category for configuration values/controls
+ENTITY_CATEGORY_CONFIG = "config"
+
+# The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address
+ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic"
diff --git a/esphome/core.py b/esphome/core/__init__.py
similarity index 60%
rename from esphome/core.py
rename to esphome/core/__init__.py
index 4b1d3b2115..addecf1326 100644
--- a/esphome/core.py
+++ b/esphome/core/__init__.py
@@ -1,20 +1,32 @@
-import functools
-import heapq
-import inspect
 import logging
-
 import math
 import os
 import re
+from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
 
-# pylint: disable=unused-import, wrong-import-order
-from typing import Any, Dict, List, Optional, Set  # noqa
+from esphome.const import (
+    CONF_COMMENT,
+    CONF_ESPHOME,
+    CONF_USE_ADDRESS,
+    CONF_ETHERNET,
+    CONF_WIFI,
+    CONF_PORT,
+    KEY_CORE,
+    KEY_TARGET_FRAMEWORK,
+    KEY_TARGET_PLATFORM,
+)
+from esphome.coroutine import FakeAwaitable as _FakeAwaitable
+from esphome.coroutine import FakeEventLoop as _FakeEventLoop
 
-from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \
-    CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI
+# pylint: disable=unused-import
+from esphome.coroutine import coroutine, coroutine_with_priority  # noqa
 from esphome.helpers import ensure_unique_string, is_hassio
 from esphome.util import OrderedDict
 
+if TYPE_CHECKING:
+    from ..cpp_generator import MockObj, MockObjClass, Statement
+    from ..types import ConfigType
+
 _LOGGER = logging.getLogger(__name__)
 
 
@@ -24,19 +36,22 @@ class EsphomeError(Exception):
 
 class HexInt(int):
     def __str__(self):
-        if 0 <= self <= 255:
-            return f"0x{self:02X}"
-        return f"0x{self:X}"
+        value = self
+        sign = "-" if value < 0 else ""
+        value = abs(value)
+        if 0 <= value <= 255:
+            return f"{sign}0x{value:02X}"
+        return f"{sign}0x{value:X}"
 
 
 class IPAddress:
     def __init__(self, *args):
         if len(args) != 4:
-            raise ValueError("IPAddress must consist up 4 items")
+            raise ValueError("IPAddress must consist of 4 items")
         self.args = args
 
     def __str__(self):
-        return '.'.join(str(x) for x in self.args)
+        return ".".join(str(x) for x in self.args)
 
 
 class MACAddress:
@@ -46,14 +61,14 @@ class MACAddress:
         self.parts = parts
 
     def __str__(self):
-        return ':'.join(f'{part:02X}' for part in self.parts)
+        return ":".join(f"{part:02X}" for part in self.parts)
 
     @property
     def as_hex(self):
         from esphome.cpp_generator import RawExpression
 
-        num = ''.join(f'{part:02X}' for part in self.parts)
-        return RawExpression(f'0x{num}ULL')
+        num = "".join(f"{part:02X}" for part in self.parts)
+        return RawExpression(f"0x{num}ULL")
 
 
 def is_approximately_integer(value):
@@ -63,8 +78,15 @@ def is_approximately_integer(value):
 
 
 class TimePeriod:
-    def __init__(self, microseconds=None, milliseconds=None, seconds=None,
-                 minutes=None, hours=None, days=None):
+    def __init__(
+        self,
+        microseconds=None,
+        milliseconds=None,
+        seconds=None,
+        minutes=None,
+        hours=None,
+        days=None,
+    ):
         if days is not None:
             if not is_approximately_integer(days):
                 frac_days, days = math.modf(days)
@@ -115,33 +137,33 @@ class TimePeriod:
     def as_dict(self):
         out = OrderedDict()
         if self.microseconds is not None:
-            out['microseconds'] = self.microseconds
+            out["microseconds"] = self.microseconds
         if self.milliseconds is not None:
-            out['milliseconds'] = self.milliseconds
+            out["milliseconds"] = self.milliseconds
         if self.seconds is not None:
-            out['seconds'] = self.seconds
+            out["seconds"] = self.seconds
         if self.minutes is not None:
-            out['minutes'] = self.minutes
+            out["minutes"] = self.minutes
         if self.hours is not None:
-            out['hours'] = self.hours
+            out["hours"] = self.hours
         if self.days is not None:
-            out['days'] = self.days
+            out["days"] = self.days
         return out
 
     def __str__(self):
         if self.microseconds is not None:
-            return f'{self.total_microseconds}us'
+            return f"{self.total_microseconds}us"
         if self.milliseconds is not None:
-            return f'{self.total_milliseconds}ms'
+            return f"{self.total_milliseconds}ms"
         if self.seconds is not None:
-            return f'{self.total_seconds}s'
+            return f"{self.total_seconds}s"
         if self.minutes is not None:
-            return f'{self.total_minutes}min'
+            return f"{self.total_minutes}min"
         if self.hours is not None:
-            return f'{self.total_hours}h'
+            return f"{self.total_hours}h"
         if self.days is not None:
-            return f'{self.total_days}d'
-        return '0s'
+            return f"{self.total_days}d"
+        return "0s"
 
     def __repr__(self):
         return f"TimePeriod<{self.total_microseconds}>"
@@ -217,7 +239,7 @@ class TimePeriodMinutes(TimePeriod):
     pass
 
 
-LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)')
+LAMBDA_PROG = re.compile(r"id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)")
 
 
 class Lambda:
@@ -234,12 +256,13 @@ class Lambda:
     def comment_remover(self, text):
         def replacer(match):
             s = match.group(0)
-            if s.startswith('/'):
+            if s.startswith("/"):
                 return " "  # note: a space and not an empty string
             return s
+
         pattern = re.compile(
             r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
-            re.DOTALL | re.MULTILINE
+            re.DOTALL | re.MULTILINE,
         )
         return re.sub(pattern, replacer, text)
 
@@ -252,7 +275,9 @@ class Lambda:
     @property
     def requires_ids(self):
         if self._requires_ids is None:
-            self._requires_ids = [ID(self.parts[i]) for i in range(1, len(self.parts), 3)]
+            self._requires_ids = [
+                ID(self.parts[i]) for i in range(1, len(self.parts), 3)
+            ]
         return self._requires_ids
 
     @property
@@ -269,7 +294,7 @@ class Lambda:
         return self.value
 
     def __repr__(self):
-        return f'Lambda<{self.value}>'
+        return f"Lambda<{self.value}>"
 
 
 class ID:
@@ -280,26 +305,28 @@ class ID:
         else:
             self.is_manual = is_manual
         self.is_declaration = is_declaration
-        self.type: Optional['MockObjClass'] = type
+        self.type: Optional["MockObjClass"] = type
 
     def resolve(self, registered_ids):
         from esphome.config_validation import RESERVED_IDS
 
         if self.id is None:
-            base = str(self.type).replace('::', '_').lower()
-            name = ''.join(c for c in base if c.isalnum() or c == '_')
-            used = set(registered_ids) | set(RESERVED_IDS)
+            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) | CORE.loaded_integrations
             self.id = ensure_unique_string(name, used)
         return self.id
 
     def __str__(self):
         if self.id is None:
-            return ''
+            return ""
         return self.id
 
     def __repr__(self):
-        return (f'ID<{self.id} declaration={self.is_declaration}, '
-                f'type={self.type}, manual={self.is_manual}>')
+        return (
+            f"ID<{self.id} declaration={self.is_declaration}, "
+            f"type={self.type}, manual={self.is_manual}>"
+        )
 
     def __eq__(self, other):
         if isinstance(other, ID):
@@ -310,8 +337,12 @@ class ID:
         return hash(self.id)
 
     def copy(self):
-        return ID(self.id, is_declaration=self.is_declaration, type=self.type,
-                  is_manual=self.is_manual)
+        return ID(
+            self.id,
+            is_declaration=self.is_declaration,
+            type=self.type,
+            is_manual=self.is_manual,
+        )
 
 
 class DocumentLocation:
@@ -322,14 +353,15 @@ class DocumentLocation:
 
     @classmethod
     def from_mark(cls, mark):
-        return cls(
-            mark.name,
-            mark.line,
-            mark.column
-        )
+        return cls(mark.name, mark.line, mark.column)
 
     def __str__(self):
-        return f'{self.document} {self.line}:{self.column}'
+        return f"{self.document} {self.line}:{self.column}"
+
+    @property
+    def as_line_directive(self):
+        document_path = str(self.document).replace("\\", "\\\\")
+        return f'#line {self.line + 1} "{document_path}"'
 
 
 class DocumentRange:
@@ -340,12 +372,11 @@ class DocumentRange:
     @classmethod
     def from_marks(cls, start_mark, end_mark):
         return cls(
-            DocumentLocation.from_mark(start_mark),
-            DocumentLocation.from_mark(end_mark)
+            DocumentLocation.from_mark(start_mark), DocumentLocation.from_mark(end_mark)
         )
 
     def __str__(self):
-        return f'[{self.start_mark} - {self.end_mark}]'
+        return f"[{self.start_mark} - {self.end_mark}]"
 
 
 class Define:
@@ -356,14 +387,14 @@ class Define:
     @property
     def as_build_flag(self):
         if self.value is None:
-            return f'-D{self.name}'
-        return f'-D{self.name}={self.value}'
+            return f"-D{self.name}"
+        return f"-D{self.name}={self.value}"
 
     @property
     def as_macro(self):
         if self.value is None:
-            return f'#define {self.name}'
-        return f'#define {self.name} {self.value}'
+            return f"#define {self.name}"
+        return f"#define {self.name} {self.value}"
 
     @property
     def as_tuple(self):
@@ -379,19 +410,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}'
+        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)
@@ -402,75 +442,6 @@ class Library:
         return NotImplemented
 
 
-def coroutine(func):
-    return coroutine_with_priority(0.0)(func)
-
-
-def coroutine_with_priority(priority):
-    def decorator(func):
-        if getattr(func, '_esphome_coroutine', False):
-            # If func is already a coroutine, do not re-wrap it (performance)
-            return func
-
-        @functools.wraps(func)
-        def _wrapper_generator(*args, **kwargs):
-            instance_id = kwargs.pop('__esphome_coroutine_instance__')
-            if not inspect.isgeneratorfunction(func):
-                # If func is not a generator, return result immediately
-                yield func(*args, **kwargs)
-                # pylint: disable=protected-access
-                CORE._remove_coroutine(instance_id)
-                return
-            gen = func(*args, **kwargs)
-            var = None
-            try:
-                while True:
-                    var = gen.send(var)
-                    if inspect.isgenerator(var):
-                        # Yielded generator, equivalent to 'yield from'
-                        x = None
-                        for x in var:
-                            yield None
-                        # Last yield value is the result
-                        var = x
-                    else:
-                        yield var
-            except StopIteration:
-                # Stopping iteration
-                yield var
-            # pylint: disable=protected-access
-            CORE._remove_coroutine(instance_id)
-
-        @functools.wraps(func)
-        def wrapper(*args, **kwargs):
-            import random
-            instance_id = random.randint(0, 2**32)
-            kwargs['__esphome_coroutine_instance__'] = instance_id
-            gen = _wrapper_generator(*args, **kwargs)
-            # pylint: disable=protected-access
-            CORE._add_active_coroutine(instance_id, gen)
-            return gen
-
-        # pylint: disable=protected-access
-        wrapper._esphome_coroutine = True
-        wrapper.priority = priority
-        return wrapper
-    return decorator
-
-
-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):
@@ -481,39 +452,35 @@ 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: ConfigType = {}
         # The validated configuration, this is None until the config has been validated
-        self.config: ConfigType = {}
+        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)
-        self.pending_tasks = []
+        self.event_loop = _FakeEventLoop()
         # Task counter for pending tasks
         self.task_counter = 0
         # The variable cache, for each ID this holds a MockObj of the variable obj
-        self.variables: Dict[str, 'MockObj'] = {}
+        self.variables: Dict[str, "MockObj"] = {}
         # A list of statements that go in the main setup() block
-        self.main_statements: List['Statement'] = []
+        self.main_statements: List["Statement"] = []
         # A list of statements to insert in the global block (includes and global variables)
-        self.global_statements: List['Statement'] = []
+        self.global_statements: List["Statement"] = []
         # A set of platformio libraries to add to the project
         self.libraries: List[Library] = []
         # A set of build flags to set in the platformio project
         self.build_flags: Set[str] = set()
         # A set of defines to set for the compile process in esphome/core/defines.h
-        self.defines: Set['Define'] = set()
-        # A dictionary of started coroutines, used to warn when a coroutine was not
-        # awaited.
-        self.active_coroutines: Dict[int, Any] = {}
+        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
@@ -524,13 +491,11 @@ 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.pending_tasks = []
+        self.event_loop = _FakeEventLoop()
         self.task_counter = 0
         self.variables = {}
         self.main_statements = []
@@ -538,37 +503,46 @@ class EsphomeCore:
         self.libraries = []
         self.build_flags = set()
         self.defines = set()
-        self.active_coroutines = {}
+        self.platformio_options = {}
         self.loaded_integrations = set()
         self.component_ids = set()
 
     @property
     def address(self) -> Optional[str]:
-        if 'wifi' in self.config:
+        if self.config is None:
+            raise ValueError("Config has not been loaded yet")
+
+        if "wifi" in self.config:
             return self.config[CONF_WIFI][CONF_USE_ADDRESS]
 
-        if 'ethernet' in self.config:
-            return self.config['ethernet'][CONF_USE_ADDRESS]
+        if CONF_ETHERNET in self.config:
+            return self.config[CONF_ETHERNET][CONF_USE_ADDRESS]
+
+        return None
+
+    @property
+    def web_port(self) -> Optional[int]:
+        if self.config is None:
+            raise ValueError("Config has not been loaded yet")
+
+        if "web_server" in self.config:
+            try:
+                return self.config["web_server"][CONF_PORT]
+            except KeyError:
+                return 80
 
         return None
 
     @property
     def comment(self) -> Optional[str]:
+        if self.config is None:
+            raise ValueError("Config has not been loaded yet")
+
         if CONF_COMMENT in self.config[CONF_ESPHOME]:
             return self.config[CONF_ESPHOME][CONF_COMMENT]
 
         return None
 
-    def _add_active_coroutine(self, instance_id, obj):
-        self.active_coroutines[instance_id] = obj
-
-    def _remove_coroutine(self, instance_id):
-        self.active_coroutines.pop(instance_id)
-
-    @property
-    def arduino_version(self) -> str:
-        return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION]
-
     @property
     def config_dir(self):
         return os.path.dirname(self.config_path)
@@ -578,84 +552,67 @@ class EsphomeCore:
         return os.path.basename(self.config_path)
 
     def relative_config_path(self, *path):
+        # pylint: disable=no-value-for-parameter
         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))
         return os.path.join(self.build_path, path_)
 
     def relative_src_path(self, *path):
-        return self.relative_build_path('src', *path)
+        return self.relative_build_path("src", *path)
 
     def relative_pioenvs_path(self, *path):
         if is_hassio():
-            return os.path.join('/data', self.name, '.pioenvs', *path)
-        return self.relative_build_path('.pioenvs', *path)
+            return os.path.join("/data", self.name, ".pioenvs", *path)
+        return self.relative_build_path(".pioenvs", *path)
 
     def relative_piolibdeps_path(self, *path):
         if is_hassio():
-            return os.path.join('/data', self.name, '.piolibdeps', *path)
-        return self.relative_build_path('.piolibdeps', *path)
+            return os.path.join("/data", self.name, ".piolibdeps", *path)
+        return self.relative_build_path(".piolibdeps", *path)
 
     @property
     def firmware_bin(self):
-        return self.relative_pioenvs_path(self.name, 'firmware.bin')
+        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):
-        coro = coroutine(func)
-        task = coro(*args, **kwargs)
-        item = (-coro.priority, self.task_counter, task)
-        self.task_counter += 1
-        heapq.heappush(self.pending_tasks, item)
-        return task
+        self.event_loop.add_job(func, *args, **kwargs)
 
     def flush_tasks(self):
-        i = 0
-        while self.pending_tasks:
-            i += 1
-            if i > 1000000:
-                raise EsphomeError("Circular dependency detected!")
-
-            inv_priority, num, task = heapq.heappop(self.pending_tasks)
-            priority = -inv_priority
-            _LOGGER.debug("Running %s (num %s)", task, num)
-            try:
-                next(task)
-                # Decrease priority over time, so that if this task is blocked
-                # due to a dependency others will clear the dependency
-                # This could be improved with a less naive approach
-                priority -= 1
-                item = (-priority, num, task)
-                heapq.heappush(self.pending_tasks, item)
-            except StopIteration:
-                _LOGGER.debug(" -> finished")
-
-        # Print not-awaited coroutines
-        for obj in self.active_coroutines.values():
-            _LOGGER.warning("Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj)
-            _LOGGER.warning("Please file a bug report with your configuration.")
-        if self.active_coroutines:
-            raise EsphomeError()
-        if self.component_ids:
-            comps = ', '.join(f"'{x}'" for x in self.component_ids)
-            _LOGGER.warning("Components %s were never registered. Please create a bug report",
-                            comps)
-            _LOGGER.warning("with your configuration.")
-            raise EsphomeError()
-        self.active_coroutines.clear()
+        try:
+            self.event_loop.flush_tasks()
+        except RuntimeError as e:
+            raise EsphomeError(str(e)) from e
 
     def add(self, expression):
         from esphome.cpp_generator import Expression, Statement, statement
@@ -663,8 +620,9 @@ class EsphomeCore:
         if isinstance(expression, Expression):
             expression = statement(expression)
         if not isinstance(expression, Statement):
-            raise ValueError("Add '{}' must be expression or statement, not {}"
-                             "".format(expression, type(expression)))
+            raise ValueError(
+                f"Add '{expression}' must be expression or statement, not {type(expression)}"
+            )
 
         self.main_statements.append(expression)
         _LOGGER.debug("Adding: %s", expression)
@@ -676,20 +634,35 @@ class EsphomeCore:
         if isinstance(expression, Expression):
             expression = statement(expression)
         if not isinstance(expression, Statement):
-            raise ValueError("Add '{}' must be expression or statement, not {}"
-                             "".format(expression, type(expression)))
+            raise ValueError(
+                f"Add '{expression}' must be expression or statement, not {type(expression)}"
+            )
         self.global_statements.append(expression)
         _LOGGER.debug("Adding global: %s", expression)
         return expression
 
     def add_library(self, library):
         if not isinstance(library, Library):
-            raise ValueError("Library {} must be instance of Library, not {}"
-                             "".format(library, type(library)))
-        _LOGGER.debug("Adding library: %s", library)
+            raise ValueError(
+                f"Library {library} must be instance of Library, not {type(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
@@ -700,10 +673,12 @@ class EsphomeCore:
             if other.version == library.version:
                 break
 
-            raise ValueError("Version pinning failed! Libraries {} and {} "
-                             "requested with conflicting versions!"
-                             "".format(library, other))
+            raise ValueError(
+                f"Version pinning failed! Libraries {library} and {other} "
+                "requested with conflicting versions!"
+            )
         else:
+            _LOGGER.debug("Adding library: %s", library)
             self.libraries.append(library)
         return library
 
@@ -718,31 +693,50 @@ class EsphomeCore:
         elif isinstance(define, Define):
             pass
         else:
-            raise ValueError("Define {} must be string or Define, not {}"
-                             "".format(define, type(define)))
+            raise ValueError(
+                f"Define {define} must be string or Define, not {type(define)}"
+            )
         self.defines.add(define)
         _LOGGER.debug("Adding define: %s", define)
         return define
 
-    def get_variable(self, id):
+    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:
+                return self.variables[id]
+            except KeyError:
+                _LOGGER.debug("Waiting for variable %s (%r)", id, id)
+                yield
+
+    async def get_variable(self, id) -> "MockObj":
         if not isinstance(id, ID):
             raise ValueError(f"ID {id!r} must be of type ID!")
-        while True:
-            if id in self.variables:
-                yield self.variables[id]
-                return
-            _LOGGER.debug("Waiting for variable %s (%r)", id, id)
-            yield None
+        # Fast path, check if already registered without awaiting
+        if id in self.variables:
+            return self.variables[id]
+        return await _FakeAwaitable(self._get_variable_generator(id))
 
-    def get_variable_with_full_id(self, id):
+    def _get_variable_with_full_id_generator(self, id):
         while True:
             if id in self.variables:
                 for k, v in self.variables.items():
                     if k == id:
-                        yield (k, v)
-                        return
+                        return (k, v)
             _LOGGER.debug("Waiting for variable %s", id)
-            yield None, None
+            yield
+
+    async def get_variable_with_full_id(self, id: ID) -> Tuple[ID, "MockObj"]:
+        if not isinstance(id, ID):
+            raise ValueError(f"ID {id!r} must be of type ID!")
+        return await _FakeAwaitable(self._get_variable_with_full_id_generator(id))
 
     def register_variable(self, id, obj):
         if id in self.variables:
@@ -762,7 +756,7 @@ class EsphomeCore:
             text = str(statement(exp))
             text = text.rstrip()
             main_code.append(text)
-        return '\n'.join(main_code) + '\n\n'
+        return "\n".join(main_code) + "\n\n"
 
     @property
     def cpp_global_section(self):
@@ -773,7 +767,7 @@ class EsphomeCore:
             text = str(statement(exp))
             text = text.rstrip()
             global_code.append(text)
-        return '\n'.join(global_code) + '\n'
+        return "\n".join(global_code) + "\n"
 
 
 class AutoLoad(OrderedDict):
@@ -782,16 +776,14 @@ class AutoLoad(OrderedDict):
 
 class EnumValue:
     """Special type used by ESPHome to mark enum values for cv.enum."""
+
     @property
     def enum_value(self):
-        return getattr(self, '_enum_value', None)
+        return getattr(self, "_enum_value", None)
 
     @enum_value.setter
     def enum_value(self, value):
-        setattr(self, '_enum_value', value)
+        setattr(self, "_enum_value", value)
 
 
 CORE = EsphomeCore()
-
-ConfigType = Dict[str, Any]
-CoreType = EsphomeCore
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index 4ecb247ec3..a423397453 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -1,6 +1,7 @@
 #include "esphome/core/application.h"
 #include "esphome/core/log.h"
 #include "esphome/core/version.h"
+#include "esphome/core/hal.h"
 
 #ifdef USE_STATUS_LED
 #include "esphome/components/status_led/status_led.h"
@@ -8,7 +9,7 @@
 
 namespace esphome {
 
-static const char *TAG = "app";
+static const char *const TAG = "app";
 
 void Application::register_component_(Component *comp) {
   if (comp == nullptr) {
@@ -18,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;
     }
   }
@@ -36,6 +37,7 @@ void Application::setup() {
 
     component->call();
     this->scheduler.process_to_add();
+    this->feed_wdt();
     if (component->can_proceed())
       continue;
 
@@ -45,10 +47,12 @@ void Application::setup() {
     do {
       uint32_t new_app_state = STATUS_LED_WARNING;
       this->scheduler.call();
+      this->feed_wdt();
       for (uint32_t j = 0; j <= i; j++) {
         this->components_[j]->call();
         new_app_state |= this->components_[j]->get_component_state();
         this->app_state_ |= new_app_state;
+        this->feed_wdt();
       }
       this->app_state_ = new_app_state;
       yield();
@@ -61,23 +65,20 @@ void Application::setup() {
 }
 void Application::loop() {
   uint32_t new_app_state = 0;
-  const uint32_t start = millis();
 
   this->scheduler.call();
+  this->feed_wdt();
   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()) {
@@ -96,27 +97,25 @@ void Application::loop() {
   }
   this->last_loop_ = now;
 
-  if (this->dump_config_at_ >= 0 && this->dump_config_at_ < this->components_.size()) {
+  if (this->dump_config_at_ < this->components_.size()) {
     if (this->dump_config_at_ == 0) {
       ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
+#ifdef ESPHOME_PROJECT_NAME
+      ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
+#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;
-  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;
+void IRAM_ATTR HOT Application::feed_wdt() {
+  static uint32_t last_feed = 0;
+  uint32_t now = micros();
+  if (now - last_feed > 3000) {
+    arch_feed_wdt();
+    last_feed = now;
 #ifdef USE_STATUS_LED
     if (status_led::global_status_led != nullptr) {
       status_led::global_status_led->call();
@@ -128,11 +127,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...");
@@ -140,11 +135,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_() {
@@ -154,6 +145,6 @@ void Application::calculate_looping_components_() {
   }
 }
 
-Application App;
+Application App;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 }  // namespace esphome
diff --git a/esphome/core/application.h b/esphome/core/application.h
index 3c293e6c8f..2a20793c19 100644
--- a/esphome/core/application.h
+++ b/esphome/core/application.h
@@ -5,6 +5,7 @@
 #include "esphome/core/defines.h"
 #include "esphome/core/preferences.h"
 #include "esphome/core/component.h"
+#include "esphome/core/hal.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/scheduler.h"
 
@@ -17,6 +18,9 @@
 #ifdef USE_SWITCH
 #include "esphome/components/switch/switch.h"
 #endif
+#ifdef USE_BUTTON
+#include "esphome/components/button/button.h"
+#endif
 #ifdef USE_TEXT_SENSOR
 #include "esphome/components/text_sensor/text_sensor.h"
 #endif
@@ -32,15 +36,26 @@
 #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) {
-    this->name_ = name;
+  void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) {
+    arch_init();
+    this->name_add_mac_suffix_ = name_add_mac_suffix;
+    if (name_add_mac_suffix) {
+      this->name_ = name + "-" + get_mac_address().substr(6);
+    } else {
+      this->name_ = name;
+    }
     this->compilation_time_ = compilation_time;
-    global_preferences.begin();
   }
 
 #ifdef USE_BINARY_SENSOR
@@ -57,6 +72,10 @@ class Application {
   void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); }
 #endif
 
+#ifdef USE_BUTTON
+  void register_button(button::Button *button) { this->buttons_.push_back(button); }
+#endif
+
 #ifdef USE_TEXT_SENSOR
   void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
 #endif
@@ -77,6 +96,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");
@@ -93,6 +120,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.
@@ -147,6 +176,15 @@ class Application {
     return nullptr;
   }
 #endif
+#ifdef USE_BUTTON
+  const std::vector &get_buttons() { return this->buttons_; }
+  button::Button *get_button_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->buttons_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
 #ifdef USE_SENSOR
   const std::vector &get_sensors() { return this->sensors_; }
   sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) {
@@ -201,6 +239,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;
 
@@ -211,6 +267,8 @@ class Application {
 
   void calculate_looping_components_();
 
+  void feed_wdt_arch_();
+
   std::vector components_{};
   std::vector looping_components_{};
 
@@ -220,6 +278,9 @@ class Application {
 #ifdef USE_SWITCH
   std::vector switches_{};
 #endif
+#ifdef USE_BUTTON
+  std::vector buttons_{};
+#endif
 #ifdef USE_SENSOR
   std::vector sensors_{};
 #endif
@@ -238,16 +299,23 @@ 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};
+  size_t dump_config_at_{SIZE_MAX};
   uint32_t app_state_{0};
 };
 
 /// Global storage of Application pointer - only one Application can exist.
-extern Application App;
+extern Application App;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 }  // namespace esphome
diff --git a/esphome/core/automation.h b/esphome/core/automation.h
index cbe96a749e..e5460bef34 100644
--- a/esphome/core/automation.h
+++ b/esphome/core/automation.h
@@ -17,14 +17,50 @@ namespace esphome {
 
 #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
 
-#define TEMPLATABLE_STRING_VALUE_(name) \
- protected: \
-  TemplatableStringValue name##_{}; \
-\
- public: \
-  template void set_##name(V name) { this->name##_ = name; }
+template class TemplatableValue {
+ public:
+  TemplatableValue() : type_(EMPTY) {}
 
-#define TEMPLATABLE_STRING_VALUE(name) TEMPLATABLE_STRING_VALUE_(name)
+  template::value, int> = 0>
+  TemplatableValue(F value) : type_(VALUE), value_(value) {}
+
+  template::value, int> = 0>
+  TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
+
+  bool has_value() { return this->type_ != EMPTY; }
+
+  T value(X... x) {
+    if (this->type_ == LAMBDA) {
+      return this->f_(x...);
+    }
+    // return value also when empty
+    return this->value_;
+  }
+
+  optional optional_value(X... x) {
+    if (!this->has_value()) {
+      return {};
+    }
+    return this->value(x...);
+  }
+
+  T value_or(X... x, T default_value) {
+    if (!this->has_value()) {
+      return default_value;
+    }
+    return this->value(x...);
+  }
+
+ protected:
+  enum {
+    EMPTY,
+    VALUE,
+    LAMBDA,
+  } type_;
+
+  T value_{};
+  std::function f_{};
+};
 
 /** Base class for all automation conditions.
  *
@@ -50,18 +86,22 @@ template class Automation;
 
 template class Trigger {
  public:
+  /// Inform the parent automation that the event has triggered.
   void trigger(Ts... x) {
     if (this->automation_parent_ == nullptr)
       return;
     this->automation_parent_->trigger(x...);
   }
   void set_automation_parent(Automation *automation_parent) { this->automation_parent_ = automation_parent; }
-  void stop() {
+
+  /// Stop any action connected to this trigger.
+  void stop_action() {
     if (this->automation_parent_ == nullptr)
       return;
     this->automation_parent_->stop();
   }
-  bool is_running() {
+  /// Returns true if any action connected to this trigger is running.
+  bool is_action_running() {
     if (this->automation_parent_ == nullptr)
       return false;
     return this->automation_parent_->is_running();
@@ -75,45 +115,67 @@ template class ActionList;
 
 template class Action {
  public:
-  virtual void play(Ts... x) = 0;
   virtual void play_complex(Ts... x) {
+    this->num_running_++;
     this->play(x...);
-    this->play_next(x...);
+    this->play_next_(x...);
   }
-  void play_next(Ts... x) {
-    if (this->next_ != nullptr) {
-      this->next_->play_complex(x...);
-    }
-  }
-  virtual void stop() {}
   virtual void stop_complex() {
-    this->stop();
-    this->stop_next();
-  }
-  void stop_next() {
-    if (this->next_ != nullptr) {
-      this->next_->stop_complex();
+    if (num_running_) {
+      this->stop();
+      this->num_running_ = 0;
     }
+    this->stop_next_();
   }
-  virtual bool is_running() { return this->is_running_next(); }
-  bool is_running_next() {
-    if (this->next_ == nullptr)
-      return false;
-    return this->next_->is_running();
-  }
+  /// Check if this or any of the following actions are currently running.
+  virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); }
 
-  void play_next_tuple(const std::tuple &tuple) {
-    this->play_next_tuple_(tuple, typename gens::type());
+  /// The total number of actions that are currently running in this plus any of
+  /// the following actions in the chain.
+  int num_running_total() {
+    int total = this->num_running_;
+    if (this->next_ != nullptr)
+      total += this->next_->num_running_total();
+    return total;
   }
 
  protected:
   friend ActionList;
 
+  virtual void play(Ts... x) = 0;
+  void play_next_(Ts... x) {
+    if (this->num_running_ > 0) {
+      this->num_running_--;
+      if (this->next_ != nullptr) {
+        this->next_->play_complex(x...);
+      }
+    }
+  }
   template void play_next_tuple_(const std::tuple &tuple, seq) {
-    this->play_next(std::get(tuple)...);
+    this->play_next_(std::get(tuple)...);
+  }
+  void play_next_tuple_(const std::tuple &tuple) {
+    this->play_next_tuple_(tuple, typename gens::type());
+  }
+
+  virtual void stop() {}
+  void stop_next_() {
+    if (this->next_ != nullptr) {
+      this->next_->stop_complex();
+    }
+  }
+
+  bool is_running_next_() {
+    if (this->next_ == nullptr)
+      return false;
+    return this->next_->is_running();
   }
 
   Action *next_ = nullptr;
+
+  /// The number of instances of this sequence in the list of actions
+  /// that is currently being executed.
+  int num_running_{0};
 };
 
 template class ActionList {
@@ -141,11 +203,19 @@ template class ActionList {
       this->actions_begin_->stop_complex();
   }
   bool empty() const { return this->actions_begin_ == nullptr; }
+
+  /// Check if any action in this action list is currently running.
   bool is_running() {
     if (this->actions_begin_ == nullptr)
       return false;
     return this->actions_begin_->is_running();
   }
+  /// Return the number of actions in this action list that are currently running.
+  int num_running() {
+    if (this->actions_begin_ == nullptr)
+      return false;
+    return this->actions_begin_->num_running_total();
+  }
 
  protected:
   template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); }
@@ -167,6 +237,9 @@ template class Automation {
 
   bool is_running() { return this->actions_.is_running(); }
 
+  /// Return the number of actions in the action part of this automation that are currently running.
+  int num_running() { return this->actions_.num_running(); }
+
  protected:
   Trigger *trigger_;
   ActionList actions_;
diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h
index add3df0bb5..e87a4a2765 100644
--- a/esphome/core/base_automation.h
+++ b/esphome/core/base_automation.h
@@ -108,34 +108,23 @@ template class DelayAction : public Action, public Compon
 
   TEMPLATABLE_VALUE(uint32_t, delay)
 
-  void stop() override {
-    this->cancel_timeout("");
-    this->num_running_ = 0;
-  }
-
-  void play(Ts... x) override { /* ignore - see play_complex */
-  }
-
   void play_complex(Ts... x) override {
-    auto f = std::bind(&DelayAction::delay_end_, this, x...);
+    auto f = std::bind(&DelayAction::play_next_, this, x...);
     this->num_running_++;
     this->set_timeout(this->delay_.value(x...), f);
   }
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
 
-  bool is_running() override { return this->num_running_ > 0 || this->is_running_next(); }
-
- protected:
-  void delay_end_(Ts... x) {
-    this->num_running_--;
-    this->play_next(x...);
+  void play(Ts... x) override { /* ignore - see play_complex */
   }
-  int num_running_{0};
+
+  void stop() override { this->cancel_timeout(""); }
 };
 
 template class LambdaAction : public Action {
  public:
   explicit LambdaAction(std::function &&f) : f_(std::move(f)) {}
+
   void play(Ts... x) override { this->f_(x...); }
 
  protected:
@@ -148,41 +137,40 @@ template class IfAction : public Action {
 
   void add_then(const std::vector *> &actions) {
     this->then_.add_actions(actions);
-    this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); }));
+    this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); }));
   }
 
   void add_else(const std::vector *> &actions) {
     this->else_.add_actions(actions);
-    this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); }));
-  }
-
-  void play(Ts... x) override { /* ignore - see play_complex */
+    this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); }));
   }
 
   void play_complex(Ts... x) override {
+    this->num_running_++;
     bool res = this->condition_->check(x...);
     if (res) {
       if (this->then_.empty()) {
-        this->play_next(x...);
-      } else {
+        this->play_next_(x...);
+      } else if (this->num_running_ > 0) {
         this->then_.play(x...);
       }
     } else {
       if (this->else_.empty()) {
-        this->play_next(x...);
-      } else {
+        this->play_next_(x...);
+      } else if (this->num_running_ > 0) {
         this->else_.play(x...);
       }
     }
   }
 
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
   void stop() override {
     this->then_.stop();
     this->else_.stop();
   }
 
-  bool is_running() override { return this->then_.is_running() || this->else_.is_running() || this->is_running_next(); }
-
  protected:
   Condition *condition_;
   ActionList then_;
@@ -196,90 +184,138 @@ template class WhileAction : public Action {
   void add_then(const std::vector *> &actions) {
     this->then_.add_actions(actions);
     this->then_.add_action(new LambdaAction([this](Ts... x) {
-      if (this->condition_->check_tuple(this->var_)) {
+      if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) {
         // play again
-        this->then_.play_tuple(this->var_);
+        if (this->num_running_ > 0) {
+          this->then_.play_tuple(this->var_);
+        }
       } else {
         // condition false, play next
-        this->play_next_tuple(this->var_);
+        this->play_next_tuple_(this->var_);
       }
     }));
   }
 
-  void play(Ts... x) override { /* ignore - see play_complex */
-  }
-
   void play_complex(Ts... x) override {
+    this->num_running_++;
     // Store loop parameters
     this->var_ = std::make_tuple(x...);
     // Initial condition check
     if (!this->condition_->check_tuple(this->var_)) {
       // If new condition check failed, stop loop if running
       this->then_.stop();
-      this->play_next_tuple(this->var_);
+      this->play_next_tuple_(this->var_);
       return;
     }
 
-    this->then_.play_tuple(this->var_);
+    if (this->num_running_ > 0) {
+      this->then_.play_tuple(this->var_);
+    }
+  }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
   }
 
   void stop() override { this->then_.stop(); }
 
-  bool is_running() override { return this->then_.is_running() || this->is_running_next(); }
-
  protected:
   Condition *condition_;
   ActionList then_;
   std::tuple var_{};
 };
 
+template class RepeatAction : public Action {
+ public:
+  TEMPLATABLE_VALUE(uint32_t, count)
+
+  void add_then(const std::vector *> &actions) {
+    this->then_.add_actions(actions);
+    this->then_.add_action(new LambdaAction([this](Ts... x) {
+      this->iteration_++;
+      if (this->iteration_ == this->count_.value(x...))
+        this->play_next_tuple_(this->var_);
+      else
+        this->then_.play_tuple(this->var_);
+    }));
+  }
+
+  void play_complex(Ts... x) override {
+    this->num_running_++;
+    this->var_ = std::make_tuple(x...);
+    this->iteration_ = 0;
+    this->then_.play_tuple(this->var_);
+  }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+  void stop() override { this->then_.stop(); }
+
+ protected:
+  uint32_t iteration_;
+  ActionList then_;
+  std::tuple var_;
+};
+
 template class WaitUntilAction : public Action, public Component {
  public:
   WaitUntilAction(Condition *condition) : condition_(condition) {}
 
-  void play(Ts... x) { /* ignore - see play_complex */
-  }
+  TEMPLATABLE_VALUE(uint32_t, timeout_value)
 
   void play_complex(Ts... x) override {
+    this->num_running_++;
     // Check if we can continue immediately.
     if (this->condition_->check(x...)) {
-      this->triggered_ = false;
-      this->play_next(x...);
+      if (this->num_running_ > 0) {
+        this->play_next_(x...);
+      }
       return;
     }
     this->var_ = std::make_tuple(x...);
-    this->triggered_ = true;
+
+    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();
   }
 
-  void stop() override { this->triggered_ = false; }
-
   void loop() override {
-    if (!this->triggered_)
+    if (this->num_running_ == 0)
       return;
 
     if (!this->condition_->check_tuple(this->var_)) {
       return;
     }
 
-    this->triggered_ = false;
-    this->play_next_tuple(this->var_);
+    this->cancel_timeout("timeout");
+
+    this->play_next_tuple_(this->var_);
   }
 
   float get_setup_priority() const override { return setup_priority::DATA; }
 
-  bool is_running() override { return this->triggered_ || this->is_running_next(); }
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+  void stop() override { this->cancel_timeout("timeout"); }
 
  protected:
   Condition *condition_;
-  bool triggered_{false};
   std::tuple var_{};
 };
 
 template class UpdateComponentAction : public Action {
  public:
   UpdateComponentAction(PollingComponent *component) : component_(component) {}
-  void play(Ts... x) override { this->component_->update(); }
+
+  void play(Ts... x) override {
+    if (this->component_->is_failed())
+      return;
+    this->component_->update();
+  }
 
  protected:
   PollingComponent *component_;
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
new file mode 100644
index 0000000000..c9ca3bcfc3
--- /dev/null
+++ b/esphome/core/color.h
@@ -0,0 +1,156 @@
+#pragma once
+
+#include "component.h"
+#include "helpers.h"
+
+namespace esphome {
+
+inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; }
+
+struct Color {
+  union {
+    struct {
+      union {
+        uint8_t r;
+        uint8_t red;
+      };
+      union {
+        uint8_t g;
+        uint8_t green;
+      };
+      union {
+        uint8_t b;
+        uint8_t blue;
+      };
+      union {
+        uint8_t w;
+        uint8_t white;
+      };
+    };
+    uint8_t raw[4];
+    uint32_t raw_32;
+  };
+
+  inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {}  // NOLINT
+  inline Color(uint8_t red, uint8_t green, uint8_t blue) ALWAYS_INLINE : r(red), g(green), b(blue), w(0) {}
+
+  inline Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ALWAYS_INLINE : r(red),
+                                                                                        g(green),
+                                                                                        b(blue),
+                                                                                        w(white) {}
+  inline explicit Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF),
+                                                            g((colorcode >> 8) & 0xFF),
+                                                            b((colorcode >> 0) & 0xFF),
+                                                            w((colorcode >> 24) & 0xFF) {}
+
+  inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; }
+  inline Color &operator=(const Color &rhs) ALWAYS_INLINE {  // NOLINT
+    this->r = rhs.r;
+    this->g = rhs.g;
+    this->b = rhs.b;
+    this->w = rhs.w;
+    return *this;
+  }
+  inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE {
+    this->w = (colorcode >> 24) & 0xFF;
+    this->r = (colorcode >> 16) & 0xFF;
+    this->g = (colorcode >> 8) & 0xFF;
+    this->b = (colorcode >> 0) & 0xFF;
+    return *this;
+  }
+  inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; }
+  inline Color operator*(uint8_t scale) const ALWAYS_INLINE {
+    return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale),
+                 esp_scale8(this->white, scale));
+  }
+  inline Color &operator*=(uint8_t scale) ALWAYS_INLINE {
+    this->red = esp_scale8(this->red, scale);
+    this->green = esp_scale8(this->green, scale);
+    this->blue = esp_scale8(this->blue, scale);
+    this->white = esp_scale8(this->white, scale);
+    return *this;
+  }
+  inline Color operator*(const Color &scale) const ALWAYS_INLINE {
+    return Color(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green),
+                 esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white));
+  }
+  inline Color &operator*=(const Color &scale) ALWAYS_INLINE {
+    this->red = esp_scale8(this->red, scale.red);
+    this->green = esp_scale8(this->green, scale.green);
+    this->blue = esp_scale8(this->blue, scale.blue);
+    this->white = esp_scale8(this->white, scale.white);
+    return *this;
+  }
+  inline Color operator+(const Color &add) const ALWAYS_INLINE {
+    Color ret;
+    if (uint8_t(add.r + this->r) < this->r)
+      ret.r = 255;
+    else
+      ret.r = this->r + add.r;
+    if (uint8_t(add.g + this->g) < this->g)
+      ret.g = 255;
+    else
+      ret.g = this->g + add.g;
+    if (uint8_t(add.b + this->b) < this->b)
+      ret.b = 255;
+    else
+      ret.b = this->b + add.b;
+    if (uint8_t(add.w + this->w) < this->w)
+      ret.w = 255;
+    else
+      ret.w = this->w + add.w;
+    return ret;
+  }
+  inline Color &operator+=(const Color &add) ALWAYS_INLINE { return *this = (*this) + add; }
+  inline Color operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + Color(add, add, add, add); }
+  inline Color &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; }
+  inline Color operator-(const Color &subtract) const ALWAYS_INLINE {
+    Color ret;
+    if (subtract.r > this->r)
+      ret.r = 0;
+    else
+      ret.r = this->r - subtract.r;
+    if (subtract.g > this->g)
+      ret.g = 0;
+    else
+      ret.g = this->g - subtract.g;
+    if (subtract.b > this->b)
+      ret.b = 0;
+    else
+      ret.b = this->b - subtract.b;
+    if (subtract.w > this->w)
+      ret.w = 0;
+    else
+      ret.w = this->w - subtract.w;
+    return ret;
+  }
+  inline Color &operator-=(const Color &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; }
+  inline Color operator-(uint8_t subtract) const ALWAYS_INLINE {
+    return (*this) - Color(subtract, subtract, subtract, subtract);
+  }
+  inline Color &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; }
+  static Color random_color() {
+    uint32_t rand = random_uint32();
+    uint8_t w = rand >> 24;
+    uint8_t r = rand >> 16;
+    uint8_t g = rand >> 8;
+    uint8_t b = rand >> 0;
+    const uint16_t max_rgb = std::max(r, std::max(g, b));
+    return Color(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)),
+                 uint8_t((uint16_t(b) * 255U / max_rgb)), w);
+  }
+  Color fade_to_white(uint8_t amnt) { return Color(255, 255, 255, 255) - (*this * amnt); }
+  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;
+};
+
+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 f4151a14fc..591c9943b5 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -1,12 +1,14 @@
 #include "esphome/core/component.h"
-#include "esphome/core/helpers.h"
-#include "esphome/core/esphal.h"
-#include "esphome/core/log.h"
+
 #include "esphome/core/application.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include 
 
 namespace esphome {
 
-static const char *TAG = "component";
+static const char *const TAG = "component";
 
 namespace setup_priority {
 
@@ -15,7 +17,10 @@ const float IO = 900.0f;
 const float HARDWARE = 800.0f;
 const float DATA = 600.0f;
 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;
@@ -32,7 +37,7 @@ const uint32_t STATUS_LED_OK = 0x0000;
 const uint32_t STATUS_LED_WARNING = 0x0100;
 const uint32_t STATUS_LED_ERROR = 0x0200;
 
-uint32_t global_state = 0;
+uint32_t global_state = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 float Component::get_loop_priority() const { return 0.0f; }
 
@@ -50,6 +55,15 @@ bool Component::cancel_interval(const std::string &name) {  // NOLINT
   return App.scheduler.cancel_interval(this, name);
 }
 
+void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
+                          std::function &&f, float backoff_increase_factor) {  // NOLINT
+  App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
+}
+
+bool Component::cancel_retry(const std::string &name) {  // NOLINT
+  return App.scheduler.cancel_retry(this, name);
+}
+
 void Component::set_timeout(const std::string &name, uint32_t timeout, std::function &&f) {  // NOLINT
   return App.scheduler.set_timeout(this, name, timeout, std::move(f));
 }
@@ -59,8 +73,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;
@@ -81,15 +96,20 @@ void Component::call() {
       // State loop: Call loop
       this->call_loop();
       break;
-    case COMPONENT_STATE_FAILED:
+    case COMPONENT_STATE_FAILED:  // NOLINT(bugprone-branch-clone)
       // State failed: Do nothing
       break;
     default:
       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();
@@ -109,6 +129,10 @@ void Component::set_timeout(uint32_t timeout, std::function &&f) {  // N
 void Component::set_interval(uint32_t interval, std::function &&f) {  // NOLINT
   App.scheduler.set_interval(this, "", interval, std::move(f));
 }
+void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f,
+                          float backoff_increase_factor) {  // NOLINT
+  App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
+}
 bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
 bool Component::can_proceed() { return true; }
 bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
@@ -133,7 +157,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_;
 }
@@ -166,21 +190,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(const std::string &name) : name_(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_whitelist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_WHITELIST);
-  // 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 e3f9a51f25..c3a4ac3782 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,11 +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.
@@ -36,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); \
@@ -53,6 +61,8 @@ extern const uint32_t STATUS_LED_OK;
 extern const uint32_t STATUS_LED_WARNING;
 extern const uint32_t STATUS_LED_ERROR;
 
+enum RetryResult { DONE, RETRY };
+
 class Component {
  public:
   /** Where the component's initialization should happen.
@@ -128,9 +138,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().
@@ -157,7 +182,35 @@ class Component {
    */
   bool cancel_interval(const std::string &name);  // NOLINT
 
-  void set_timeout(uint32_t timeout, std::function &&f);  // NOLINT
+  /** Set an retry function with a unique name. Empty name means no cancelling possible.
+   *
+   * This will call f. If f returns RetryResult::RETRY f is called again after initial_wait_time ms.
+   * f should return RetryResult::DONE if no repeat is required. The initial wait time will be increased
+   * by backoff_increase_factor for each iteration. Default is doubling the time between iterations
+   * Can be cancelled via cancel_retry().
+   *
+   * IMPORTANT: Do not rely on this having correct timing. This is only called from
+   * loop() and therefore can be significantly delayed.
+   *
+   * @param name The identifier for this retry function.
+   * @param initial_wait_time The time in ms before f is called again
+   * @param max_attempts The maximum number of retries
+   * @param f The function (or lambda) that should be called
+   * @param backoff_increase_factor time between retries is increased by this factor on every retry
+   * @see cancel_retry()
+   */
+  void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,  // NOLINT
+                 std::function &&f, float backoff_increase_factor = 1.0f);    // NOLINT
+
+  void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f,  // NOLINT
+                 float backoff_increase_factor = 1.0f);                                               // NOLINT
+
+  /** Cancel a retry function.
+   *
+   * @param name The identifier for this retry function.
+   * @return Whether a retry function was deleted.
+   */
+  bool cancel_retry(const std::string &name);  // NOLINT
 
   /** Set a timeout function with a unique name.
    *
@@ -175,6 +228,8 @@ class Component {
    */
   void set_timeout(const std::string &name, uint32_t timeout, std::function &&f);  // NOLINT
 
+  void set_timeout(uint32_t timeout, std::function &&f);  // NOLINT
+
   /** Cancel a timeout function.
    *
    * @param name The identifier for this timeout function.
@@ -199,6 +254,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.
@@ -240,29 +296,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(const 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
new file mode 100644
index 0000000000..68c253f7b4
--- /dev/null
+++ b/esphome/core/config.py
@@ -0,0 +1,355 @@
+import logging
+import os
+import re
+
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.const import (
+    CONF_ARDUINO_VERSION,
+    CONF_BOARD,
+    CONF_BOARD_FLASH_MODE,
+    CONF_BUILD_PATH,
+    CONF_COMMENT,
+    CONF_ESPHOME,
+    CONF_FRAMEWORK,
+    CONF_INCLUDES,
+    CONF_LIBRARIES,
+    CONF_NAME,
+    CONF_ON_BOOT,
+    CONF_ON_LOOP,
+    CONF_ON_SHUTDOWN,
+    CONF_PLATFORM,
+    CONF_PLATFORMIO_OPTIONS,
+    CONF_PRIORITY,
+    CONF_PROJECT,
+    CONF_SOURCE,
+    CONF_TRIGGER_ID,
+    CONF_TYPE,
+    CONF_VERSION,
+    KEY_CORE,
+    TARGET_PLATFORMS,
+    PLATFORM_ESP8266,
+)
+from esphome.core import CORE, coroutine_with_priority
+from esphome.helpers import copy_file_if_changed, walk_files
+
+_LOGGER = logging.getLogger(__name__)
+
+BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"]
+StartupTrigger = cg.esphome_ns.class_(
+    "StartupTrigger", cg.Component, automation.Trigger.template()
+)
+ShutdownTrigger = cg.esphome_ns.class_(
+    "ShutdownTrigger", cg.Component, automation.Trigger.template()
+)
+LoopTrigger = cg.esphome_ns.class_(
+    "LoopTrigger", cg.Component, automation.Trigger.template()
+)
+
+VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$")
+
+CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix"
+
+
+VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
+
+
+def validate_hostname(config):
+    max_length = 31
+    if config[CONF_NAME_ADD_MAC_SUFFIX]:
+        max_length -= 7  # "-AABBCC" is appended when add mac suffix option is used
+    if len(config[CONF_NAME]) > max_length:
+        raise cv.Invalid(
+            f"Hostnames can only be {max_length} characters long", path=[CONF_NAME]
+        )
+    if "_" in config[CONF_NAME]:
+        _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",
+            config[CONF_NAME],
+        )
+    return config
+
+
+def valid_include(value):
+    try:
+        return cv.directory(value)
+    except cv.Invalid:
+        pass
+    value = cv.file_(value)
+    _, ext = os.path.splitext(value)
+    if ext not in VALID_INCLUDE_EXTS:
+        raise cv.Invalid(
+            f"Include has invalid file extension {ext} - valid extensions are {', '.join(VALID_INCLUDE_EXTS)}"
+        )
+    return value
+
+
+def valid_project_name(value: str):
+    if value.count(".") != 1:
+        raise cv.Invalid("project name needs to have a namespace")
+
+    value = value.replace(" ", "_")
+
+    return value
+
+
+CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash"
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.Required(CONF_NAME): cv.valid_name,
+            cv.Optional(CONF_COMMENT): 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.Optional(CONF_ON_BOOT): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger),
+                    cv.Optional(CONF_PRIORITY, default=600.0): cv.float_,
+                }
+            ),
+            cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ShutdownTrigger),
+                }
+            ),
+            cv.Optional(CONF_ON_LOOP): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger),
+                }
+            ),
+            cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
+            cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
+            cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean,
+            cv.Optional(CONF_PROJECT): cv.Schema(
+                {
+                    cv.Required(CONF_NAME): cv.All(
+                        cv.string_strict, valid_project_name
+                    ),
+                    cv.Required(CONF_VERSION): cv.string_strict,
+                }
+            ),
+        }
+    ),
+    validate_hostname,
+)
+
+PRELOAD_CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_NAME): cv.valid_name,
+        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,
+)
+
+
+def preload_core_config(config, result):
+    with cv.prepend_path(CONF_ESPHOME):
+        conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME])
+
+    CORE.name = conf[CONF_NAME]
+    CORE.data[KEY_CORE] = {}
+
+    if CONF_BUILD_PATH not in conf:
+        conf[CONF_BUILD_PATH] = f".esphome/build/{CORE.name}"
+    CORE.build_path = CORE.relative_config_path(conf[CONF_BUILD_PATH])
+
+    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],
+        )
+    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] = {}
+            if plat != PLATFORM_ESP8266:
+                plat_conf[CONF_FRAMEWORK][CONF_TYPE] = "arduino"
+
+            try:
+                if conf[CONF_ARDUINO_VERSION] not in ("recommended", "latest", "dev"):
+                    cv.Version.parse(conf[CONF_ARDUINO_VERSION])
+                plat_conf[CONF_FRAMEWORK][CONF_VERSION] = conf.pop(CONF_ARDUINO_VERSION)
+            except ValueError:
+                plat_conf[CONF_FRAMEWORK][CONF_SOURCE] = 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):
+    parts = basename.split(os.path.sep)
+    dst = CORE.relative_src_path(*parts)
+    copy_file_if_changed(path, dst)
+
+    _, ext = os.path.splitext(path)
+    if ext in [".h", ".hpp", ".tcc"]:
+        # Header, add include statement
+        cg.add_global(cg.RawStatement(f'#include "{basename}"'))
+
+
+ARDUINO_GLUE_CODE = """\
+#define yield() esphome::yield()
+#define millis() esphome::millis()
+#define micros() esphome::micros()
+#define delay(x) esphome::delay(x)
+#define delayMicroseconds(x) esphome::delayMicroseconds(x)
+"""
+
+
+@coroutine_with_priority(-999.0)
+async def add_arduino_global_workaround():
+    # The Arduino framework defined these itself in the global
+    # namespace. For the esphome codebase that is not a problem,
+    # but when custom code
+    #   1. writes `millis()` for example AND
+    #   2. has `using namespace esphome;` like our guides suggest
+    # Then the compiler will complain that the call is ambiguous
+    # Define a hacky macro so that the call is never ambiguous
+    # and always uses the esphome namespace one.
+    # See also https://github.com/esphome/issues/issues/2510
+    # Priority -999 so that it runs before adding includes, as those
+    # also might reference these symbols
+    for line in ARDUINO_GLUE_CODE.splitlines():
+        cg.add_global(cg.RawStatement(line))
+
+
+@coroutine_with_priority(-1000.0)
+async def add_includes(includes):
+    # Add includes at the very end, so that the included files can access global variables
+    for include in includes:
+        path = CORE.relative_config_path(include)
+        if os.path.isdir(path):
+            # Directory, copy tree
+            for p in walk_files(path):
+                basename = os.path.relpath(p, os.path.dirname(path))
+                include_file(p, basename)
+        else:
+            # Copy file
+            basename = os.path.basename(path)
+            include_file(path, basename)
+
+
+@coroutine_with_priority(-1000.0)
+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)
+async def _add_automations(config):
+    for conf in config.get(CONF_ON_BOOT, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
+
+    for conf in config.get(CONF_ON_SHUTDOWN, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
+
+    for conf in config.get(CONF_ON_LOOP, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
+
+
+@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],
+            cg.RawExpression('__DATE__ ", " __TIME__'),
+            config[CONF_NAME_ADD_MAC_SUFFIX],
+        )
+    )
+
+    CORE.add_job(_add_automations, config)
+
+    cg.add_build_flag("-fno-exceptions")
+
+    # Libraries
+    for lib in config[CONF_LIBRARIES]:
+        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 CORE.using_arduino:
+        CORE.add_job(add_arduino_global_workaround)
+
+    if config[CONF_INCLUDES]:
+        CORE.add_job(add_includes, config[CONF_INCLUDES])
+
+    if CONF_PROJECT in config:
+        cg.add_define("ESPHOME_PROJECT_NAME", config[CONF_PROJECT][CONF_NAME])
+        cg.add_define("ESPHOME_PROJECT_VERSION", config[CONF_PROJECT][CONF_VERSION])
+
+    if 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 bd68d777ff..6d3a76a292 100644
--- a/esphome/core/controller.cpp
+++ b/esphome/core/controller.cpp
@@ -4,55 +4,67 @@
 
 namespace esphome {
 
-void Controller::setup_controller() {
+void Controller::setup_controller(bool include_internal) {
 #ifdef USE_BINARY_SENSOR
   for (auto *obj : App.get_binary_sensors()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); });
   }
 #endif
 #ifdef USE_FAN
   for (auto *obj : App.get_fans()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); });
   }
 #endif
 #ifdef USE_LIGHT
   for (auto *obj : App.get_lights()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); });
   }
 #endif
 #ifdef USE_SENSOR
   for (auto *obj : App.get_sensors()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); });
   }
 #endif
 #ifdef USE_SWITCH
   for (auto *obj : App.get_switches()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); });
   }
 #endif
 #ifdef USE_COVER
   for (auto *obj : App.get_covers()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); });
   }
 #endif
 #ifdef USE_TEXT_SENSOR
   for (auto *obj : App.get_text_sensors()) {
-    if (!obj->is_internal())
-      obj->add_on_state_callback([this, obj](std::string state) { this->on_text_sensor_update(obj, state); });
+    if (include_internal || !obj->is_internal())
+      obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_sensor_update(obj, state); });
   }
 #endif
 #ifdef USE_CLIMATE
   for (auto *obj : App.get_climates()) {
-    if (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); });
   }
 #endif
+#ifdef USE_NUMBER
+  for (auto *obj : App.get_numbers()) {
+    if (include_internal || !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 (include_internal || !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 fa7d1f2ef0..0c3722855c 100644
--- a/esphome/core/controller.h
+++ b/esphome/core/controller.h
@@ -22,15 +22,24 @@
 #ifdef USE_SWITCH
 #include "esphome/components/switch/switch.h"
 #endif
+#ifdef USE_BUTTON
+#include "esphome/components/button/button.h"
+#endif
 #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 {
 
 class Controller {
  public:
-  void setup_controller();
+  void setup_controller(bool include_internal = false);
 #ifdef USE_BINARY_SENSOR
   virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){};
 #endif
@@ -50,11 +59,17 @@ class Controller {
   virtual void on_cover_update(cover::Cover *obj){};
 #endif
 #ifdef USE_TEXT_SENSOR
-  virtual void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state){};
+  virtual void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state){};
 #endif
 #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 fe10a42baa..a74755f651 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -1,25 +1,78 @@
 #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 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"
+#define ESPHOME_VARIANT "ESP32"
+
+// Feature flags
 #define USE_API
-#define USE_LOGGER
+#define USE_API_NOISE
+#define USE_API_PLAINTEXT
 #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_BUTTON
 #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
-#endif
-#define USE_TIME
+#define USE_COVER
 #define USE_DEEP_SLEEP
+#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_PASSWORD
+#define USE_OTA_STATE_CALLBACK
+#define USE_POWER_SUPPLY
+#define USE_SELECT
+#define USE_SENSOR
+#define USE_STATUS_LED
+#define USE_SWITCH
+#define USE_TEXT_SENSOR
+#define USE_TIME
+#define USE_UART_DEBUGGER
+#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_PROMETHEUS
+#define USE_WEBSERVER
+#define USE_WIFI_WPA2_EAP
+#define WEBSERVER_PORT 80  // NOLINT
+#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_ESP8266_PREFERENCES_FLASH
+#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..a9e1414018
--- /dev/null
+++ b/esphome/core/entity_base.cpp
@@ -0,0 +1,44 @@
+#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 Category
+EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; }
+void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; }
+
+// 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_ = str_sanitize(str_snake_case(this->name_));
+  // 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..c489d71910
--- /dev/null
+++ b/esphome/core/entity_base.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include 
+#include 
+
+namespace esphome {
+
+enum EntityCategory : uint8_t {
+  ENTITY_CATEGORY_NONE = 0,
+  ENTITY_CATEGORY_CONFIG = 1,
+  ENTITY_CATEGORY_DIAGNOSTIC = 2,
+};
+
+// 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 the entity category.
+  EntityCategory get_entity_category() const;
+  void set_entity_category(EntityCategory entity_category);
+
+  // 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};
+  EntityCategory entity_category_{ENTITY_CATEGORY_NONE};
+};
+
+}  // 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 13d54e726d..0000000000
--- a/esphome/core/esphal.cpp
+++ /dev/null
@@ -1,280 +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);
-};
-#endif
-
-namespace esphome {
-
-static const char *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::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_);
-}
-
-}  // 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
diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h
deleted file mode 100644
index 493f7f5e37..0000000000
--- a/esphome/core/esphal.h
+++ /dev/null
@@ -1,118 +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;
-
-  ISRInternalGPIOPin *to_isr() const;
-
- protected:
-  void attach_interrupt_(void (*func)(void *), void *arg, int mode) 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);
-}
-
-}  // namespace esphome
diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h
new file mode 100644
index 0000000000..04658d567c
--- /dev/null
+++ b/esphome/core/gpio.h
@@ -0,0 +1,99 @@
+#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();
+  void pin_mode(gpio::Flags flags);
+
+ 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..034f9d692f
--- /dev/null
+++ b/esphome/core/hal.h
@@ -0,0 +1,48 @@
+#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_init();
+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 2222a1a664..b82a2666e7 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -1,52 +1,72 @@
 #include "esphome/core/helpers.h"
+#include "esphome/core/defines.h"
 #include 
 #include 
+#include 
+#include 
 
-#ifdef ARDUINO_ARCH_ESP8266
-#include 
-#else
+#if defined(USE_ESP8266)
+#include 
+#include 
+// for xt_rsil()/xt_wsr_ps()
+#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 *TAG = "helpers";
+static const char *const TAG = "helpers";
 
-std::string get_mac_address() {
-  char tmp[20];
-  uint8_t mac[6];
-#ifdef ARDUINO_ARCH_ESP32
+void get_mac_address_raw(uint8_t *mac) {
+#if defined(USE_ESP32)
+#if defined(USE_ESP32_IGNORE_EFUSE_MAC_CRC)
+  // On some devices, the MAC address that is burnt into EFuse does not
+  // match the CRC that goes along with it. For those devices, this
+  // work-around reads and uses the MAC address as-is from EFuse,
+  // without doing the CRC check.
+  esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
+#else
   esp_efuse_mac_get_default(mac);
 #endif
-#ifdef ARDUINO_ARCH_ESP8266
-  WiFi.macAddress(mac);
+#elif defined(USE_ESP8266)
+  wifi_get_macaddr(STATION_IF, mac);
 #endif
-  sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-  return std::string(tmp);
+}
+
+std::string get_mac_address() {
+  uint8_t mac[6];
+  get_mac_address_raw(mac);
+  return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 }
 
 std::string get_mac_address_pretty() {
-  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
-  sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-  return std::string(tmp);
+  get_mac_address_raw(mac);
+  return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 }
 
+#ifdef USE_ESP32
+void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
+#endif
+
 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,7 +75,18 @@ double random_double() { return random_uint32() / double(UINT32_MAX); }
 
 float random_float() { return float(random_double()); }
 
-static uint32_t fast_random_seed = 0;
+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; }
 uint32_t fast_random_32() {
@@ -67,7 +98,7 @@ uint16_t fast_random_16() {
   return (rand32 & 0xFFFF) + (rand32 >> 16);
 }
 uint8_t fast_random_8() {
-  uint8_t rand32 = fast_random_32();
+  uint32_t rand32 = fast_random_32();
   return (rand32 & 0xFF) + ((rand32 >> 8) & 0xFF);
 }
 
@@ -79,36 +110,23 @@ float gamma_correct(float value, float gamma) {
 
   return powf(value, gamma);
 }
-std::string to_lowercase_underscore(std::string s) {
-  std::transform(s.begin(), s.end(), s.begin(), ::tolower);
-  std::replace(s.begin(), s.end(), ' ', '_');
-  return s;
-}
+float gamma_uncorrect(float value, float gamma) {
+  if (value <= 0.0f)
+    return 0.0f;
+  if (gamma <= 0.0f)
+    return value;
 
-std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist) {
-  std::string out(s);
-  out.erase(std::remove_if(out.begin(), out.end(),
-                           [&whitelist](const char &c) { return whitelist.find(c) == std::string::npos; }),
-            out.end());
-  return out;
-}
-
-std::string sanitize_hostname(const std::string &hostname) {
-  std::string s = sanitize_string_whitelist(hostname, HOSTNAME_CHARACTER_WHITELIST);
-  return truncate_string(s, 63);
-}
-
-std::string truncate_string(const std::string &s, size_t length) {
-  if (s.length() > length)
-    return s.substr(0, length);
-  return s;
+  return powf(value, 1 / gamma);
 }
 
 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 +141,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;
-static size_t global_json_build_buffer_size = 0;
-
-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)
@@ -154,8 +157,6 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
   return PARSE_NONE;
 }
 
-const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
-
 uint8_t crc8(uint8_t *data, uint8_t len) {
   uint8_t crc = 0;
 
@@ -171,16 +172,19 @@ uint8_t crc8(uint8_t *data, uint8_t len) {
   }
   return crc;
 }
-void delay_microseconds_accurate(uint32_t usec) {
-  if (usec == 0)
-    return;
 
-  if (usec <= 16383UL) {
-    delayMicroseconds(usec);
-  } else {
-    delay(usec / 16383UL);
-    delayMicroseconds(usec % 16383UL);
+void delay_microseconds_safe(uint32_t us) {  // avoids CPU locks that could trigger WDT or affect WiFi/BT stability
+  auto start = micros();
+  const uint32_t lag = 5000;  // microseconds, specifies the maximum time for a CPU busy-loop.
+                              // it must be larger than the worst-case duration of a delay(1) call (hardware tasks)
+                              // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known
+  if (us > lag) {
+    delay((us - lag) / 1000UL);  // note: in disabled-interrupt contexts delay() won't actually sleep
+    while (micros() - start < us - lag)
+      delay(1);  // in those cases, this loop allows to yield for BT/WiFi stack tasks
   }
+  while (micros() - start < us)  // fine delay the remaining usecs
+    ;
 }
 
 uint8_t reverse_bits_8(uint8_t x) {
@@ -199,27 +203,27 @@ std::string to_string(int val) {
   sprintf(buf, "%d", val);
   return buf;
 }
-std::string to_string(long val) {
+std::string to_string(long val) {  // NOLINT
   char buf[64];
   sprintf(buf, "%ld", val);
   return buf;
 }
-std::string to_string(long long val) {
+std::string to_string(long long val) {  // NOLINT
   char buf[64];
   sprintf(buf, "%lld", val);
   return buf;
 }
-std::string to_string(unsigned val) {
+std::string to_string(unsigned val) {  // NOLINT
   char buf[64];
   sprintf(buf, "%u", val);
   return buf;
 }
-std::string to_string(unsigned long val) {
+std::string to_string(unsigned long val) {  // NOLINT
   char buf[64];
   sprintf(buf, "%lu", val);
   return buf;
 }
-std::string to_string(unsigned long long val) {
+std::string to_string(unsigned long long val) {  // NOLINT
   char buf[64];
   sprintf(buf, "%llu", val);
   return buf;
@@ -239,13 +243,7 @@ std::string to_string(long double val) {
   sprintf(buf, "%Lf", val);
   return buf;
 }
-optional parse_float(const std::string &str) {
-  char *end;
-  float value = ::strtof(str.c_str(), &end);
-  if (end == nullptr || end != str.end().base())
-    return {};
-  return value;
-}
+
 uint32_t fnv1_hash(const std::string &str) {
   uint32_t hash = 2166136261UL;
   for (char c : str) {
@@ -262,7 +260,7 @@ template uint32_t reverse_bits(uint32_t x) {
   return uint32_t(reverse_bits_16(x & 0xFFFF) << 16) | uint32_t(reverse_bits_16(x >> 16));
 }
 
-static int high_freq_num_requests = 0;
+static int high_freq_num_requests = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void HighFrequencyLoopRequester::start() {
   if (this->started_)
@@ -278,50 +276,199 @@ 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 uint8_t clamp(uint8_t, uint8_t, uint8_t);
+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_snprintf(const char *fmt, size_t length, ...) {
+  std::string str;
+  va_list args;
 
-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) {
-  uint8_t msb = (value >> 8) & 0xFF;
-  uint8_t lsb = (value >> 0) & 0xFF;
-  return {msb, lsb};
+  str.resize(length);
+  va_start(args, length);
+  size_t out_length = vsnprintf(&str[0], length + 1, fmt, args);
+  va_end(args);
+
+  if (out_length < length)
+    str.resize(out_length);
+
+  return str;
+}
+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;
 }
 
-std::string hexencode(const uint8_t *data, uint32_t len) {
-  char buf[20];
-  std::string res;
-  for (size_t i = 0; i < len; i++) {
-    if (i + 1 != len) {
-      sprintf(buf, "%02X.", data[i]);
-    } else {
-      sprintf(buf, "%02X ", data[i]);
-    }
-    res += buf;
+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;
   }
-  sprintf(buf, "(%u)", len);
-  res += buf;
-  return res;
+
+  red += delta;
+  green += delta;
+  blue += delta;
 }
 
-#ifdef ARDUINO_ARCH_ESP8266
-ICACHE_RAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); }
-ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); }
+#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
 
+// ---------------------------------------------------------------------------------------------------------------------
+
+// Strings
+
+std::string str_truncate(const std::string &str, size_t length) {
+  return str.length() > length ? str.substr(0, length) : str;
+}
+std::string str_until(const char *str, char ch) {
+  char *pos = strchr(str, ch);
+  return pos == nullptr ? std::string(str) : std::string(str, pos - str);
+}
+std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); }
+std::string str_snake_case(const std::string &str) {
+  std::string result;
+  result.resize(str.length());
+  std::transform(str.begin(), str.end(), result.begin(), ::tolower);
+  std::replace(result.begin(), result.end(), ' ', '_');
+  return result;
+}
+std::string str_sanitize(const std::string &str) {
+  std::string out;
+  std::copy_if(str.begin(), str.end(), std::back_inserter(out), [](const char &c) {
+    return c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+  });
+  return out;
+}
+
+// Parsing & formatting
+
+size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
+  uint8_t val;
+  size_t chars = std::min(length, 2 * count);
+  for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
+    if (*str >= '0' && *str <= '9')
+      val = *str - '0';
+    else if (*str >= 'A' && *str <= 'F')
+      val = 10 + (*str - 'A');
+    else if (*str >= 'a' && *str <= 'f')
+      val = 10 + (*str - 'a');
+    else
+      return 0;
+    data[i >> 1] = !(i & 1) ? val << 4 : data[i >> 1] | val;
+  }
+  return chars;
+}
+
+static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
+std::string format_hex(const uint8_t *data, size_t length) {
+  std::string ret;
+  ret.resize(length * 2);
+  for (size_t i = 0; i < length; i++) {
+    ret[2 * i] = format_hex_char((data[i] & 0xF0) >> 4);
+    ret[2 * i + 1] = format_hex_char(data[i] & 0x0F);
+  }
+  return ret;
+}
+std::string format_hex(std::vector data) { return format_hex(data.data(), data.size()); }
+
+static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
+std::string format_hex_pretty(const uint8_t *data, size_t length) {
+  if (length == 0)
+    return "";
+  std::string ret;
+  ret.resize(3 * length - 1);
+  for (size_t i = 0; i < length; i++) {
+    ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
+    ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
+    if (i != length - 1)
+      ret[3 * i + 2] = '.';
+  }
+  if (length > 4)
+    return ret + " (" + to_string(length) + ")";
+  return ret;
+}
+std::string format_hex_pretty(std::vector data) { return format_hex_pretty(data.data(), data.size()); }
+
 }  // namespace esphome
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index ab3d883e05..90f35ee4ca 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -1,62 +1,63 @@
 #pragma once
 
+#include 
+#include 
+
 #include 
 #include 
 #include 
 #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))
 
 namespace esphome {
 
-/// The characters that are allowed in a hostname.
-extern const char *HOSTNAME_CHARACTER_WHITELIST;
+/// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
+void get_mac_address_raw(uint8_t *mac);
 
-/// Gets the MAC address as a string, this can be used as way to identify this ESP.
+/// Get the device MAC address as a string, in lowercase hex notation.
 std::string get_mac_address();
 
+/// Get the device MAC address as a string, in colon-separated uppercase 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);
-std::string to_string(long long val);
-std::string to_string(unsigned val);
-std::string to_string(unsigned long val);
-std::string to_string(unsigned long long val);
+std::string to_string(long val);                // NOLINT
+std::string to_string(long long val);           // NOLINT
+std::string to_string(unsigned val);            // NOLINT
+std::string to_string(unsigned long val);       // NOLINT
+std::string to_string(unsigned long long val);  // NOLINT
 std::string to_string(float val);
 std::string to_string(double val);
 std::string to_string(long double val);
-optional parse_float(const std::string &str);
-
-/// Sanitize the hostname by removing characters that are not in the whitelist and truncating it to 63 chars.
-std::string sanitize_hostname(const std::string &hostname);
-
-/// Truncate a string to a specific length
-std::string truncate_string(const std::string &s, size_t length);
-
-/// Convert the string to lowercase_underscore.
-std::string to_lowercase_underscore(std::string s);
 
 /// Compare string a to string b (ignoring case) and return whether they are equal.
 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);
 
+/// snprintf-like function returning std::string with a given maximum length.
+std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t length, ...);
+
+/// sprintf-like function returning std::string.
+std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
+
 class HighFrequencyLoopRequester {
  public:
   void start();
@@ -75,7 +76,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.
  *
@@ -87,10 +88,15 @@ float clamp(float val, float min, float max);
  */
 float lerp(float completion, float start, float end);
 
-/// std::make_unique
-template std::unique_ptr make_unique(Args &&... args) {
+// 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();
@@ -104,6 +110,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();
@@ -111,6 +119,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);
@@ -121,17 +131,14 @@ std::string uint64_to_string(uint64_t num);
 /// Convert a uint32_t to a hex string
 std::string uint32_to_string(uint32_t num);
 
-/// Sanitizes the input string with the whitelist.
-std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist);
-
 uint8_t reverse_bits_8(uint8_t x);
 uint16_t reverse_bits_16(uint16_t x);
 uint32_t reverse_bits_32(uint32_t x);
 
-/// Encode a 16-bit unsigned integer given a most and least-significant byte.
-uint16_t encode_uint16(uint8_t msb, uint8_t lsb);
-/// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte.
-std::array decode_uint16(uint16_t value);
+/// 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.
@@ -139,7 +146,7 @@ std::array decode_uint16(uint16_t value);
  * 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).
  *
@@ -161,7 +168,7 @@ class InterruptLock {
   ~InterruptLock();
 
  protected:
-#ifdef ARDUINO_ARCH_ESP8266
+#ifdef USE_ESP8266
   uint32_t xt_state_;
 #endif
 };
@@ -178,10 +185,6 @@ enum ParseOnOffState {
 
 ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
 
-// Encode raw data to a human-readable string (for debugging)
-std::string hexencode(const uint8_t *data, uint32_t len);
-template std::string hexencode(const T &data) { return hexencode(data.data(), data.size()); }
-
 // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
 template struct seq {};                                       // NOLINT
 template struct gens : gens {};  // NOLINT
@@ -224,64 +227,7 @@ struct is_callable  // NOLINT
   static constexpr auto value = decltype(test(nullptr))::value;  // NOLINT
 };
 
-template class TemplatableValue {
- public:
-  TemplatableValue() : type_(EMPTY) {}
-
-  template::value, int> = 0>
-  TemplatableValue(F value) : type_(VALUE), value_(value) {}
-
-  template::value, int> = 0>
-  TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
-
-  bool has_value() { return this->type_ != EMPTY; }
-
-  T value(X... x) {
-    if (this->type_ == LAMBDA) {
-      return this->f_(x...);
-    }
-    // return value also when empty
-    return this->value_;
-  }
-
-  optional optional_value(X... x) {
-    if (!this->has_value()) {
-      return {};
-    }
-    return this->value(x...);
-  }
-
-  T value_or(X... x, T default_value) {
-    if (!this->has_value()) {
-      return default_value;
-    }
-    return this->value(x...);
-  }
-
- protected:
-  enum {
-    EMPTY,
-    VALUE,
-    LAMBDA,
-  } type_;
-
-  T value_;
-  std::function f_;
-};
-
-template class TemplatableStringValue : public TemplatableValue {
- public:
-  TemplatableStringValue() : TemplatableValue() {}
-
-  template::value, int> = 0>
-  TemplatableStringValue(F value) : TemplatableValue(value) {}
-
-  template::value, int> = 0>
-  TemplatableStringValue(F f)
-      : TemplatableValue([f](X... x) -> std::string { return to_string(f(x...)); }) {}
-};
-
-void delay_microseconds_accurate(uint32_t usec);
+void delay_microseconds_safe(uint32_t us);
 
 template class Deduplicator {
  public:
@@ -315,4 +261,243 @@ template class Parented {
 
 uint32_t fnv1_hash(const std::string &str);
 
+template T *new_buffer(size_t length) {
+  T *buffer;
+#ifdef USE_ESP32_FRAMEWORK_ARDUINO
+  if (psramFound()) {
+    buffer = (T *) ps_malloc(length);
+  } else {
+    buffer = new T[length];  // NOLINT(cppcoreguidelines-owning-memory)
+  }
+#else
+  buffer = new T[length];  // NOLINT(cppcoreguidelines-owning-memory)
+#endif
+
+  return buffer;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+
+/// @name STL backports
+///@{
+
+// std::byteswap is from C++23 and technically should be a template, but this will do for now.
+constexpr uint8_t byteswap(uint8_t n) { return n; }
+constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); }
+constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); }
+constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); }
+
+///@}
+
+/// @name Bit manipulation
+///@{
+
+/// Encode a 16-bit value given the most and least significant byte.
+constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb) {
+  return (static_cast(msb) << 8) | (static_cast(lsb));
+}
+/// Encode a 32-bit value given four bytes in most to least significant byte order.
+constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4) {
+  return (static_cast(byte1) << 24) | (static_cast(byte2) << 16) |
+         (static_cast(byte3) << 8) | (static_cast(byte4));
+}
+
+/// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T).
+template::value, int> = 0> inline T encode_value(const uint8_t *bytes) {
+  T val = 0;
+  for (size_t i = 0; i < sizeof(T); i++) {
+    val <<= 8;
+    val |= bytes[i];
+  }
+  return val;
+}
+/// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T).
+template::value, int> = 0>
+inline T encode_value(const std::array bytes) {
+  return encode_value(bytes.data());
+}
+/// Decode a value into its constituent bytes (from most to least significant).
+template::value, int> = 0>
+inline std::array decode_value(T val) {
+  std::array ret{};
+  for (size_t i = sizeof(T); i > 0; i--) {
+    ret[i - 1] = val & 0xFF;
+    val >>= 8;
+  }
+  return ret;
+}
+
+/// Convert a value between host byte order and big endian (most significant byte first) order.
+template::value, int> = 0> constexpr T convert_big_endian(T val) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+  return byteswap(val);
+#else
+  return val;
+#endif
+}
+
+///@}
+
+/// @name Strings
+///@{
+
+/// Truncate a string to a specific length.
+std::string str_truncate(const std::string &str, size_t length);
+
+/// Extract the part of the string until either the first occurence of the specified character, or the end (requires str
+/// to be null-terminated).
+std::string str_until(const char *str, char ch);
+/// Extract the part of the string until either the first occurence of the specified character, or the end.
+std::string str_until(const std::string &str, char ch);
+
+/// Convert the string to snake case (lowercase with underscores).
+std::string str_snake_case(const std::string &str);
+
+/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
+std::string str_sanitize(const std::string &str);
+
+///@}
+
+/// @name Parsing & formatting
+///@{
+
+/// Parse an unsigned decimal number from a null-terminated string.
+template::value && std::is_unsigned::value), int> = 0>
+optional parse_number(const char *str) {
+  char *end = nullptr;
+  unsigned long value = ::strtoul(str, &end, 10);  // NOLINT(google-runtime-int)
+  if (end == str || *end != '\0' || value > std::numeric_limits::max())
+    return {};
+  return value;
+}
+/// Parse an unsigned decimal number.
+template::value && std::is_unsigned::value), int> = 0>
+optional parse_number(const std::string &str) {
+  return parse_number(str.c_str());
+}
+/// Parse a signed decimal number from a null-terminated string.
+template::value && std::is_signed::value), int> = 0>
+optional parse_number(const char *str) {
+  char *end = nullptr;
+  signed long value = ::strtol(str, &end, 10);  // NOLINT(google-runtime-int)
+  if (end == str || *end != '\0' || value < std::numeric_limits::min() || value > std::numeric_limits::max())
+    return {};
+  return value;
+}
+/// Parse a signed decimal number.
+template::value && std::is_signed::value), int> = 0>
+optional parse_number(const std::string &str) {
+  return parse_number(str.c_str());
+}
+/// Parse a decimal floating-point number from a null-terminated string.
+template::value), int> = 0> optional parse_number(const char *str) {
+  char *end = nullptr;
+  float value = ::strtof(str, &end);
+  if (end == str || *end != '\0' || value == HUGE_VALF)
+    return {};
+  return value;
+}
+/// Parse a decimal floating-point number.
+template::value), int> = 0>
+optional parse_number(const std::string &str) {
+  return parse_number(str.c_str());
+}
+
+/** Parse bytes from a hex-encoded string into a byte array.
+ *
+ * When \p len is less than \p 2*count, the result is written to the back of \p data (i.e. this function treats \p str
+ * as if it were padded with zeros at the front).
+ *
+ * @param str String to read from.
+ * @param len Length of \p str (excluding optional null-terminator), is a limit on the number of characters parsed.
+ * @param data Byte array to write to.
+ * @param count Length of \p data.
+ * @return The number of characters parsed from \p str.
+ */
+size_t parse_hex(const char *str, size_t len, uint8_t *data, size_t count);
+/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into array \p data.
+inline bool parse_hex(const char *str, uint8_t *data, size_t count) {
+  return parse_hex(str, strlen(str), data, count) == 2 * count;
+}
+/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into array \p data.
+inline bool parse_hex(const std::string &str, uint8_t *data, size_t count) {
+  return parse_hex(str.c_str(), str.length(), data, count) == 2 * count;
+}
+/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into vector \p data.
+inline bool parse_hex(const char *str, std::vector &data, size_t count) {
+  data.resize(count);
+  return parse_hex(str, strlen(str), data.data(), count) == 2 * count;
+}
+/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into vector \p data.
+inline bool parse_hex(const std::string &str, std::vector &data, size_t count) {
+  data.resize(count);
+  return parse_hex(str.c_str(), str.length(), data.data(), count) == 2 * count;
+}
+/** Parse a hex-encoded string into an unsigned integer.
+ *
+ * @param str String to read from, starting with the most significant byte.
+ * @param len Length of \p str (excluding optional null-terminator), is a limit on the number of characters parsed.
+ */
+template::value, int> = 0>
+optional parse_hex(const char *str, size_t len) {
+  T val = 0;
+  if (len > 2 * sizeof(T) || parse_hex(str, len, reinterpret_cast(&val), sizeof(T)) == 0)
+    return {};
+  return convert_big_endian(val);
+}
+/// Parse a hex-encoded null-terminated string (starting with the most significant byte) into an unsigned integer.
+template::value, int> = 0> optional parse_hex(const char *str) {
+  return parse_hex(str, strlen(str));
+}
+/// Parse a hex-encoded null-terminated string (starting with the most significant byte) into an unsigned integer.
+template::value, int> = 0> optional parse_hex(const std::string &str) {
+  return parse_hex(str.c_str(), str.length());
+}
+
+/// Format the byte array \p data of length \p len in lowercased hex.
+std::string format_hex(const uint8_t *data, size_t length);
+/// Format the vector \p data in lowercased hex.
+std::string format_hex(std::vector data);
+/// Format an unsigned integer in lowercased hex, starting with the most significant byte.
+template::value, int> = 0> std::string format_hex(T val) {
+  val = convert_big_endian(val);
+  return format_hex(reinterpret_cast(&val), sizeof(T));
+}
+
+/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex.
+std::string format_hex_pretty(const uint8_t *data, size_t length);
+/// Format the vector \p data in pretty-printed, human-readable hex.
+std::string format_hex_pretty(std::vector data);
+/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
+template::value, int> = 0> std::string format_hex_pretty(T val) {
+  val = convert_big_endian(val);
+  return format_hex_pretty(reinterpret_cast(&val), sizeof(T));
+}
+
+///@}
+
+/// @name Number manipulation
+///@{
+
+/// Remap a number from one range to another.
+template T remap(U value, U min, U max, T min_out, T max_out) {
+  return (value - min) * (max_out - min_out) / (max - min) + min_out;
+}
+
+///@}
+
+/// @name Deprecated functions
+///@{
+
+ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1")
+inline std::string hexencode(const uint8_t *data, uint32_t len) { return format_hex_pretty(data, len); }
+
+template
+ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1")
+std::string hexencode(const T &data) {
+  return hexencode(data.data(), data.size());
+}
+
+///@}
+
 }  // namespace esphome
diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp
index 15d49c0038..424154d253 100644
--- a/esphome/core/log.cpp
+++ b/esphome/core/log.cpp
@@ -46,23 +46,13 @@ 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;
   if (log == nullptr)
     return 0;
 
-  size_t len = strlen(format);
-  if (format[len - 1] == '\n') {
-    // Remove trailing newline from format
-    // Use locally stored
-    static std::string FORMAT_COPY;
-    FORMAT_COPY.clear();
-    FORMAT_COPY.insert(0, format, len - 1);
-    format = FORMAT_COPY.c_str();
-  }
-
   log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args);
 #endif
   return 0;
diff --git a/esphome/core/log.h b/esphome/core/log.h
index 361fbe1182..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
 
@@ -160,5 +167,39 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
       ((byte) &0x08 ? '1' : '0'), ((byte) &0x04 ? '1' : '0'), ((byte) &0x02 ? '1' : '0'), ((byte) &0x01 ? '1' : '0')
 #define YESNO(b) ((b) ? "YES" : "NO")
 #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 f6b050f5f8..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
@@ -42,7 +44,7 @@ template class optional {  // NOLINT
 
   optional(nullopt_t) {}
 
-  optional(T const &arg) : has_value_(true), value_(arg) {}
+  optional(T const &arg) : has_value_(true), value_(arg) {}  // NOLINT
 
   template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {}
 
diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp
deleted file mode 100644
index 8b41cbc7b5..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 *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;
-
-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;
-
-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_);
-  uint32_t len = (this->length_words_ + 1) * 4;
-
-  uint32_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;
-
-}  // namespace esphome
diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h
index bfea4c2336..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 bool DEFAULT_IN_FLASH = true;
-#else
-static bool DEFAULT_IN_FLASH = false;
-#endif
-#endif
+class ESPPreferenceObject {
+ public:
+  ESPPreferenceObject() = default;
+  ESPPreferenceObject(ESPPreferenceBackend *backend) : backend_(backend) {}
 
-#ifdef ARDUINO_ARCH_ESP32
-static 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;
-
-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 e61b2b13c6..3fe07f94b5 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -1,13 +1,14 @@
 #include "scheduler.h"
 #include "esphome/core/log.h"
 #include "esphome/core/helpers.h"
+#include "esphome/core/hal.h"
 #include 
 
 namespace esphome {
 
-static const char *TAG = "scheduler";
+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
 // #define ESPHOME_DEBUG_SCHEDULER
@@ -31,7 +32,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u
   item->timeout = timeout;
   item->last_execution = now;
   item->last_execution_major = this->millis_major_;
-  item->f = std::move(func);
+  item->void_callback = std::move(func);
   item->remove = false;
   this->push_(std::move(item));
 }
@@ -64,13 +65,47 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name,
   item->last_execution_major = this->millis_major_;
   if (item->last_execution > now)
     item->last_execution_major--;
-  item->f = std::move(func);
+  item->void_callback = std::move(func);
   item->remove = false;
   this->push_(std::move(item));
 }
 bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
   return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
 }
+
+void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
+                              uint8_t max_attempts, std::function &&func,
+                              float backoff_increase_factor) {
+  const uint32_t now = this->millis_();
+
+  if (!name.empty())
+    this->cancel_retry(component, name);
+
+  if (initial_wait_time == SCHEDULER_DONT_RUN)
+    return;
+
+  ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u,max_attempts=%u, backoff_factor=%0.1f)", name.c_str(),
+            initial_wait_time, max_attempts, backoff_increase_factor);
+
+  auto item = make_unique();
+  item->component = component;
+  item->name = name;
+  item->type = SchedulerItem::RETRY;
+  item->interval = initial_wait_time;
+  item->retry_countdown = max_attempts;
+  item->backoff_multiplier = backoff_increase_factor;
+  item->last_execution = now - initial_wait_time;
+  item->last_execution_major = this->millis_major_;
+  if (item->last_execution > now)
+    item->last_execution_major--;
+  item->retry_callback = std::move(func);
+  item->remove = false;
+  this->push_(std::move(item));
+}
+bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
+  return this->cancel_item_(component, name, SchedulerItem::RETRY);
+}
+
 optional HOT Scheduler::next_schedule_in() {
   if (this->empty_())
     return {};
@@ -81,7 +116,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();
 
@@ -94,10 +129,9 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() {
     ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
     while (!this->empty_()) {
       auto item = std::move(this->items_[0]);
-      const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
-      ESP_LOGVV(TAG, "  %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(),
-                item->interval, item->last_execution, item->last_execution_major, item->next_execution(),
-                item->next_execution_major());
+      ESP_LOGVV(TAG, "  %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(),
+                item->name.c_str(), item->interval, item->last_execution, item->last_execution_major,
+                item->next_execution(), item->next_execution_major());
 
       this->pop_raw_();
       old_items.push_back(std::move(item));
@@ -107,7 +141,28 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() {
   }
 #endif  // ESPHOME_DEBUG_SCHEDULER
 
+  auto to_remove_was = to_remove_;
+  auto items_was = items_.size();
+  // If we have too many items to remove
+  if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
+    std::vector> valid_items;
+    while (!this->empty_()) {
+      auto item = std::move(this->items_[0]);
+      this->pop_raw_();
+      valid_items.push_back(std::move(item));
+    }
+    this->items_ = std::move(valid_items);
+
+    // The following should not happen unless I'm missing something
+    if (to_remove_ != 0) {
+      ESP_LOGW(TAG, "to_remove_ was %u now: %u items where %zu now %zu. Please report this", to_remove_was, to_remove_,
+               items_was, items_.size());
+      to_remove_ = 0;
+    }
+  }
+
   while (!this->empty_()) {
+    RetryResult retry_result = RETRY;
     // use scoping to indicate visibility of `item` variable
     {
       // Don't copy-by value yet
@@ -126,15 +181,20 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() {
       }
 
 #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
-      const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
-      ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(),
-                item->interval, item->last_execution, now);
+      ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", item->get_type_str(),
+                item->name.c_str(), item->interval, item->last_execution, now);
 #endif
 
-      // Warning: During f(), a lot of stuff can happen, including:
+      // Warning: During callback(), 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};
+        if (item->type == SchedulerItem::RETRY)
+          retry_result = item->retry_callback();
+        else
+          item->void_callback();
+      }
     }
 
     {
@@ -147,16 +207,20 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() {
 
       if (item->remove) {
         // We were removed/cancelled in the function call, stop
+        to_remove_--;
         continue;
       }
 
-      if (item->type == SchedulerItem::INTERVAL) {
+      if (item->type == SchedulerItem::INTERVAL ||
+          (item->type == SchedulerItem::RETRY && (--item->retry_countdown > 0 && retry_result != RetryResult::DONE))) {
         if (item->interval != 0) {
           const uint32_t before = item->last_execution;
           const uint32_t amount = (now - item->last_execution) / item->interval;
           item->last_execution += amount * item->interval;
           if (item->last_execution < before)
             item->last_execution_major++;
+          if (item->type == SchedulerItem::RETRY)
+            item->interval *= item->backoff_multiplier;
         }
         this->push_(std::move(item));
       }
@@ -182,6 +246,7 @@ void HOT Scheduler::cleanup_() {
     if (!item->remove)
       return;
 
+    to_remove_--;
     this->pop_raw_();
   }
 }
@@ -193,7 +258,8 @@ void HOT Scheduler::push_(std::unique_ptr item) { this
 bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
   bool ret = false;
   for (auto &it : this->items_)
-    if (it->component == component && it->name == name && it->type == type) {
+    if (it->component == component && it->name == name && it->type == type && !it->remove) {
+      to_remove_++;
       it->remove = true;
       ret = true;
     }
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
index 5688058a1e..dc96d58329 100644
--- a/esphome/core/scheduler.h
+++ b/esphome/core/scheduler.h
@@ -15,6 +15,10 @@ class Scheduler {
   void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func);
   bool cancel_interval(Component *component, const std::string &name);
 
+  void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
+                 std::function &&func, float backoff_increase_factor = 1.0f);
+  bool cancel_retry(Component *component, const std::string &name);
+
   optional next_schedule_in();
 
   void call();
@@ -25,13 +29,20 @@ class Scheduler {
   struct SchedulerItem {
     Component *component;
     std::string name;
-    enum Type { TIMEOUT, INTERVAL } type;
+    enum Type { TIMEOUT, INTERVAL, RETRY } type;
     union {
       uint32_t interval;
       uint32_t timeout;
     };
     uint32_t last_execution;
-    std::function f;
+    // Ideally this should be a union or std::variant
+    // but unions don't work with object like std::function
+    //  union CallBack_{
+    std::function void_callback;
+    std::function retry_callback;
+    //  };
+    uint8_t retry_countdown{3};
+    float backoff_multiplier{1.0f};
     bool remove;
     uint8_t last_execution_major;
 
@@ -45,6 +56,18 @@ class Scheduler {
     }
 
     static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b);
+    const char *get_type_str() {
+      switch (this->type) {
+        case SchedulerItem::INTERVAL:
+          return "interval";
+        case SchedulerItem::RETRY:
+          return "retry";
+        case SchedulerItem::TIMEOUT:
+          return "timeout";
+        default:
+          return "";
+      }
+    }
   };
 
   uint32_t millis_();
@@ -61,6 +84,7 @@ class Scheduler {
   std::vector> to_add_;
   uint32_t last_millis_{0};
   uint8_t millis_major_{0};
+  uint32_t to_remove_{0};
 };
 
 }  // namespace esphome
diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp
index ea5e347c72..996cf8e310 100644
--- a/esphome/core/util.cpp
+++ b/esphome/core/util.cpp
@@ -4,91 +4,34 @@
 #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 ARDUINO_ARCH_ESP32
-#include 
-#endif
-#ifdef ARDUINO_ARCH_ESP8266
-#include 
+#ifdef USE_MQTT
+#include "esphome/components/mqtt/mqtt_client.h"
 #endif
 
 namespace esphome {
 
-bool network_is_connected() {
-#ifdef USE_ETHERNET
-  if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
-    return true;
+bool api_is_connected() {
+#ifdef USE_API
+  if (api::global_api_server != nullptr) {
+    return api::global_api_server->is_connected();
+  }
 #endif
-
-#ifdef USE_WIFI
-  if (wifi::global_wifi_component != nullptr)
-    return wifi::global_wifi_component->is_connected();
-#endif
-
   return false;
 }
 
-#ifdef ARDUINO_ARCH_ESP8266
-bool mdns_setup;
+bool mqtt_is_connected() {
+#ifdef USE_MQTT
+  if (mqtt::global_mqtt_client != nullptr) {
+    return mqtt::global_mqtt_client->is_connected();
+  }
 #endif
+  return false;
+}
 
-#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(), 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());
-    } else {
-#endif
-      // Publish "http" service if not using native API.
-      // This is just to have *some* mDNS service so that .local resolution works
-      MDNS.addService("http", "tcp", 80);
-      MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION);
-#ifdef USE_API
-    }
-#endif
-  }
-  void network_tick_mdns() {
-#ifdef ARDUINO_ARCH_ESP8266
-    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 "";
-  }
+bool remote_is_connected() { return api_is_connected() || mqtt_is_connected(); }
 
 }  // namespace esphome
diff --git a/esphome/core/util.h b/esphome/core/util.h
index 0e121ef382..1ca0173eab 100644
--- a/esphome/core/util.h
+++ b/esphome/core/util.h
@@ -1,22 +1,15 @@
 #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();
 
-/// 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();
+/// Return whether the node has an active connection to an MQTT broker
+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();
 
 }  // 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/core_config.py b/esphome/core_config.py
deleted file mode 100644
index 35f2fa1f80..0000000000
--- a/esphome/core_config.py
+++ /dev/null
@@ -1,267 +0,0 @@
-import logging
-import os
-import re
-
-import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome import automation, pins
-from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \
-    CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \
-    CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \
-    CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \
-    CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \
-    CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266_2_3_0, \
-    ARDUINO_VERSION_ESP8266_2_5_0, ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2
-from esphome.core import CORE, coroutine_with_priority
-from esphome.helpers import copy_file_if_changed, walk_files
-from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS
-
-_LOGGER = logging.getLogger(__name__)
-
-BUILD_FLASH_MODES = ['qio', 'qout', 'dio', 'dout']
-StartupTrigger = cg.esphome_ns.class_('StartupTrigger', cg.Component, automation.Trigger.template())
-ShutdownTrigger = cg.esphome_ns.class_('ShutdownTrigger', cg.Component,
-                                       automation.Trigger.template())
-LoopTrigger = cg.esphome_ns.class_('LoopTrigger', cg.Component,
-                                   automation.Trigger.template())
-
-VERSION_REGEX = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$')
-
-
-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('ESP32', 'ESP8266', upper=True)
-
-PLATFORMIO_ESP8266_LUT = {
-    '2.6.3': 'espressif8266@2.3.2',
-    '2.6.2': 'espressif8266@2.3.1',
-    '2.6.1': 'espressif8266@2.3.0',
-    '2.5.2': 'espressif8266@2.2.3',
-    '2.5.1': 'espressif8266@2.1.0',
-    '2.5.0': 'espressif8266@2.0.1',
-    '2.4.2': 'espressif8266@1.8.0',
-    '2.4.1': 'espressif8266@1.7.3',
-    '2.4.0': 'espressif8266@1.6.0',
-    '2.3.0': 'espressif8266@1.5.0',
-    'RECOMMENDED': 'espressif8266@2.2.3',
-    'LATEST': 'espressif8266',
-    'DEV': ARDUINO_VERSION_ESP8266_DEV,
-}
-
-PLATFORMIO_ESP32_LUT = {
-    '1.0.0': 'espressif32@1.4.0',
-    '1.0.1': 'espressif32@1.6.0',
-    '1.0.2': 'espressif32@1.9.0',
-    '1.0.3': 'espressif32@1.10.0',
-    '1.0.4': 'espressif32@1.11.0',
-    'RECOMMENDED': 'espressif32@1.11.0',
-    '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'}
-
-
-def valid_include(value):
-    try:
-        return cv.directory(value)
-    except cv.Invalid:
-        pass
-    value = cv.file_(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)))
-    return value
-
-
-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.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.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),
-        cv.Optional(CONF_PRIORITY, default=600.0): cv.float_,
-    }),
-    cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation({
-        cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ShutdownTrigger),
-    }),
-    cv.Optional(CONF_ON_LOOP): automation.validate_automation({
-        cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger),
-    }),
-    cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
-    cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(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,
-}, 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):
-    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:'.")
-        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])
-
-
-def include_file(path, basename):
-    parts = basename.split(os.path.sep)
-    dst = CORE.relative_src_path(*parts)
-    copy_file_if_changed(path, dst)
-
-    _, ext = os.path.splitext(path)
-    if ext in ['.h', '.hpp', '.tcc']:
-        # Header, add include statement
-        cg.add_global(cg.RawStatement(f'#include "{basename}"'))
-
-
-@coroutine_with_priority(-1000.0)
-def add_includes(includes):
-    # Add includes at the very end, so that the included files can access global variables
-    for include in includes:
-        path = CORE.relative_config_path(include)
-        if os.path.isdir(path):
-            # Directory, copy tree
-            for p in walk_files(path):
-                basename = os.path.relpath(p, os.path.dirname(path))
-                include_file(p, basename)
-        else:
-            # Copy file
-            basename = os.path.basename(path)
-            include_file(path, basename)
-
-
-@coroutine_with_priority(100.0)
-def to_code(config):
-    cg.add_global(cg.global_ns.namespace('esphome').using)
-    cg.add(cg.App.pre_setup(config[CONF_NAME], cg.RawExpression('__DATE__ ", " __TIME__')))
-
-    for conf in config.get(CONF_ON_BOOT, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
-
-    for conf in config.get(CONF_ON_SHUTDOWN, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
-
-    for conf in config.get(CONF_ON_LOOP, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
-
-    # Build flags
-    if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \
-            CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0:
-        flash_size = ESP8266_FLASH_SIZES[CORE.board]
-        ld_scripts = ESP8266_LD_SCRIPTS[flash_size]
-        ld_script = None
-
-        if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3',
-                                    'espressif8266@1.6.0'):
-            ld_script = ld_scripts[0]
-        elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0,
-                                      ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2):
-            ld_script = ld_scripts[1]
-
-        if ld_script is not None:
-            cg.add_build_flag(f'-Wl,-T{ld_script}')
-
-    cg.add_build_flag('-fno-exceptions')
-
-    # Libraries
-    if CORE.is_esp32:
-        cg.add_library('ESPmDNS', None)
-    elif CORE.is_esp8266:
-        cg.add_library('ESP8266WiFi', None)
-        cg.add_library('ESP8266mDNS', None)
-
-    for lib in config[CONF_LIBRARIES]:
-        if '@' in lib:
-            name, vers = lib.split('@', 1)
-            cg.add_library(name, vers)
-        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])
diff --git a/esphome/coroutine.py b/esphome/coroutine.py
new file mode 100644
index 0000000000..58f79c6b36
--- /dev/null
+++ b/esphome/coroutine.py
@@ -0,0 +1,252 @@
+"""
+ESPHome's coroutine system.
+
+The Problem: When running the code generationg, components can depend on variables being registered.
+For example, an i2c-based sensor would need the i2c bus component to first be declared before the
+codegen can emit code using that variable (or otherwise the C++ won't compile).
+
+ESPHome's codegen system solves this by using coroutine-like methods. When a component depends on
+a variable, it waits for it to be registered using `await cg.get_variable()`. If the variable
+hasn't been registered yet, control will be yielded back to another component until the variable
+is registered. This leads to a topological sort, solving the dependency problem.
+
+Importantly, ESPHome only uses the coroutine *syntax*, no actual asyncio event loop is running in
+the background. This is so that we can ensure the order of execution is constant for the same
+YAML configuration, thus main.cpp only has to be recompiled if the configuration actually changes.
+
+There are two syntaxes for ESPHome coroutines ("old style" vs "new style" coroutines).
+
+"new style" - This is very much like coroutines you might be used to:
+
+```py
+async def my_coroutine(config):
+    var = await cg.get_variable(config[CONF_ID])
+    await some_other_coroutine(xyz)
+    return var
+```
+
+new style coroutines are `async def` methods that use `await` to await the result of another coroutine,
+and can return values using a `return` statement.
+
+"old style" - This was a hack for when ESPHome still had to run on python 2, but is still compatible
+
+```py
+@coroutine
+def my_coroutine(config):
+    var = yield cg.get_variable(config[CONF_ID])
+    yield some_other_coroutine(xyz)
+    yield var
+```
+
+Here everything is combined in `yield` expressions. You await other coroutines using `yield` and
+the last `yield` expression defines what is returned.
+"""
+
+import collections
+import functools
+import heapq
+import inspect
+import logging
+import types
+from typing import Any, Awaitable, Callable, Generator, Iterator, List, Tuple
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def coroutine(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]:
+    """Decorator to apply to methods to convert them to ESPHome coroutines."""
+    if getattr(func, "_esphome_coroutine", False):
+        # If func is already a coroutine, do not re-wrap it (performance)
+        return func
+    if inspect.isasyncgenfunction(func):
+        # Trade-off: In ESPHome, there's not really a use-case for async generators.
+        # and during the transition to new-style syntax it will happen that a `yield`
+        # is not replaced properly, so don't accept async generators.
+        raise ValueError(
+            f"Async generator functions are not allowed. "
+            f"Please check whether you've replaced all yields with awaits/returns. "
+            f"See {func} in {func.__module__}"
+        )
+    if inspect.iscoroutinefunction(func):
+        # A new-style async-def coroutine function, no conversion needed.
+        return func
+
+    if inspect.isgeneratorfunction(func):
+
+        @functools.wraps(func)
+        def coro(*args, **kwargs):
+            gen = func(*args, **kwargs)
+            ret = yield from _flatten_generator(gen)
+            return ret
+
+    else:
+        # A "normal" function with no `yield` statements, convert to generator
+        # that includes a yield just so it's also a generator function
+        @functools.wraps(func)
+        def coro(*args, **kwargs):
+            res = func(*args, **kwargs)
+            yield
+            return res
+
+    # Add coroutine internal python flag so that it can be awaited from new-style coroutines.
+    coro = types.coroutine(coro)
+    # pylint: disable=protected-access
+    coro._esphome_coroutine = True
+    return coro
+
+
+def coroutine_with_priority(priority: float):
+    """Decorator to apply to functions to convert them to ESPHome coroutines.
+
+    :param priority: priority with which to schedule the coroutine, higher priorities run first.
+    """
+
+    def decorator(func):
+        coro = coroutine(func)
+        coro.priority = priority
+        return coro
+
+    return decorator
+
+
+def _flatten_generator(gen: Generator[Any, Any, Any]):
+    to_send = None
+    while True:
+        try:
+            # Run until next yield expression
+            val = gen.send(to_send)
+        except StopIteration as e:
+            # return statement or end of function
+
+            # From py3.3, return with a value is allowed in generators,
+            # and return value is transported in the value field of the exception.
+            # If we find a value in the exception, use that as the return value,
+            # otherwise use the value from the last yield statement ("old style")
+            ret = to_send if e.value is None else e.value
+            return ret
+
+        if isinstance(val, collections.abc.Awaitable):
+            # yielded object that is awaitable (like `yield some_new_style_method()`)
+            # yield from __await__() like actual coroutines would.
+            to_send = yield from val.__await__()
+        elif inspect.isgenerator(val):
+            # Old style, like `yield cg.get_variable()`
+            to_send = yield from _flatten_generator(val)
+        else:
+            # Could be the last expression from this generator, record this as the return value
+            to_send = val
+            # perform a yield so that expressions like `while some_condition(): yield None`
+            # do not run without yielding control back to the top
+            yield
+
+
+class FakeAwaitable:
+    """Convert a generator to an awaitable object.
+
+    Needed for internals of `cg.get_variable`. There we can't use @coroutine because
+    native coroutines await from types.coroutine() directly without yielding back control to the top
+    (likely as a performance enhancement).
+
+    If we instead wrap the generator in this FakeAwaitable, control is yielded back to the top
+    (reason unknown).
+    """
+
+    def __init__(self, gen: Generator[Any, Any, Any]) -> None:
+        self._gen = gen
+
+    def __await__(self):
+        ret = yield from self._gen
+        return ret
+
+
+@functools.total_ordering
+class _Task:
+    def __init__(
+        self,
+        priority: float,
+        id_number: int,
+        iterator: Iterator[None],
+        original_function: Any,
+    ):
+        self.priority = priority
+        self.id_number = id_number
+        self.iterator = iterator
+        self.original_function = original_function
+
+    def with_priority(self, priority: float) -> "_Task":
+        return _Task(priority, self.id_number, self.iterator, self.original_function)
+
+    @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 FakeEventLoop:
+    """Emulate an asyncio EventLoop to run some registered coroutine jobs in sequence."""
+
+    def __init__(self):
+        self._pending_tasks: List[_Task] = []
+        self._task_counter = 0
+
+    def add_job(self, func, *args, **kwargs):
+        """Add a job to the task queue,
+
+        Optionally retrieves priority from the function object, and schedules according to that.
+        """
+        if inspect.iscoroutine(func):
+            raise ValueError("Can only add coroutine functions, not coroutine objects")
+        if inspect.iscoroutinefunction(func):
+            coro = func
+            gen = coro(*args, **kwargs).__await__()
+        else:
+            coro = coroutine(func)
+            gen = coro(*args, **kwargs)
+        prio = getattr(coro, "priority", 0.0)
+        task = _Task(prio, self._task_counter, gen, func)
+        self._task_counter += 1
+        heapq.heappush(self._pending_tasks, task)
+
+    def flush_tasks(self):
+        """Run until all tasks have been completed.
+
+        :raises RuntimeError: if a deadlock is detected.
+        """
+        i = 0
+        while self._pending_tasks:
+            i += 1
+            if i > 1000000:
+                # Detect deadlock/circular dependency by measuring how many times tasks have been
+                # executed. On the big tests/test1.yaml we only get to a fraction of this, so
+                # this shouldn't be a problem.
+                raise RuntimeError(
+                    "Circular dependency detected! "
+                    "Please run with -v option to see what functions failed to "
+                    "complete."
+                )
+
+            task: _Task = heapq.heappop(self._pending_tasks)
+            _LOGGER.debug(
+                "Running %s in %s (num %s)",
+                task.original_function.__qualname__,
+                task.original_function.__module__,
+                task.id_number,
+            )
+
+            try:
+                next(task.iterator)
+                # Decrease priority over time, so that if this task is blocked
+                # due to a dependency others will clear the dependency
+                # This could be improved with a less naive approach
+                new_task = task.with_priority(task.priority - 1)
+                heapq.heappush(self._pending_tasks, new_task)
+            except StopIteration:
+                _LOGGER.debug(" -> finished")
diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py
index e9bcdc7d1f..937b6cceb4 100644
--- a/esphome/cpp_generator.py
+++ b/esphome/cpp_generator.py
@@ -1,14 +1,27 @@
 import abc
 import inspect
 import math
+import re
+from esphome.yaml_util import ESPHomeDataBase
 
 # pylint: disable=unused-import, wrong-import-order
 from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence
 
 from esphome.core import (  # noqa
-    CORE, HexInt, ID, Lambda, TimePeriod, TimePeriodMicroseconds,
-    TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine, Library, Define,
-    EnumValue)
+    CORE,
+    HexInt,
+    ID,
+    Lambda,
+    TimePeriod,
+    TimePeriodMicroseconds,
+    TimePeriodMilliseconds,
+    TimePeriodMinutes,
+    TimePeriodSeconds,
+    coroutine,
+    Library,
+    Define,
+    EnumValue,
+)
 from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
 from esphome.util import OrderedDict
 
@@ -23,12 +36,23 @@ class Expression(abc.ABC):
         """
 
 
-SafeExpType = Union[Expression, bool, str, str, int, float, TimePeriod,
-                    Type[bool], Type[int], Type[float], Sequence[Any]]
+SafeExpType = Union[
+    Expression,
+    bool,
+    str,
+    str,
+    int,
+    float,
+    TimePeriod,
+    Type[bool],
+    Type[int],
+    Type[float],
+    Sequence[Any],
+]
 
 
 class RawExpression(Expression):
-    __slots__ = ("text", )
+    __slots__ = ("text",)
 
     def __init__(self, text: str):
         self.text = text
@@ -66,7 +90,7 @@ class VariableDeclarationExpression(Expression):
 
 
 class ExpressionList(Expression):
-    __slots__ = ("args", )
+    __slots__ = ("args",)
 
     def __init__(self, *args: Optional[SafeExpType]):
         # Remove every None on end
@@ -84,13 +108,13 @@ class ExpressionList(Expression):
 
 
 class TemplateArguments(Expression):
-    __slots__ = ("args", )
+    __slots__ = ("args",)
 
     def __init__(self, *args: SafeExpType):
         self.args = ExpressionList(*args)
 
     def __str__(self):
-        return f'<{self.args}>'
+        return f"<{self.args}>"
 
     def __iter__(self):
         return iter(self.args)
@@ -110,8 +134,8 @@ class CallExpression(Expression):
 
     def __str__(self):
         if self.template_args is not None:
-            return f'{self.base}{self.template_args}({self.args})'
-        return f'{self.base}({self.args})'
+            return f"{self.base}{self.template_args}({self.args})"
+        return f"{self.base}({self.args})"
 
 
 class StructInitializer(Expression):
@@ -130,10 +154,10 @@ class StructInitializer(Expression):
             self.args[key] = exp
 
     def __str__(self):
-        cpp = f'{self.base}{{\n'
+        cpp = f"{self.base}{{\n"
         for key, value in self.args.items():
-            cpp += f'  .{key} = {value},\n'
-        cpp += '}'
+            cpp += f"  .{key} = {value},\n"
+        cpp += "}"
         return cpp
 
 
@@ -151,14 +175,14 @@ class ArrayInitializer(Expression):
 
     def __str__(self):
         if not self.args:
-            return '{}'
+            return "{}"
         if self.multiline:
-            cpp = '{\n'
+            cpp = "{\n"
             for arg in self.args:
-                cpp += f'  {arg},\n'
-            cpp += '}'
+                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
 
 
@@ -174,9 +198,11 @@ class ParameterExpression(Expression):
 
 
 class ParameterListExpression(Expression):
-    __slots__ = ("parameters", )
+    __slots__ = ("parameters",)
 
-    def __init__(self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]]):
+    def __init__(
+        self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]]
+    ):
         self.parameters = []
         for parameter in parameters:
             if not isinstance(parameter, ParameterExpression):
@@ -188,26 +214,32 @@ class ParameterListExpression(Expression):
 
 
 class LambdaExpression(Expression):
-    __slots__ = ("parts", "parameters", "capture", "return_type")
+    __slots__ = ("parts", "parameters", "capture", "return_type", "source")
 
-    def __init__(self, parts, parameters, capture: str = '=', return_type=None):
+    def __init__(
+        self, parts, parameters, capture: str = "=", return_type=None, source=None
+    ):
         self.parts = parts
         if not isinstance(parameters, ParameterListExpression):
             parameters = ParameterListExpression(*parameters)
         self.parameters = parameters
+        self.source = source
         self.capture = capture
         self.return_type = safe_exp(return_type) if return_type is not None else None
 
     def __str__(self):
-        cpp = f'[{self.capture}]({self.parameters})'
+        cpp = f"[{self.capture}]({self.parameters})"
         if self.return_type is not None:
-            cpp += f' -> {self.return_type}'
-        cpp += f' {{\n{self.content}\n}}'
+            cpp += f" -> {self.return_type}"
+        cpp += " {\n"
+        if self.source is not None:
+            cpp += f"{self.source.as_line_directive}\n"
+        cpp += f"{self.content}\n}}"
         return indent_all_but_first_and_last(cpp)
 
     @property
     def content(self):
-        return ''.join(str(part) for part in self.parts)
+        return "".join(str(part) for part in self.parts)
 
 
 # pylint: disable=abstract-method
@@ -216,9 +248,10 @@ class Literal(Expression, metaclass=abc.ABCMeta):
 
 
 class StringLiteral(Literal):
-    __slots__ = ("string", )
+    __slots__ = ("string",)
 
     def __init__(self, string: str):
+        super().__init__()
         self.string = string
 
     def __str__(self):
@@ -226,23 +259,24 @@ class StringLiteral(Literal):
 
 
 class IntLiteral(Literal):
-    __slots__ = ("i", )
+    __slots__ = ("i",)
 
     def __init__(self, i: int):
+        super().__init__()
         self.i = i
 
     def __str__(self):
         if self.i > 4294967295:
-            return f'{self.i}ULL'
+            return f"{self.i}ULL"
         if self.i > 2147483647:
-            return f'{self.i}UL'
+            return f"{self.i}UL"
         if self.i < -2147483648:
-            return f'{self.i}LL'
+            return f"{self.i}LL"
         return str(self.i)
 
 
 class BoolLiteral(Literal):
-    __slots__ = ("binary", )
+    __slots__ = ("binary",)
 
     def __init__(self, binary: bool):
         super().__init__()
@@ -253,9 +287,10 @@ class BoolLiteral(Literal):
 
 
 class HexIntLiteral(Literal):
-    __slots__ = ("i", )
+    __slots__ = ("i",)
 
     def __init__(self, i: int):
+        super().__init__()
         self.i = HexInt(i)
 
     def __str__(self):
@@ -263,9 +298,10 @@ class HexIntLiteral(Literal):
 
 
 class FloatLiteral(Literal):
-    __slots__ = ("f", )
+    __slots__ = ("f",)
 
     def __init__(self, value: float):
+        super().__init__()
         self.f = value
 
     def __str__(self):
@@ -274,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.
@@ -311,11 +372,13 @@ def safe_exp(obj: SafeExpType) -> Expression:
     if obj is float:
         return float_
     if isinstance(obj, ID):
-        raise ValueError("Object {} is an ID. Did you forget to register the variable?"
-                         "".format(obj))
+        raise ValueError(
+            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 "
-                         "'yield'?".format(obj))
+        raise ValueError(
+            f"Object {obj} is a coroutine. Did you forget to await the expression with 'await'?"
+        )
     raise ValueError("Object is not an expression", obj)
 
 
@@ -330,7 +393,7 @@ class Statement(abc.ABC):
 
 
 class RawStatement(Statement):
-    __slots__ = ("text", )
+    __slots__ = ("text",)
 
     def __init__(self, text: str):
         self.text = text
@@ -340,7 +403,7 @@ class RawStatement(Statement):
 
 
 class ExpressionStatement(Statement):
-    __slots__ = ("expression", )
+    __slots__ = ("expression",)
 
     def __init__(self, expression):
         self.expression = safe_exp(expression)
@@ -350,46 +413,64 @@ class ExpressionStatement(Statement):
 
 
 class LineComment(Statement):
-    __slots__ = ("value", )
+    __slots__ = ("value",)
 
     def __init__(self, value: str):
         self.value = value
 
     def __str__(self):
-        parts = self.value.split('\n')
-        parts = [f'// {x}' for x in parts]
-        return '\n'.join(parts)
+        parts = re.sub(r"\\\s*\n", r"\n", self.value, re.MULTILINE).split("\n")
+        parts = [f"// {x}" for x in parts]
+        return "\n".join(parts)
 
 
 class ProgmemAssignmentExpression(AssignmentExpression):
     __slots__ = ()
 
     def __init__(self, type_, name, rhs, obj):
-        super().__init__(type_, '', name, rhs, obj)
+        super().__init__(type_, "", name, rhs, obj)
 
     def __str__(self):
         return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}"
 
 
+class StaticConstAssignmentExpression(AssignmentExpression):
+    __slots__ = ()
+
+    def __init__(self, type_, name, rhs, obj):
+        super().__init__(type_, "", name, rhs, obj)
+
+    def __str__(self):
+        return f"static const {self.type} {self.name}[] = {self.rhs}"
+
+
 def progmem_array(id_, rhs) -> "MockObj":
     rhs = safe_exp(rhs)
-    obj = MockObj(id_, '.')
+    obj = MockObj(id_, ".")
     assignment = ProgmemAssignmentExpression(id_.type, id_, rhs, obj)
     CORE.add(assignment)
     CORE.register_variable(id_, obj)
     return obj
 
 
+def static_const_array(id_, rhs) -> "MockObj":
+    rhs = safe_exp(rhs)
+    obj = MockObj(id_, ".")
+    assignment = StaticConstAssignmentExpression(id_.type, id_, rhs, obj)
+    CORE.add(assignment)
+    CORE.register_variable(id_, obj)
+    return obj
+
+
 def statement(expression: Union[Expression, Statement]) -> Statement:
-    """Convert expression into a statement unless is already a statement.
-    """
+    """Convert expression into a statement unless is already a statement."""
     if isinstance(expression, Statement):
         return expression
     return ExpressionStatement(expression)
 
 
 def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
-    """Declare a new variable (not pointer type) in the code generation.
+    """Declare a new variable, not pointer type, in the code generation.
 
     :param id_: The ID used to declare the variable.
     :param rhs: The expression to place on the right hand side of the assignment.
@@ -400,10 +481,33 @@ def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
     """
     assert isinstance(id_, ID)
     rhs = safe_exp(rhs)
-    obj = MockObj(id_, '.')
+    obj = MockObj(id_, ".")
     if type_ is not None:
         id_.type = type_
-    assignment = AssignmentExpression(id_.type, '', id_, rhs, obj)
+    assignment = AssignmentExpression(id_.type, "", id_, rhs, obj)
+    CORE.add(assignment)
+    CORE.register_variable(id_, obj)
+    return obj
+
+
+def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
+    """Declare and define a new variable, not pointer type, in the code generation.
+
+    :param id_: The ID used to declare the variable.
+    :param rhs: The expression to place on the right hand side of the assignment.
+    :param type_: Manually define a type for the variable, only use this when it's not possible
+      to do so during config validation phase (for example because of template arguments).
+
+    :returns The new variable as a MockObj.
+    """
+    assert isinstance(id_, ID)
+    rhs = safe_exp(rhs)
+    obj = MockObj(id_, ".")
+    if type_ is not None:
+        id_.type = type_
+    decl = VariableDeclarationExpression(id_.type, "", id_)
+    CORE.add_global(decl)
+    assignment = AssignmentExpression(None, "", id_, rhs, obj)
     CORE.add(assignment)
     CORE.register_variable(id_, obj)
     return obj
@@ -420,10 +524,10 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
     :returns The new variable as a MockObj.
     """
     rhs = safe_exp(rhs)
-    obj = MockObj(id_, '->')
+    obj = MockObj(id_, "->")
     if type_ is not None:
         id_.type = type_
-    decl = VariableDeclarationExpression(id_.type, '*', id_)
+    decl = VariableDeclarationExpression(id_.type, "*", id_)
     CORE.add_global(decl)
     assignment = AssignmentExpression(None, None, id_, rhs, obj)
     CORE.add(assignment)
@@ -462,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):
@@ -487,45 +591,46 @@ def add_define(name: str, value: SafeExpType = None):
         CORE.add_define(Define(name, safe_exp(value)))
 
 
-@coroutine
-def get_variable(id_: ID) -> Generator["MockObj", None, None]:
+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
     return it as a MockObj.
 
-    This is a coroutine, you need to await it with a 'yield' expression!
+    This is a coroutine, you need to await it with a 'await' expression!
 
     :param id_: The ID to retrieve
     :return: The variable as a MockObj.
     """
-    var = yield CORE.get_variable(id_)
-    yield var
+    return await CORE.get_variable(id_)
 
 
-@coroutine
-def get_variable_with_full_id(id_: ID) -> Generator[Tuple[ID, "MockObj"], None, None]:
+async def get_variable_with_full_id(id_: ID) -> Tuple[ID, "MockObj"]:
     """
     Wait for the given ID to be defined in the code generation and
     return it as a MockObj.
 
-    This is a coroutine, you need to await it with a 'yield' expression!
+    This is a coroutine, you need to await it with a 'await' expression!
 
     :param id_: The ID to retrieve
     :return: The variable as a MockObj.
     """
-    full_id, var = yield CORE.get_variable_with_full_id(id_)
-    yield full_id, var
+    return await CORE.get_variable_with_full_id(id_)
 
 
-@coroutine
-def process_lambda(
-        value: Lambda, parameters: List[Tuple[SafeExpType, str]],
-        capture: str = '=', return_type: SafeExpType = None
+async def process_lambda(
+    value: Lambda,
+    parameters: List[Tuple[SafeExpType, str]],
+    capture: str = "=",
+    return_type: SafeExpType = None,
 ) -> Generator[LambdaExpression, None, None]:
     """Process the given lambda value into a LambdaExpression.
 
     This is a coroutine because lambdas can depend on other IDs,
-    you need to await it with 'yield'!
+    you need to await it with 'await'!
 
     :param value: The lambda to process.
     :param parameters: The parameters to pass to the Lambda, list of tuples
@@ -533,25 +638,36 @@ 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:
-        yield
         return
     parts = value.parts[:]
     for i, id in enumerate(value.requires_ids):
-        full_id, var = yield CORE.get_variable_with_full_id(id)
-        if full_id is not None and isinstance(full_id.type, MockObjClass) and \
-                full_id.type.inherits_from(GlobalsComponent):
+        full_id, var = await get_variable_with_full_id(id)
+        if (
+            full_id is not None
+            and isinstance(full_id.type, MockObjClass)
+            and (
+                full_id.type.inherits_from(GlobalsComponent)
+                or full_id.type.inherits_from(RestoringGlobalsComponent)
+            )
+        ):
             parts[i * 3 + 1] = var.value()
             continue
 
-        if parts[i * 3 + 2] == '.':
+        if parts[i * 3 + 2] == ".":
             parts[i * 3 + 1] = var._
         else:
             parts[i * 3 + 1] = var
-        parts[i * 3 + 2] = ''
-    yield LambdaExpression(parts, parameters, capture, return_type)
+        parts[i * 3 + 2] = ""
+
+    if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
+        location = value.esp_range.start_mark
+        location.line += value.content_offset
+    else:
+        location = None
+    return LambdaExpression(parts, parameters, capture, return_type, location)
 
 
 def is_template(value):
@@ -559,11 +675,12 @@ def is_template(value):
     return isinstance(value, Lambda)
 
 
-@coroutine
-def templatable(value: Any,
-                args: List[Tuple[SafeExpType, str]],
-                output_type: Optional[SafeExpType],
-                to_exp: Any = None):
+async def templatable(
+    value: Any,
+    args: List[Tuple[SafeExpType, str]],
+    output_type: Optional[SafeExpType],
+    to_exp: Any = None,
+):
     """Generate code for a templatable config option.
 
     If `value` is a templated value, the lambda expression is returned.
@@ -576,15 +693,12 @@ def templatable(value: Any,
     :return: The potentially templated value.
     """
     if is_template(value):
-        lambda_ = yield process_lambda(value, args, return_type=output_type)
-        yield lambda_
-    else:
-        if to_exp is None:
-            yield value
-        elif isinstance(to_exp, dict):
-            yield to_exp[value]
-        else:
-            yield to_exp(value)
+        return await process_lambda(value, args, return_type=output_type)
+    if to_exp is None:
+        return value
+    if isinstance(to_exp, dict):
+        return to_exp[value]
+    return to_exp(value)
 
 
 class MockObj(Expression):
@@ -592,20 +706,24 @@ class MockObj(Expression):
 
     Mostly consists of magic methods that allow ESPHome's codegen syntax.
     """
+
     __slots__ = ("base", "op")
 
-    def __init__(self, base, op='.'):
+    def __init__(self, base, op="."):
         self.base = base
         self.op = op
 
     def __getattr__(self, attr: str) -> "MockObj":
-        next_op = '.'
-        if attr.startswith('P') and self.op not in ['::', '']:
+        # prevent python dunder methods being replaced by mock objects
+        if attr.startswith("__"):
+            raise AttributeError()
+        next_op = "."
+        if attr.startswith("P") and self.op not in ["::", ""]:
             attr = attr[1:]
-            next_op = '->'
-        if attr.startswith('_'):
+            next_op = "->"
+        if attr.startswith("_"):
             attr = attr[1:]
-        return MockObj(f'{self.base}{self.op}{attr}', next_op)
+        return MockObj(f"{self.base}{self.op}{attr}", next_op)
 
     def __call__(self, *args):  # type: (SafeExpType) -> MockObj
         call = CallExpression(self.base, *args)
@@ -615,29 +733,30 @@ 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":
-        return MockObj(f'{self.base}{self.op}')
+        return MockObj(f"{self.base}{self.op}")
 
     @property
     def new(self) -> "MockObj":
-        return MockObj(f'new {self.base}', '->')
+        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:
             args = args[0]
-        return MockObj(f'{self.base}{args}')
+        return MockObj(f"{self.base}{args}")
 
     def namespace(self, name: str) -> "MockObj":
-        return MockObj(f'{self._}{name}', '::')
+        return MockObj(f"{self._}{name}", "::")
 
     def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass":
-        op = '' if self.op == '' else '::'
-        return MockObjClass(f'{self.base}{op}{name}', '.', parents=parents)
+        op = "" if self.op == "" else "::"
+        return MockObjClass(f"{self.base}{op}{name}", ".", parents=parents)
 
     def struct(self, name: str) -> "MockObjClass":
         return self.class_(name)
@@ -646,50 +765,210 @@ class MockObj(Expression):
         return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op)
 
     def operator(self, name: str) -> "MockObj":
-        if name == 'ref':
-            return MockObj(f'{self.base} &', '')
-        if name == 'ptr':
-            return MockObj(f'{self.base} *', '')
+        """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":
+            return MockObj(f"{self.base} *", "")
         if name == "const":
-            return MockObj(f'const {self.base}', '')
+            return MockObj(f"const {self.base}", "")
         raise ValueError("Expected one of ref, ptr, const.")
 
     @property
     def using(self) -> "MockObj":
-        assert self.op == '::'
-        return MockObj(f'using namespace {self.base}')
+        assert self.op == "::"
+        return MockObj(f"using namespace {self.base}")
 
     def __getitem__(self, item: Union[str, Expression]) -> "MockObj":
-        next_op = '.'
-        if isinstance(item, str) and item.startswith('P'):
+        next_op = "."
+        if isinstance(item, str) and item.startswith("P"):
             item = item[1:]
-            next_op = '->'
-        return MockObj(f'{self.base}[{item}]', next_op)
+            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):
-        self._enum = kwargs.pop('enum')
-        self._is_class = kwargs.pop('is_class')
-        base = kwargs.pop('base')
+        self._enum = kwargs.pop("enum")
+        self._is_class = kwargs.pop("is_class")
+        base = kwargs.pop("base")
         if self._is_class:
-            base = base + '::' + self._enum
-            kwargs['op'] = '::'
-        kwargs['base'] = base
+            base = f"{base}::{self._enum}"
+            kwargs["op"] = "::"
+        kwargs["base"] = base
         MockObj.__init__(self, *args, **kwargs)
 
     def __str__(self):
         if self._is_class:
             return super().__str__()
-        return f'{self.base}{self.op}{self._enum}'
+        return f"{self.base}{self.op}{self._enum}"
 
     def __repr__(self):
-        return f'MockObj<{str(self.base)}>'
+        return f"MockObj<{str(self.base)}>"
 
 
 class MockObjClass(MockObj):
     def __init__(self, *args, **kwargs):
-        parens = kwargs.pop('parents')
+        parens = kwargs.pop("parents")
         MockObj.__init__(self, *args, **kwargs)
         self._parents = []
         for paren in parens:
@@ -700,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
 
@@ -714,7 +993,7 @@ class MockObjClass(MockObj):
             args = args[0]
         new_parents = self._parents[:]
         new_parents.append(self)
-        return MockObjClass(f'{self.base}{args}', parents=new_parents)
+        return MockObjClass(f"{self.base}{args}", parents=new_parents)
 
     def __repr__(self):
-        return f'MockObjClass<{str(self.base)}, parents={self._parents}>'
+        return f"MockObjClass<{str(self.base)}, parents={self._parents}>"
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index f01981acc8..9127f88e39 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -1,82 +1,128 @@
-from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \
-    CONF_UPDATE_INTERVAL, CONF_TYPE_ID
+import logging
+
+from esphome.const import (
+    CONF_DISABLED_BY_DEFAULT,
+    CONF_ENTITY_CATEGORY,
+    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
 
 
-@coroutine
-def gpio_pin_expression(conf):
+_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 'yield' expression!
+    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:
-            yield coroutine(func)(conf)
-            return
-
-    number = conf[CONF_NUMBER]
-    mode = conf[CONF_MODE]
-    inverted = conf.get(CONF_INVERTED)
-    yield GPIOPin.new(number, RawExpression(mode), inverted)
+            return await coroutine(func)(conf)
+    return await coroutine(pins.PIN_SCHEMA_REGISTRY[CORE.target_platform][0])(conf)
 
 
-@coroutine
-def register_component(var, config):
+async def register_component(var, config):
     """Register the given obj as a component.
 
-    This is a coroutine, you must await it with a 'yield' expression!
+    This is a coroutine, you must await it with a 'await' expression!
 
     :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_))
+        raise ValueError(
+            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))
-    yield var
+    return var
 
 
-@coroutine
-def register_parented(var, value):
+async def register_parented(var, value):
     if isinstance(value, ID):
-        paren = yield get_variable(value)
+        paren = await get_variable(value)
     else:
         paren = 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]))
+    if CONF_ENTITY_CATEGORY in config:
+        add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
+
+
 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)
     return registry[key], config
 
 
-@coroutine
-def build_registry_entry(registry, full_config):
+async def build_registry_entry(registry, full_config):
     registry_entry, config = extract_registry_entry_config(registry, full_config)
     type_id = full_config[CONF_TYPE_ID]
     builder = registry_entry.coroutine_fun
-    yield builder(config, type_id)
+    return await builder(config, type_id)
 
 
-@coroutine
-def build_registry_list(registry, config):
+async def build_registry_list(registry, config):
     actions = []
     for conf in config:
-        action = yield build_registry_entry(registry, conf)
+        action = await build_registry_entry(registry, conf)
         actions.append(action)
-    yield actions
+    return actions
diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py
index 4a9dce332b..13d088e1cb 100644
--- a/esphome/cpp_types.py
+++ b/esphome/cpp_types.py
@@ -1,33 +1,37 @@
 from esphome.cpp_generator import MockObj
 
-global_ns = MockObj('', '')
-void = global_ns.namespace('void')
-nullptr = global_ns.namespace('nullptr')
-float_ = global_ns.namespace('float')
-double = global_ns.namespace('double')
-bool_ = global_ns.namespace('bool')
-int_ = global_ns.namespace('int')
-std_ns = global_ns.namespace('std')
-std_string = std_ns.class_('string')
-std_vector = std_ns.class_('vector')
-uint8 = global_ns.namespace('uint8_t')
-uint16 = global_ns.namespace('uint16_t')
-uint32 = global_ns.namespace('uint32_t')
-int32 = global_ns.namespace('int32_t')
-const_char_ptr = global_ns.namespace('const char *')
-NAN = global_ns.namespace('NAN')
+global_ns = MockObj("", "")
+void = global_ns.namespace("void")
+nullptr = global_ns.namespace("nullptr")
+float_ = global_ns.namespace("float")
+double = global_ns.namespace("double")
+bool_ = global_ns.namespace("bool")
+int_ = global_ns.namespace("int")
+std_ns = global_ns.namespace("std")
+std_string = std_ns.class_("string")
+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')
-Component = esphome_ns.class_('Component')
-ComponentPtr = Component.operator('ptr')
-PollingComponent = esphome_ns.class_('PollingComponent', Component)
-Application = esphome_ns.class_('Application')
-optional = esphome_ns.class_('optional')
-arduino_json_ns = global_ns.namespace('ArduinoJson')
-JsonObject = arduino_json_ns.class_('JsonObject')
-JsonObjectRef = JsonObject.operator('ref')
-JsonObjectConstRef = JsonObjectRef.operator('const')
-Controller = esphome_ns.class_('Controller')
-
-GPIOPin = esphome_ns.class_('GPIOPin')
+EntityBase = esphome_ns.class_("EntityBase")
+Component = esphome_ns.class_("Component")
+ComponentPtr = Component.operator("ptr")
+PollingComponent = esphome_ns.class_("PollingComponent", Component)
+Application = esphome_ns.class_("Application")
+optional = esphome_ns.class_("optional")
+arduino_json_ns = global_ns.namespace("ArduinoJson")
+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)
+EntityCategory = esphome_ns.enum("EntityCategory")
diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index 4f2d63d545..5e5cc4ecd2 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -9,6 +9,8 @@ import json
 import logging
 import multiprocessing
 import os
+from pathlib import Path
+import secrets
 import shutil
 import subprocess
 import threading
@@ -25,53 +27,60 @@ import tornado.process
 import tornado.web
 import tornado.websocket
 
-from esphome import const, util
-from esphome.__main__ import get_serial_ports
+from esphome import const, platformio_api, util, yaml_util
 from esphome.helpers import mkdir_p, get_bool_env, run_system_command
-from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \
-    esphome_storage_path, ext_storage_path, trash_storage_path
-from esphome.util import shlex_quote
+from esphome.storage_json import (
+    EsphomeStorageJSON,
+    StorageJSON,
+    esphome_storage_path,
+    ext_storage_path,
+    trash_storage_path,
+)
+from esphome.util import shlex_quote, get_serial_ports
+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__)
 
+ENV_DEV = "ESPHOME_DASHBOARD_DEV"
+
 
 class DashboardSettings:
     def __init__(self):
-        self.config_dir = ''
-        self.password_digest = ''
-        self.username = ''
+        self.config_dir = ""
+        self.password_hash = ""
+        self.username = ""
         self.using_password = False
         self.on_hassio = False
         self.cookie_secret = None
 
     def parse_args(self, args):
         self.on_hassio = args.hassio
-        password = args.password or os.getenv('PASSWORD', '')
+        password = args.password or os.getenv("PASSWORD", "")
         if not self.on_hassio:
-            self.username = args.username or os.getenv('USERNAME', '')
+            self.username = args.username or os.getenv("USERNAME", "")
             self.using_password = bool(password)
         if self.using_password:
-            self.password_digest = hmac.new(password.encode()).digest()
-        self.config_dir = args.configuration[0]
+            self.password_hash = password_hash(password)
+        self.config_dir = args.configuration
 
     @property
     def relative_url(self):
-        return os.getenv('ESPHOME_DASHBOARD_RELATIVE_URL', '/')
+        return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/")
 
     @property
     def status_use_ping(self):
-        return get_bool_env('ESPHOME_DASHBOARD_USE_PING')
+        return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
 
     @property
     def using_hassio_auth(self):
         if not self.on_hassio:
             return False
-        return not get_bool_env('DISABLE_HA_AUTHENTICATION')
+        return not get_bool_env("DISABLE_HA_AUTHENTICATION")
 
     @property
     def using_auth(self):
@@ -83,30 +92,37 @@ class DashboardSettings:
         if username != self.username:
             return False
 
-        password = hmac.new(password.encode()).digest()
-        return username == self.username and hmac.compare_digest(self.password_digest, password)
+        # Compare password in constant running time (to prevent timing attacks)
+        return hmac.compare_digest(self.password_hash, password_hash(password))
 
     def rel_path(self, *args):
         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()
 
-cookie_authenticated_yes = b'yes'
+cookie_authenticated_yes = b"yes"
 
 
 def template_args():
     version = const.__version__
+    if "b" in version:
+        docs_link = "https://beta.esphome.io/"
+    elif "dev" in version:
+        docs_link = "https://next.esphome.io/"
+    else:
+        docs_link = "https://www.esphome.io/"
+
     return {
-        'version': version,
-        'docs_link': 'https://beta.esphome.io/' if 'b' in version else 'https://esphome.io/',
-        'get_static_file_url': get_static_file_url,
-        'relative_url': settings.relative_url,
-        'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'),
-        'config_dir': settings.config_dir,
+        "version": version,
+        "docs_link": docs_link,
+        "get_static_file_url": get_static_file_url,
+        "relative_url": settings.relative_url,
+        "streamer_mode": get_bool_env("ESPHOME_STREAMER_MODE"),
+        "config_dir": settings.config_dir,
     }
 
 
@@ -114,9 +130,10 @@ def authenticated(func):
     @functools.wraps(func)
     def decorator(self, *args, **kwargs):
         if not is_authenticated(self):
-            self.redirect('./login')
+            self.redirect("./login")
             return None
         return func(self, *args, **kwargs)
+
     return decorator
 
 
@@ -124,23 +141,24 @@ def is_authenticated(request_handler):
     if settings.on_hassio:
         # Handle ingress - disable auth on ingress port
         # X-Hassio-Ingress is automatically stripped on the non-ingress server in nginx
-        header = request_handler.request.headers.get('X-Hassio-Ingress', 'NO')
-        if str(header) == 'YES':
+        header = request_handler.request.headers.get("X-Hassio-Ingress", "NO")
+        if str(header) == "YES":
             return True
     if settings.using_auth:
-        return request_handler.get_secure_cookie('authenticated') == cookie_authenticated_yes
+        return (
+            request_handler.get_secure_cookie("authenticated")
+            == cookie_authenticated_yes
+        )
     return True
 
 
 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
+        configuration = self.get_argument("configuration")
         kwargs = kwargs.copy()
-        kwargs['configuration'] = configuration
+        kwargs["configuration"] = configuration
         return func(self, *args, **kwargs)
+
     return decorator
 
 
@@ -151,7 +169,7 @@ class BaseHandler(tornado.web.RequestHandler):
 
 def websocket_class(cls):
     # pylint: disable=protected-access
-    if not hasattr(cls, '_message_handlers'):
+    if not hasattr(cls, "_message_handlers"):
         cls._message_handlers = {}
 
     for _, method in cls.__dict__.items():
@@ -166,6 +184,7 @@ def websocket_method(name):
         # pylint: disable=protected-access
         fn._message_handler = name
         return fn
+
     return wrap
 
 
@@ -181,7 +200,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
     def on_message(self, message):
         # Messages are always JSON, 500 when not
         json_message = json.loads(message)
-        type_ = json_message['type']
+        type_ = json_message["type"]
         # pylint: disable=no-member
         handlers = type(self)._message_handlers
         if type_ not in handlers:
@@ -190,17 +209,19 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
 
         handlers[type_](self, json_message)
 
-    @websocket_method('spawn')
+    @websocket_method("spawn")
     def handle_spawn(self, json_message):
         if self._proc is not None:
             # spawn can only be called once
             return
         command = self.build_command(json_message)
-        _LOGGER.info("Running command '%s'", ' '.join(shlex_quote(x) for x in command))
-        self._proc = tornado.process.Subprocess(command,
-                                                stdout=tornado.process.Subprocess.STREAM,
-                                                stderr=subprocess.STDOUT,
-                                                stdin=tornado.process.Subprocess.STREAM)
+        _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command))
+        self._proc = tornado.process.Subprocess(
+            command,
+            stdout=tornado.process.Subprocess.STREAM,
+            stderr=subprocess.STDOUT,
+            stdin=tornado.process.Subprocess.STREAM,
+        )
         self._proc.set_exit_callback(self._proc_on_exit)
         tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout)
 
@@ -208,34 +229,34 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
     def is_process_active(self):
         return self._proc is not None and self._proc.returncode is None
 
-    @websocket_method('stdin')
+    @websocket_method("stdin")
     def handle_stdin(self, json_message):
         if not self.is_process_active:
             return
-        data = json_message['data']
-        data = codecs.encode(data, 'utf8', 'replace')
+        data = json_message["data"]
+        data = codecs.encode(data, "utf8", "replace")
         _LOGGER.debug("< stdin: %s", data)
         self._proc.stdin.write(data)
 
     @tornado.gen.coroutine
     def _redirect_stdout(self):
-        reg = b'[\n\r]'
+        reg = b"[\n\r]"
 
         while True:
             try:
                 data = yield self._proc.stdout.read_until_regex(reg)
             except tornado.iostream.StreamClosedError:
                 break
-            data = codecs.decode(data, 'utf8', 'replace')
+            data = codecs.decode(data, "utf8", "replace")
 
             _LOGGER.debug("> stdout: %s", data)
-            self.write_message({'event': 'line', 'data': data})
+            self.write_message({"event": "line", "data": data})
 
     def _proc_on_exit(self, returncode):
         if not self._is_closed:
             # Check if the proc was not forcibly closed
             _LOGGER.info("Process exited with return code %s", returncode)
-            self.write_message({'event': 'exit', 'code': returncode})
+            self.write_message({"event": "exit", "code": returncode})
 
     def on_close(self):
         # Check if proc exists (if 'start' has been run)
@@ -251,55 +272,67 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
 
 class EsphomeLogsHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        config_file = settings.rel_path(json_message['configuration'])
-        return ["esphome", "--dashboard", config_file, "logs", '--serial-port',
-                json_message["port"]]
+        config_file = settings.rel_path(json_message["configuration"])
+        return [
+            "esphome",
+            "--dashboard",
+            "logs",
+            config_file,
+            "--device",
+            json_message["port"],
+        ]
 
 
 class EsphomeUploadHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        config_file = settings.rel_path(json_message['configuration'])
-        return ["esphome", "--dashboard", config_file, "run", '--upload-port',
-                json_message["port"]]
+        config_file = settings.rel_path(json_message["configuration"])
+        return [
+            "esphome",
+            "--dashboard",
+            "run",
+            config_file,
+            "--device",
+            json_message["port"],
+        ]
 
 
 class EsphomeCompileHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        config_file = settings.rel_path(json_message['configuration'])
-        return ["esphome", "--dashboard", config_file, "compile"]
+        config_file = settings.rel_path(json_message["configuration"])
+        return ["esphome", "--dashboard", "compile", config_file]
 
 
 class EsphomeValidateHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        config_file = settings.rel_path(json_message['configuration'])
-        return ["esphome", "--dashboard", config_file, "config"]
+        config_file = settings.rel_path(json_message["configuration"])
+        return ["esphome", "--dashboard", "config", config_file]
 
 
 class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        config_file = settings.rel_path(json_message['configuration'])
-        return ["esphome", "--dashboard", config_file, "clean-mqtt"]
+        config_file = settings.rel_path(json_message["configuration"])
+        return ["esphome", "--dashboard", "clean-mqtt", config_file]
 
 
 class EsphomeCleanHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        config_file = settings.rel_path(json_message['configuration'])
-        return ["esphome", "--dashboard", config_file, "clean"]
+        config_file = settings.rel_path(json_message["configuration"])
+        return ["esphome", "--dashboard", "clean", config_file]
 
 
 class EsphomeVscodeHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        return ["esphome", "--dashboard", "-q", 'dummy', "vscode"]
+        return ["esphome", "--dashboard", "-q", "vscode", "dummy"]
 
 
 class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"]
+        return ["esphome", "--dashboard", "-q", "vscode", "--ace", settings.config_dir]
 
 
 class EsphomeUpdateAllHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        return ["esphome", "--dashboard", settings.config_dir, "update-all"]
+        return ["esphome", "--dashboard", "update-all", settings.config_dir]
 
 
 class SerialPortRequestHandler(BaseHandler):
@@ -307,16 +340,18 @@ class SerialPortRequestHandler(BaseHandler):
     def get(self):
         ports = get_serial_ports()
         data = []
-        for port, desc in ports:
-            if port == '/dev/ttyAMA0':
-                desc = 'UART pins on GPIO header'
-            split_desc = desc.split(' - ')
+        for port in ports:
+            desc = port.description
+            if port.path == "/dev/ttyAMA0":
+                desc = "UART pins on GPIO header"
+            split_desc = desc.split(" - ")
             if len(split_desc) == 2 and split_desc[0] == split_desc[1]:
                 # Some serial ports repeat their values
                 desc = split_desc[0]
-            data.append({'port': port, 'desc': desc})
-        data.append({'port': 'OTA', 'desc': 'Over-The-Air'})
-        data.sort(key=lambda x: x['port'], reverse=True)
+            data.append({"port": port.path, "desc": desc})
+        data.append({"port": "OTA", "desc": "Over-The-Air"})
+        data.sort(key=lambda x: x["port"], reverse=True)
+        self.set_header("content-type", "application/json")
         self.write(json.dumps(data))
 
 
@@ -326,30 +361,84 @@ 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")
         }
-        destination = settings.rel_path(kwargs['name'] + '.yaml')
+        kwargs["ota_password"] = secrets.token_hex(16)
+        destination = settings.rel_path(f"{kwargs['name']}.yaml")
         wizard.wizard_write(path=destination, **kwargs)
-        self.redirect('./?begin=True')
+        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
     def get(self, configuration=None):
-        # pylint: disable=no-value-for-parameter
-        storage_path = ext_storage_path(settings.config_dir, configuration)
-        storage_json = StorageJSON.load(storage_path)
-        if storage_json is None:
-            self.send_error()
+        type = self.get_argument("type", "firmware.bin")
+
+        if type == "firmware.bin":
+            storage_path = ext_storage_path(settings.config_dir, configuration)
+            storage_json = StorageJSON.load(storage_path)
+            if storage_json is None:
+                self.send_error(404)
+                return
+            filename = f"{storage_json.name}.bin"
+            path = storage_json.firmware_bin_path
+
+        else:
+            args = ["esphome", "idedata", settings.rel_path(configuration)]
+            rc, stdout, _ = run_system_command(*args)
+
+            if rc != 0:
+                self.send_error(404 if rc == 2 else 500)
+                return
+
+            idedata = platformio_api.IDEData(json.loads(stdout))
+
+            found = False
+            for image in idedata.extra_flash_images:
+                if image.path.endswith(type):
+                    path = image.path
+                    filename = type
+                    found = True
+                    break
+
+            if not found:
+                self.send_error(404)
+                return
+
+        self.set_header("Content-Type", "application/octet-stream")
+        self.set_header("Content-Disposition", f'attachment; filename="{filename}"')
+        if not Path(path).is_file():
+            self.send_error(404)
             return
 
-        path = storage_json.firmware_bin_path
-        self.set_header('Content-Type', 'application/octet-stream')
-        filename = f'{storage_json.name}.bin'
-        self.set_header("Content-Disposition", f'attachment; filename="{filename}"')
-        with open(path, 'rb') as f:
+        with open(path, "rb") as f:
             while True:
                 data = f.read(16384)
                 if not data:
@@ -358,6 +447,38 @@ class DownloadBinaryRequestHandler(BaseHandler):
         self.finish()
 
 
+class ManifestRequestHandler(BaseHandler):
+    @authenticated
+    @bind_config
+    def get(self, configuration=None):
+        args = ["esphome", "idedata", settings.rel_path(configuration)]
+        rc, stdout, _ = run_system_command(*args)
+
+        if rc != 0:
+            self.send_error(404 if rc == 2 else 500)
+            return
+
+        idedata = platformio_api.IDEData(json.loads(stdout))
+
+        firmware_offset = "0x10000" if idedata.extra_flash_images else "0x0"
+        flash_images = [
+            {
+                "path": f"./download.bin?configuration={configuration}&type=firmware.bin",
+                "offset": firmware_offset,
+            }
+        ] + [
+            {
+                "path": f"./download.bin?configuration={configuration}&type={os.path.basename(image.path)}",
+                "offset": image.offset,
+            }
+            for image in idedata.extra_flash_images
+        ]
+
+        self.set_header("Content-Type", "application/json")
+        self.write(json.dumps(flash_images))
+        self.finish()
+
+
 def _list_dashboard_entries():
     files = settings.list_yaml_files()
     return [DashboardEntry(file) for file in files]
@@ -376,7 +497,9 @@ class DashboardEntry:
     @property
     def storage(self):  # type: () -> Optional[StorageJSON]
         if not self._loaded_storage:
-            self._storage = StorageJSON.load(ext_storage_path(settings.config_dir, self.filename))
+            self._storage = StorageJSON.load(
+                ext_storage_path(settings.config_dir, self.filename)
+            )
             self._loaded_storage = True
         return self._storage
 
@@ -386,10 +509,16 @@ class DashboardEntry:
             return None
         return self.storage.address
 
+    @property
+    def web_port(self):
+        if self.storage is None:
+            return None
+        return self.storage.web_port
+
     @property
     def name(self):
         if self.storage is None:
-            return self.filename[:-len('.yaml')]
+            return self.filename.replace(".yml", "").replace(".yaml", "")
         return self.storage.name
 
     @property
@@ -399,16 +528,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):
@@ -419,8 +542,8 @@ class DashboardEntry:
     @property
     def update_old(self):
         if self.storage is None:
-            return ''
-        return self.storage.esphome_version or ''
+            return ""
+        return self.storage.esphome_version or ""
 
     @property
     def update_new(self):
@@ -433,97 +556,161 @@ 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,
+                            "web_port": entry.web_port,
+                            "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()
+        begin = bool(self.get_argument("begin", False))
 
-        self.render("templates/index.html", entries=entries, begin=begin,
-                    **template_args())
+        self.render(
+            get_template_path("index"),
+            begin=begin,
+            **template_args(),
+            login_enabled=settings.using_password,
+        )
 
 
 def _ping_func(filename, address):
-    if os.name == 'nt':
-        command = ['ping', '-n', '1', address]
+    if os.name == "nt":
+        command = ["ping", "-n", "1", address]
     else:
-        command = ['ping', '-c', '1', address]
+        command = ["ping", "-c", "1", address]
     rc, _, _ = run_system_command(*command)
     return filename, rc == 0
 
 
 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})
+            stat.request_query(
+                {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):
-        pool = multiprocessing.Pool(processes=8)
-        while not STOP_EVENT.is_set():
-            # Only do pings if somebody has the dashboard open
+        with multiprocessing.Pool(processes=8) as pool:
+            while not STOP_EVENT.wait(2):
+                # Only do pings if somebody has the dashboard open
 
-            def callback(ret):
-                PING_RESULT[ret[0]] = ret[1]
+                def callback(ret):
+                    PING_RESULT[ret[0]] = ret[1]
 
-            entries = _list_dashboard_entries()
-            queue = collections.deque()
-            for entry in entries:
-                if entry.address is None:
-                    PING_RESULT[entry.filename] = None
-                    continue
+                entries = _list_dashboard_entries()
+                queue = collections.deque()
+                for entry in entries:
+                    if entry.address is None:
+                        PING_RESULT[entry.filename] = None
+                        continue
 
-                result = pool.apply_async(_ping_func, (entry.filename, entry.address),
-                                          callback=callback)
-                queue.append(result)
+                    result = pool.apply_async(
+                        _ping_func, (entry.filename, entry.address), callback=callback
+                    )
+                    queue.append(result)
 
-            while queue:
-                item = queue[0]
-                if item.ready():
-                    queue.popleft()
-                    continue
+                while queue:
+                    item = queue[0]
+                    if item.ready():
+                        queue.popleft()
+                        continue
 
-                try:
-                    item.get(0.1)
-                except OSError:
-                    # ping not installed
-                    pass
-                except multiprocessing.TimeoutError:
-                    pass
+                    try:
+                        item.get(0.1)
+                    except OSError:
+                        # ping not installed
+                        pass
+                    except multiprocessing.TimeoutError:
+                        pass
 
-                if STOP_EVENT.is_set():
-                    pool.terminate()
-                    return
+                    if STOP_EVENT.is_set():
+                        pool.terminate()
+                        return
 
-            PING_REQUEST.wait()
-            PING_REQUEST.clear()
+                PING_REQUEST.wait()
+                PING_REQUEST.clear()
 
 
 class PingRequestHandler(BaseHandler):
     @authenticated
     def get(self):
         PING_REQUEST.set()
+        self.set_header("content-type", "application/json")
         self.write(json.dumps(PING_RESULT))
 
 
-def is_allowed(configuration):
-    return os.path.sep not in configuration
+class InfoRequestHandler(BaseHandler):
+    @authenticated
+    @bind_config
+    def get(self, configuration=None):
+        yaml_path = settings.rel_path(configuration)
+        all_yaml_files = settings.list_yaml_files()
+
+        if yaml_path not in all_yaml_files:
+            self.set_status(404)
+            return
+
+        self.set_header("content-type", "application/json")
+        self.write(DashboardEntry(yaml_path).storage.to_json())
 
 
 class EditRequestHandler(BaseHandler):
@@ -531,10 +718,10 @@ class EditRequestHandler(BaseHandler):
     @bind_config
     def get(self, configuration=None):
         filename = settings.rel_path(configuration)
-        content = ''
+        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)
 
@@ -542,7 +729,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)
 
@@ -553,20 +740,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):
@@ -579,6 +764,7 @@ class UndoDeleteRequestHandler(BaseHandler):
 
 
 PING_RESULT = {}  # type: dict
+IMPORT_RESULT = {}
 STOP_EVENT = threading.Event()
 PING_REQUEST = threading.Event()
 
@@ -586,29 +772,34 @@ PING_REQUEST = threading.Event()
 class LoginHandler(BaseHandler):
     def get(self):
         if is_authenticated(self):
-            self.redirect('/')
+            self.redirect("/")
         else:
             self.render_login_page()
 
     def render_login_page(self, error=None):
-        self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth,
-                    has_username=bool(settings.username), **template_args())
+        self.render(
+            get_template_path("login"),
+            error=error,
+            hassio=settings.using_hassio_auth,
+            has_username=bool(settings.username),
+            **template_args(),
+        )
 
     def post_hassio_login(self):
         import requests
 
         headers = {
-            'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
+            "X-HASSIO-KEY": os.getenv("HASSIO_TOKEN"),
         }
         data = {
-            'username': self.get_argument('username', ''),
-            'password': self.get_argument('password', '')
+            "username": self.get_argument("username", ""),
+            "password": self.get_argument("password", ""),
         }
         try:
-            req = requests.post('http://hassio/auth', headers=headers, data=data)
+            req = requests.post("http://hassio/auth", headers=headers, data=data)
             if req.status_code == 200:
                 self.set_secure_cookie("authenticated", cookie_authenticated_yes)
-                self.redirect('/')
+                self.redirect("/")
                 return
         except Exception as err:  # pylint: disable=broad-except
             _LOGGER.warning("Error during Hass.io auth request: %s", err)
@@ -619,13 +810,15 @@ class LoginHandler(BaseHandler):
         self.render_login_page(error="Invalid username or password")
 
     def post_native_login(self):
-        username = self.get_argument("username", '')
-        password = self.get_argument("password", '')
+        username = self.get_argument("username", "")
+        password = self.get_argument("password", "")
         if settings.check_password(username, password):
             self.set_secure_cookie("authenticated", cookie_authenticated_yes)
             self.redirect("/")
             return
-        error_str = "Invalid username or password" if settings.username else "Invalid password"
+        error_str = (
+            "Invalid username or password" if settings.username else "Invalid password"
+        )
         self.set_status(401)
         self.render_login_page(error=error_str)
 
@@ -640,25 +833,73 @@ class LogoutHandler(BaseHandler):
     @authenticated
     def get(self):
         self.clear_cookie("authenticated")
-        self.redirect('./login')
+        self.redirect("./login")
 
 
-_STATIC_FILE_HASHES = {}
+class SecretKeysRequestHandler(BaseHandler):
+    @authenticated
+    def get(self):
+
+        filename = None
+
+        for secret_filename in const.SECRETS_FILES:
+            relative_filename = settings.rel_path(secret_filename)
+            if os.path.isfile(relative_filename):
+                filename = relative_filename
+                break
+
+        if filename is None:
+            self.send_error(404)
+            return
+
+        secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False))
+
+        self.set_header("content-type", "application/json")
+        self.write(json.dumps(secret_keys))
 
 
+def get_base_frontend_path():
+    if ENV_DEV not in os.environ:
+        import esphome_dashboard
+
+        return esphome_dashboard.where()
+
+    static_path = os.environ[ENV_DEV]
+    if not static_path.endswith("/"):
+        static_path += "/"
+
+    # This path can be relative, so resolve against the root or else templates don't work
+    return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard"))
+
+
+def get_template_path(template_name):
+    return os.path.join(get_base_frontend_path(), f"{template_name}.template.html")
+
+
+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):
-    static_path = os.path.join(os.path.dirname(__file__), 'static')
-    if name in _STATIC_FILE_HASHES:
-        hash_ = _STATIC_FILE_HASHES[name]
-    else:
-        path = os.path.join(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_}'
+    base = f"./static/{name}"
+
+    if ENV_DEV in os.environ:
+        return base
+
+    # Module imports can't deduplicate if stuff added to url
+    if name == "js/esphome/index.js":
+        import esphome_dashboard
+
+        return base.replace("index.js", esphome_dashboard.entrypoint())
+
+    path = get_static_path(name)
+    with open(path, "rb") as f_handle:
+        hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8]
+    return f"{base}?hash={hash_}"
 
 
-def make_app(debug=False):
+def make_app(debug=get_bool_env(ENV_DEV)):
     def log_function(handler):
         if handler.get_status() < 400:
             log_method = access_log.info
@@ -674,47 +915,56 @@ def make_app(debug=False):
 
         request_time = 1000.0 * handler.request.request_time()
         # pylint: disable=protected-access
-        log_method("%d %s %.2fms", handler.get_status(),
-                   handler._request_summary(), request_time)
+        log_method(
+            "%d %s %.2fms",
+            handler.get_status(),
+            handler._request_summary(),
+            request_time,
+        )
 
     class StaticFileHandler(tornado.web.StaticFileHandler):
         def set_extra_headers(self, path):
-            if 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"
+            )
 
-    static_path = os.path.join(os.path.dirname(__file__), 'static')
     app_settings = {
-        'debug': debug,
-        'cookie_secret': settings.cookie_secret,
-        'log_function': log_function,
-        'websocket_ping_interval': 30.0,
+        "debug": debug,
+        "cookie_secret": settings.cookie_secret,
+        "log_function": log_function,
+        "websocket_ping_interval": 30.0,
     }
     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 + "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': static_path}),
-    ], **app_settings)
-
-    if debug:
-        _STATIC_FILE_HASHES.clear()
+    app = tornado.web.Application(
+        [
+            (f"{rel}", MainRequestHandler),
+            (f"{rel}login", LoginHandler),
+            (f"{rel}logout", LogoutHandler),
+            (f"{rel}logs", EsphomeLogsHandler),
+            (f"{rel}upload", EsphomeUploadHandler),
+            (f"{rel}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}manifest.json", ManifestRequestHandler),
+            (f"{rel}serial-ports", SerialPortRequestHandler),
+            (f"{rel}ping", PingRequestHandler),
+            (f"{rel}delete", DeleteRequestHandler),
+            (f"{rel}undo-delete", UndoDeleteRequestHandler),
+            (f"{rel}wizard", WizardRequestHandler),
+            (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}),
+            (f"{rel}devices", ListDevicesHandler),
+            (f"{rel}import", ImportRequestHandler),
+            (f"{rel}secret_keys", SecretKeysRequestHandler),
+        ],
+        **app_settings,
+    )
 
     return app
 
@@ -733,20 +983,27 @@ def start_web_server(args):
 
     app = make_app(args.verbose)
     if args.socket is not None:
-        _LOGGER.info("Starting dashboard web server on unix socket %s and configuration dir %s...",
-                     args.socket, settings.config_dir)
+        _LOGGER.info(
+            "Starting dashboard web server on unix socket %s and configuration dir %s...",
+            args.socket,
+            settings.config_dir,
+        )
         server = tornado.httpserver.HTTPServer(app)
         socket = tornado.netutil.bind_unix_socket(args.socket, mode=0o666)
         server.add_socket(socket)
     else:
-        _LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...",
-                     args.port, settings.config_dir)
-        app.listen(args.port)
+        _LOGGER.info(
+            "Starting dashboard web server on http://%s:%s and configuration dir %s...",
+            args.address,
+            args.port,
+            settings.config_dir,
+        )
+        app.listen(args.port, args.address)
 
         if args.open_ui:
             import webbrowser
 
-            webbrowser.open(f'localhost:{args.port}')
+            webbrowser.open(f"http://{args.address}:{args.port}")
 
     if settings.status_use_ping:
         status_thread = PingStatusThread()
diff --git a/esphome/dashboard/static/css/esphome.css b/esphome/dashboard/static/css/esphome.css
deleted file mode 100644
index db0ac55985..0000000000
--- a/esphome/dashboard/static/css/esphome.css
+++ /dev/null
@@ -1,393 +0,0 @@
-/* Base */
-
-:root {
-  /* Colors */
-  --primary-bg-color: #fafafa;
-
-  --alert-standard-color: #666666;
-  --alert-standard-color-bg: #e6e6e6;
-  --alert-info-color: #00539f;
-  --alert-info-color-bg: #E6EEF5;
-  --alert-success-color: #4CAF50;
-  --alert-success-color-bg: #EDF7EE;
-  --alert-warning-color: #FF9800;
-  --alert-warning-color-bg: #FFF5E6;
-  --alert-error-color: #D93025;
-  --alert-error-color-bg: #FAEFEB;
-}
-
-body {
-  display: flex;
-  min-height: 100vh;
-  flex-direction: column;
-  background-color: var(--primary-bg-color);
-}
-
-/* Layout */
-.valign-wrapper {
-  position: absolute;
-  width:100vw;
-  height:100vh;
-}
-
-.valign {
-  width: 100%;
-}
-
-main {
-  flex: 1 0 auto;
-}
-
-/* Alerts & Errors */
-.alert {
-  width: 100%;
-  margin: 10px auto;
-  padding: 10px;
-  border-radius: 2px;
-  border-left-width: 4px;
-  border-left-style: solid;
-}
-
-.alert .title {
-  font-weight: bold;
-}
-
-.alert .title::after {
-  content: "\A";
-  white-space: pre;
-}
-
-.alert.alert-error {
-  color: var(--alert-error-color);
-  border-left-color: var(--alert-error-color);
-  background-color: var(--alert-error-color-bg);
-}
-
-.card.card-error, .card.status-offline {
-  border-top: 4px solid var(--alert-error-color);
-}
-
-.card.status-online {
-  border-top: 4px solid var(--alert-success-color);
-}
-
-.card.status-not-responding {
-  border-top: 4px solid var(--alert-warning-color);
-}
-
-.card.status-unknown {
-  border-top: 4px solid var(--alert-standard-color);
-}
-
-/* Login Page */
-#login-page .row.no-bottom-margin {
-  margin-bottom: 0 !important;
-}
-
-#login-page .logo {
-  display: block;
-  width: auto;
-  max-width: 300px;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-#login-page .input-field input:focus + label  {
-  color: #000;
-}
-
-#login-page .input-field input:focus {
-  border-bottom: 1px solid #000;
-  box-shadow: 0 1px 0 0 #000;
-}
-
-#login-page .input-field .prefix.active {
-  color: #000;
-}
-
-#login-page .version-number {
-  display: block;
-  text-align: center;
-  margin-bottom: 20px;;
-  color:#808080;
-  font-size: 12px;
-}
-
-#login-page footer {
-  color: #757575;
-  font-size: 12px;
-}
-
-#login-page footer a {
-  color: #424242;
-}
-
-#login-page footer p {
-  -webkit-margin-before: 0px;
-  margin-block-start: 0px;
-  -webkit-margin-after: 5px;
-  margin-block-end: 5px;
-}
-
-#login-page footer p:last-child {
-  -webkit-margin-after: 0px;
-  margin-block-end: 0px;
-}
-
-/* Dashboard */
-.logo-wrapper {
-  height: 64px;
-  height: 100%;
-  width: 0;
-  margin-left: 24px;
-}
-
-.logo {
-  width: auto;
-  height: 48px;
-  margin: 8px 0;
-}
-
-@media only screen and (max-width: 601px) {
-  .logo {
-    height: 38px;
-    margin: 9px 0;
-  }
-}
-
-.nav-icons {
-  margin-right: 24px;
-}
-
-.nav-icons i {
-  color: black;
-}
-
-.select-port-container {
-  margin-top: 8px;
-  margin-right: 10px;
-  width: 350px;
-}
-
-.serial-port-select {
-  margin-top: 8px;
-  margin-right: 10px;
-  width: 350px;
-}
-
-.serial-port-select .select-dropdown {
-  color: black;
-}
-
-.serial-port-select .select-dropdown:focus {
-  border-bottom: 1px solid #607d8b !important;
-}
-
-.serial-port-select .caret {
-  fill: black;
-}
-
-.serial-port-select .dropdown-content li>span {
-  color: black;
-}
-
-#nav-dropdown li a,  .node-dropdown li a {
-  color: black;
-}
-
-main .container {
-  margin-top: 20px;
-  margin-bottom: 20px;
-  width: 90%;
-  max-width: 1920px;
-}
-
-#nodes .card-content {
-  height: calc(100% - 47px);
-}
-
-#nodes .card-content, #nodes .card-action  {
-  padding: 12px;
-}
-
-#nodes .grid-1-col {
-  display: grid;
-  grid-template-columns: 1fr;
-}
-
-#nodes .grid-2-col {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  grid-column-gap: 1.5rem;
-}
-
-#nodes .grid-3-col {
-  display: grid;
-  grid-template-columns: 1fr 1fr 1fr;
-  grid-column-gap: 1.5rem;
-}
-
-@media only screen and (max-width: 1100px) {
-  #nodes .grid-3-col {
-    grid-template-columns: 1fr 1fr;
-    grid-column-gap: 1.5rem;
-  }
-}
-
-@media only screen and (max-width: 750px) {
-  #nodes .grid-2-col {
-    grid-template-columns: 1fr;
-    grid-column-gap: 0;
-  }
-
-  #nodes .grid-3-col {
-    grid-template-columns: 1fr;
-    grid-column-gap: 0;
-  }
-}
-
-i.node-update-avaliable {
-  color:#3f51b5;
-}
-
-i.node-webserver {
-  color:#039be5;
-}
-
-.node-config-path {
-  margin-top: -8px;
-  margin-bottom: 8px;
-  font-size: 14px;
-}
-
-.node-card-comment {
-  color: #444;
-  font-style: italic;
-}
-
-.card-action a, .card-dropdown-action a {
-  cursor: pointer;
-}
-
-.tooltipped {
-  cursor: help;
-}
-
-#js-loading-indicator {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-}
-
-.editor {
-  margin-top: 0;
-  margin-bottom: 0;
-  border-radius: 3px;
-  height: calc(100% - 56px);
-}
-
-.inlinecode {
-  box-sizing: border-box;
-  padding: 0.2em 0.4em;
-  margin: 0;
-  font-size: 85%;
-  background-color: rgba(27,31,35,0.05);
-  border-radius: 3px;
-  font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
-}
-
-.log {
-  height: 100%;
-  max-height: calc(100% - 56px);
-  background-color: #1c1c1c;
-  margin-top: 0;
-  margin-bottom: 0;
-  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
-  font-size: 12px;
-  padding: 16px;
-  overflow: auto;
-  line-height: 1.45;
-  border-radius: 3px;
-  white-space: pre-wrap;
-  overflow-wrap: break-word;
-  color: #DDD;
-}
-
-.log-bold { font-weight: bold; }
-.log-italic { font-style: italic; }
-.log-underline { text-decoration: underline; }
-.log-strikethrough { text-decoration: line-through; }
-.log-underline.log-strikethrough { text-decoration: underline line-through; }
-.log-secret {
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-}
-.log-secret-redacted {
-  opacity: 0;
-  width: 1px;
-  font-size: 1px;
-}
-.log-fg-black { color: rgb(128,128,128); }
-.log-fg-red { color: rgb(255,0,0); }
-.log-fg-green { color: rgb(0,255,0); }
-.log-fg-yellow { color: rgb(255,255,0); }
-.log-fg-blue { color: rgb(0,0,255); }
-.log-fg-magenta { color: rgb(255,0,255); }
-.log-fg-cyan { color: rgb(0,255,255); }
-.log-fg-white { color: rgb(187,187,187); }
-.log-bg-black { background-color: rgb(0,0,0); }
-.log-bg-red { background-color: rgb(255,0,0); }
-.log-bg-green { background-color: rgb(0,255,0); }
-.log-bg-yellow { background-color: rgb(255,255,0); }
-.log-bg-blue { background-color: rgb(0,0,255); }
-.log-bg-magenta { background-color: rgb(255,0,255); }
-.log-bg-cyan { background-color: rgb(0,255,255); }
-.log-bg-white { background-color: rgb(255,255,255); }
-
-ul.browser-default {
-  padding-left: 30px;
-  margin-top: 10px;
-  margin-bottom: 15px;
-}
-
-ul.browser-default li {
-  list-style-type: initial;
-}
-
-ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before {
-  background-color: #3f51b5 !important;
-}
-
-.select-action {
-  width: auto !important;
-  height: auto !important;
-  white-space: nowrap;
-}
-
-.modal {
-  width: 95%;
-  max-height: 90%;
-  height: 85% !important;
-}
-
-.page-footer {
-  display: flex;
-  align-items: center;
-  min-height: 50px;
-  padding-top: 0;
-  color: grey;
-}
-
-.page-footer a {
-  color: #afafaf;
-}
-
-@media only screen and (max-width: 992px) {
-  .page-footer .left, .page-footer .right {
-    width: 100%;
-    text-align: center;
-  }
-}
diff --git a/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css b/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css
deleted file mode 100644
index 390f16a011..0000000000
--- a/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Materialize Stepper - A little plugin that implements a stepper to Materializecss framework.
- * @version v3.1.0
- * @author Igor Marcossi (Kinark) .
- * @link https://github.com/Kinark/Materialize-stepper
- *
- * Licensed under the MIT License (https://github.com/Kinark/Materialize-stepper/blob/master/LICENSE).
- */
-
- .card-content ul.stepper{margin:1em -24px;padding:0 24px}@media only screen and (min-width:993px){.card-content ul.stepper.horizontal{margin-left:-24px;margin-right:-24px;padding-left:24px;padding-right:24px}.card-content ul.stepper.horizontal:first-child{margin-top:-24px}.card-content ul.stepper.horizontal .step.step-content{padding-left:40px;padding-right:40px}.card-content ul.stepper.horizontal .step.step-content .step-actions{padding-left:40px;padding-right:40px}}ul.stepper{counter-reset:section;overflow-y:auto;overflow-x:hidden}ul.stepper .wait-feedback{left:0;right:0;top:0;z-index:2;position:absolute;width:100%;height:100%;text-align:center;display:flex;justify-content:center;align-items:center}ul.stepper .step{position:relative;transition:height .4s cubic-bezier(.4,0,.2,1),padding-bottom .4s cubic-bezier(.4,0,.2,1)}ul.stepper .step .step-title{margin:0 -24px;cursor:pointer;padding:15.5px 44px 24px 64px;display:block}ul.stepper .step .step-title:hover{background-color:rgba(0,0,0,.06)}ul.stepper .step .step-title::after{content:attr(data-step-label);display:block;position:absolute;font-size:12.8px;font-size:.8rem;color:#424242;font-weight:400}ul.stepper .step .step-content{position:relative;display:none;height:0;transition:height .4s cubic-bezier(.4,0,.2,1);width:inherit;overflow:visible;margin-left:41px;margin-right:24px}ul.stepper .step .step-content .step-actions{padding-top:16px;padding-bottom:4px;display:flex;justify-content:flex-start}ul.stepper .step .step-content .step-actions .btn-flat:not(:last-child),ul.stepper .step .step-content .step-actions .btn-large:not(:last-child),ul.stepper .step .step-content .step-actions .btn:not(:last-child){margin-right:5px}ul.stepper .step .step-content .row{margin-bottom:7px}ul.stepper .step::before{position:absolute;counter-increment:section;content:counter(section);height:26px;width:26px;color:#fff;background-color:#b2b2b2;border-radius:50%;text-align:center;line-height:26px;font-weight:400;transition:background-color .4s cubic-bezier(.4,0,.2,1);font-size:14px;left:1px;top:13px}ul.stepper .step.active .step-title{font-weight:500}ul.stepper .step.active .step-content{height:auto;display:block}ul.stepper .step.active::before,ul.stepper .step.done::before{background-color:#2196f3}ul.stepper .step.done::before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper .step.wrong::before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red}ul.stepper .step.feedbacking .step-content>:not(.wait-feedback){opacity:.1}ul.stepper .step:not(:last-of-type)::after{content:'';position:absolute;top:52px;left:13.5px;width:1px;height:40%;height:calc(100% - 52px);background-color:rgba(0,0,0,.1);transition:height .4s cubic-bezier(.4,0,.2,1)}ul.stepper .step:not(:last-of-type).active{padding-bottom:36px}ul.stepper>li:not(:last-of-type){padding-bottom:10px}@media only screen and (min-width:993px){ul.stepper.horizontal{position:relative;display:flex;justify-content:space-between;min-height:458px;overflow:hidden}ul.stepper.horizontal::before{content:'';background-color:transparent;width:100%;min-height:84px;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);position:absolute;left:0}ul.stepper.horizontal .step{position:static;padding:0!important;width:100%;display:flex;align-items:center;height:84px}ul.stepper.horizontal .step::before{content:none}ul.stepper.horizontal .step:last-of-type{width:auto!important}ul.stepper.horizontal .step.active:not(:last-of-type)::after,ul.stepper.horizontal .step:not(:last-of-type)::after{content:'';position:static;display:inline-block;width:100%;height:1px}ul.stepper.horizontal .step .step-title{line-height:84px;height:84px;margin:0;padding:0 25px 0 65px;display:inline-block;max-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}ul.stepper.horizontal .step .step-title::before{position:absolute;counter-increment:section;content:counter(section);height:26px;width:26px;color:#fff;background-color:#b2b2b2;border-radius:50%;text-align:center;line-height:26px;font-weight:400;transition:background-color .4s cubic-bezier(.4,0,.2,1);font-size:14px;left:1px;top:28.5px;left:19px}ul.stepper.horizontal .step .step-title::after{top:15px}ul.stepper.horizontal .step.active~.step .step-content{left:100%}ul.stepper.horizontal .step.active .step-content{left:0!important}ul.stepper.horizontal .step.active .step-title::before,ul.stepper.horizontal .step.done .step-title::before{background-color:#2196f3}ul.stepper.horizontal .step.done .step-title::before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper.horizontal .step.wrong .step-title::before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red}ul.stepper.horizontal .step .step-content{position:absolute;height:calc(100% - 84px);top:84px;display:block;left:-100%;width:100%;overflow-y:auto;overflow-x:hidden;margin:0;padding:20px 20px 76px 20px;transition:left .4s cubic-bezier(.4,0,.2,1)}ul.stepper.horizontal .step .step-content .step-actions{position:absolute;bottom:0;left:0;width:100%;padding:20px;background-color:transparent;flex-direction:row-reverse}ul.stepper.horizontal .step .step-content .step-actions .btn-flat:not(:last-child),ul.stepper.horizontal .step .step-content .step-actions .btn-large:not(:last-child),ul.stepper.horizontal .step .step-content .step-actions .btn:not(:last-child){margin-left:5px;margin-right:0}}
diff --git a/esphome/dashboard/static/css/vendor/materialize/materialize.min.css b/esphome/dashboard/static/css/vendor/materialize/materialize.min.css
deleted file mode 100644
index 74b1741b62..0000000000
--- a/esphome/dashboard/static/css/vendor/materialize/materialize.min.css
+++ /dev/null
@@ -1,13 +0,0 @@
-/*!
- * Materialize v1.0.0 (http://materializecss.com)
- * Copyright 2014-2017 Materialize
- * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)
- */
-.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{-webkit-box-shadow:none !important;box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-small,.btn-floating,.dropdown-content,.collapsible,.sidenav{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-small:hover,.btn-floating:hover{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2);box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{-webkit-box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3);box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{-webkit-box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2);box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{-webkit-box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2);box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{-webkit-box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2);box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s}.hoverable:hover{-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width: 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;-webkit-transition:width .3s linear;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;-webkit-box-sizing:border-box;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:'liga';-moz-font-feature-settings:'liga';font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width: 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{-webkit-transition:background-color .3s;transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{-webkit-transition:background-color .3s;transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;-webkit-box-shadow:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);-webkit-transition:color .3s;transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{-webkit-transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .2s !important;transition:-webkit-transform .2s !important;transition:transform .2s !important;transition:transform .2s, -webkit-transform .2s !important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;-webkit-transition:color .3s ease;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease, background-color .28s ease;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width: 992px){.tabs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0%;transform-origin:50% 0%;visibility:hidden}.btn,.btn-large,.btn-small,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.disabled.btn-small,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-large:disabled,.btn-small:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-large[disabled],.btn-small[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;-webkit-box-shadow:none;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.disabled.btn-large:hover,.disabled.btn-small:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-large,.btn-small,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-large i,.btn-small i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-large:focus,.btn-small:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover,.btn-small:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;-webkit-transition:background-color .3s;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;-webkit-transition:none;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;-webkit-box-shadow:none;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;-webkit-transition:background-color .2s;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{-webkit-box-shadow:none;box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b2b2 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}.dropdown-trigger{cursor:pointer}/*!
- * Waves v0.6.0
- * http://fian.my.id/Waves
- *
- * Copyright 2014 Alfiana E. Sibuea and other contributors
- * Released under the MIT license
- * https://github.com/fians/Waves/blob/master/LICENSE
- */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{-webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;-webkit-box-shadow:none;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;-webkit-transition:all .3s;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix ~ .chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty ~ label{font-size:0.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;-webkit-transition:opacity .4s;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-transition:border .3s, -webkit-box-shadow .3s;transition:border .3s, -webkit-box-shadow .3s;transition:box-shadow .3s, border .3s;transition:box-shadow .3s, border .3s, -webkit-box-shadow .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid ~ label,input[type=text]:not(.browser-default):focus.valid ~ label,input[type=password]:not(.browser-default):focus.valid ~ label,input[type=email]:not(.browser-default):focus.valid ~ label,input[type=url]:not(.browser-default):focus.valid ~ label,input[type=time]:not(.browser-default):focus.valid ~ label,input[type=date]:not(.browser-default):focus.valid ~ label,input[type=datetime]:not(.browser-default):focus.valid ~ label,input[type=datetime-local]:not(.browser-default):focus.valid ~ label,input[type=tel]:not(.browser-default):focus.valid ~ label,input[type=number]:not(.browser-default):focus.valid ~ label,input[type=search]:not(.browser-default):focus.valid ~ label,textarea.materialize-textarea:focus.valid ~ label{color:#4CAF50}input:not([type]):focus.invalid ~ label,input[type=text]:not(.browser-default):focus.invalid ~ label,input[type=password]:not(.browser-default):focus.invalid ~ label,input[type=email]:not(.browser-default):focus.invalid ~ label,input[type=url]:not(.browser-default):focus.invalid ~ label,input[type=time]:not(.browser-default):focus.invalid ~ label,input[type=date]:not(.browser-default):focus.invalid ~ label,input[type=datetime]:not(.browser-default):focus.invalid ~ label,input[type=datetime-local]:not(.browser-default):focus.invalid ~ label,input[type=tel]:not(.browser-default):focus.invalid ~ label,input[type=number]:not(.browser-default):focus.invalid ~ label,input[type=search]:not(.browser-default):focus.invalid ~ label,textarea.materialize-textarea:focus.invalid ~ label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}input.valid:not([type]),input.valid:not([type]):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus,.select-wrapper.valid>input.select-dropdown{border-bottom:1px solid #4CAF50;-webkit-box-shadow:0 1px 0 0 #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input.invalid:not([type]),input.invalid:not([type]):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus,.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus{border-bottom:1px solid #F44336;-webkit-box-shadow:0 1px 0 0 #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).valid ~ .helper-text[data-success],input:not([type]):focus.valid ~ .helper-text[data-success],input:not([type]).invalid ~ .helper-text[data-error],input:not([type]):focus.invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default).valid ~ .helper-text[data-success],input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default).valid ~ .helper-text[data-success],input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default).valid ~ .helper-text[data-success],input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default).valid ~ .helper-text[data-success],input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default).valid ~ .helper-text[data-success],input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default).valid ~ .helper-text[data-success],input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default).valid ~ .helper-text[data-success],input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default).valid ~ .helper-text[data-success],input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error],textarea.materialize-textarea.valid ~ .helper-text[data-success],textarea.materialize-textarea:focus.valid ~ .helper-text[data-success],textarea.materialize-textarea.invalid ~ .helper-text[data-error],textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error],.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid ~ .helper-text[data-error]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}input:not([type]).valid ~ .helper-text:after,input:not([type]):focus.valid ~ .helper-text:after,input[type=text]:not(.browser-default).valid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=password]:not(.browser-default).valid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=email]:not(.browser-default).valid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=url]:not(.browser-default).valid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=time]:not(.browser-default).valid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=date]:not(.browser-default).valid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=tel]:not(.browser-default).valid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=number]:not(.browser-default).valid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=search]:not(.browser-default).valid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after,textarea.materialize-textarea.valid ~ .helper-text:after,textarea.materialize-textarea:focus.valid ~ .helper-text:after,.select-wrapper.valid ~ .helper-text:after{content:attr(data-success);color:#4CAF50}input:not([type]).invalid ~ .helper-text:after,input:not([type]):focus.invalid ~ .helper-text:after,input[type=text]:not(.browser-default).invalid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=password]:not(.browser-default).invalid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=email]:not(.browser-default).invalid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=url]:not(.browser-default).invalid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=time]:not(.browser-default).invalid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=date]:not(.browser-default).invalid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=tel]:not(.browser-default).invalid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=number]:not(.browser-default).invalid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=search]:not(.browser-default).invalid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after,textarea.materialize-textarea.invalid ~ .helper-text:after,textarea.materialize-textarea:focus.invalid ~ .helper-text:after,.select-wrapper.invalid ~ .helper-text:after{content:attr(data-error);color:#F44336}input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after,.select-wrapper+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;-webkit-transition:.2s opacity ease-out, .2s color ease-out;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix ~ label,.input-field.col .prefix ~ .validate ~ label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;-webkit-transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:transform .2s ease-out, color .2s ease-out;transition:transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out;-webkit-transform-origin:0% 100%;transform-origin:0% 100%;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;-webkit-transition:color .2s;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea,.input-field .prefix ~ label,.input-field .prefix ~ .validate ~ label,.input-field .prefix ~ .helper-text,.input-field .prefix ~ .autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;-webkit-transition:.3s background-color;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;-webkit-box-shadow:none;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;-webkit-box-shadow:none;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;-webkit-transition:.3s color;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;-webkit-box-sizing:border-box;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-transition:.28s ease;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;-webkit-transition:.28s ease;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{-webkit-transform:scale(0.5);transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;-webkit-transition:.2s;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;-webkit-transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 10px rgba(38,166,154,0.26);box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.sidenav.sidenav-fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0% 50%;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:rgba(255,255,255,0.5);-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;-webkit-transition:visibility 0s .3s;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;-webkit-transition:visibility 0s;transition:visibility 0s}.tap-target-wrapper.open .tap-target{-webkit-transform:scale(1);transform:scale(1);opacity:.95;-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;-webkit-transition:opacity .3s,
 visibility 0s 1s,
 -webkit-transform .3s;transition:opacity .3s,
 visibility 0s 1s,
 -webkit-transform .3s;transition:opacity .3s,
 transform .3s,
 visibility 0s 1s;transition:opacity .3s,
 transform .3s,
 visibility 0s 1s,
 -webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;-webkit-box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s, -webkit-transform .3s}.tap-target-wave::after{visibility:hidden;-webkit-transition:opacity .3s,
 visibility 0s,
 -webkit-transform .3s;transition:opacity .3s,
 visibility 0s,
 -webkit-transform .3s;transition:opacity .3s,
 transform .3s,
 visibility 0s;transition:opacity .3s,
 transform .3s,
 visibility 0s,
 -webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;-webkit-transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, transform .3s;transition:opacity .3s, transform .3s, -webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.datepicker-controls{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width: 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.datepicker-date-display{-webkit-box-flex:0;-webkit-flex:0 1 270px;-ms-flex:0 1 270px;flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{-webkit-transition:opacity 350ms, -webkit-transform 350ms;transition:opacity 350ms, -webkit-transform 350ms;transition:transform 350ms, opacity 350ms;transition:transform 350ms, opacity 350ms, -webkit-transform 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1, 1.1);transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(0.8, 0.8);transform:scale(0.8, 0.8)}.timepicker-canvas{-webkit-transition:opacity 175ms;transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width: 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}
diff --git a/esphome/dashboard/static/fonts/material-icons/LICENSE b/esphome/dashboard/static/fonts/material-icons/LICENSE
deleted file mode 100644
index 7a4a3ea242..0000000000
--- a/esphome/dashboard/static/fonts/material-icons/LICENSE
+++ /dev/null
@@ -1,202 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
diff --git a/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff b/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff
deleted file mode 100644
index b648a3eea2..0000000000
Binary files a/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff and /dev/null differ
diff --git a/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2 b/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2
deleted file mode 100644
index 9fa2112520..0000000000
Binary files a/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2 and /dev/null differ
diff --git a/esphome/dashboard/static/fonts/material-icons/README.md b/esphome/dashboard/static/fonts/material-icons/README.md
deleted file mode 100644
index 34d980de08..0000000000
--- a/esphome/dashboard/static/fonts/material-icons/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
-
-```html
-
-```
-
-Read more in our full usage guide:
-http://google.github.io/material-design-icons/#icon-font-for-the-web
-
-Source:
-https://github.com/google/material-design-icons
diff --git a/esphome/dashboard/static/fonts/material-icons/material-icons.css b/esphome/dashboard/static/fonts/material-icons/material-icons.css
deleted file mode 100644
index 51f2e0a0d1..0000000000
--- a/esphome/dashboard/static/fonts/material-icons/material-icons.css
+++ /dev/null
@@ -1,34 +0,0 @@
-@font-face {
-  font-family: 'Material Icons';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Material Icons'),
-       local('MaterialIcons-Regular'),
-       url(MaterialIcons-Regular.woff2) format('woff2'),
-       url(MaterialIcons-Regular.woff) format('woff');
-}
-
-.material-icons {
-  font-family: 'Material Icons';
-  font-weight: normal;
-  font-style: normal;
-  font-size: 24px;  /* Preferred icon size */
-  display: inline-block;
-  line-height: 1;
-  text-transform: none;
-  letter-spacing: normal;
-  word-wrap: normal;
-  white-space: nowrap;
-  direction: ltr;
-
-  /* Support for all WebKit browsers. */
-  -webkit-font-smoothing: antialiased;
-  /* Support for Safari and Chrome. */
-  text-rendering: optimizeLegibility;
-
-  /* Support for Firefox. */
-  -moz-osx-font-smoothing: grayscale;
-
-  /* Support for IE. */
-  font-feature-settings: 'liga';
-}
diff --git a/esphome/dashboard/static/images/favicon.ico b/esphome/dashboard/static/images/favicon.ico
deleted file mode 100644
index 88dcd7e2d1..0000000000
Binary files a/esphome/dashboard/static/images/favicon.ico and /dev/null differ
diff --git a/esphome/dashboard/static/js/esphome.js b/esphome/dashboard/static/js/esphome.js
deleted file mode 100644
index f6e9979eb2..0000000000
--- a/esphome/dashboard/static/js/esphome.js
+++ /dev/null
@@ -1,1005 +0,0 @@
-'use strict';
-
-// Document Ready
-$(document).ready(function () {
-  M.AutoInit(document.body);
-  nodeGrid();
-  startAceWebsocket();
-});
-
-// WebSocket URL Helper
-const loc = window.location;
-const wsLoc = new URL("./", `${loc.protocol}//${loc.host}${loc.pathname}`);
-wsLoc.protocol = 'ws:';
-if (loc.protocol === "https:") {
-  wsLoc.protocol = 'wss:';
-}
-const wsUrl = wsLoc.href;
-
-
-/**
- *  Dashboard Dynamic Grid
- */
-const nodeGrid = () => {
-  const nodeCount = document.querySelectorAll("#nodes .card").length;
-  const nodeGrid = document.querySelector("#nodes #grid");
-
-  if (nodeCount <= 3) {
-    nodeGrid.classList.add("grid-1-col");
-  } else if (nodeCount <= 6) {
-    nodeGrid.classList.add("grid-2-col");
-  } else {
-    nodeGrid.classList.add("grid-3-col");
-  }
-}
-
-/**
- *  Online/ Offline Status Indication
- */
-
-let isFetchingPing = false;
-
-const fetchPing = () => {
-  if (isFetchingPing) {
-    return;
-  }
-
-  isFetchingPing = true;
-
-  fetch(`./ping`, { credentials: "same-origin" }).then(res => res.json())
-    .then(response => {
-      for (let filename in response) {
-        let node = document.querySelector(`#nodes .card[data-filename="${filename}"]`);
-
-        if (node === null) {
-          continue;
-        }
-
-        let status = response[filename];
-        let className;
-
-        if (status === null) {
-          className = 'status-unknown';
-        } else if (status === true) {
-          className = 'status-online';
-          node.setAttribute('data-last-connected', Date.now().toString());
-        } else if (node.hasAttribute('data-last-connected')) {
-          const attr = parseInt(node.getAttribute('data-last-connected'));
-          if (Date.now() - attr <= 5000) {
-            className = 'status-not-responding';
-          } else {
-            className = 'status-offline';
-          }
-        } else {
-          className = 'status-offline';
-        }
-
-        if (node.classList.contains(className)) {
-          continue;
-        }
-
-        node.classList.remove('status-unknown', 'status-online', 'status-offline', 'status-not-responding');
-        node.classList.add(className);
-      }
-
-      isFetchingPing = false;
-    });
-};
-setInterval(fetchPing, 2000);
-fetchPing();
-
-/**
- *  Log Color Parsing
- */
-
-const initializeColorState = () => {
-  return {
-    bold: false,
-    italic: false,
-    underline: false,
-    strikethrough: false,
-    foregroundColor: false,
-    backgroundColor: false,
-    carriageReturn: false,
-    secret: false,
-  };
-};
-
-const colorReplace = (pre, state, text) => {
-  const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
-  let i = 0;
-
-  if (state.carriageReturn) {
-    if (text !== "\n") {
-      // don't remove if \r\n
-      pre.removeChild(pre.lastChild);
-    }
-    state.carriageReturn = false;
-  }
-
-  if (text.includes("\r")) {
-    state.carriageReturn = true;
-  }
-
-  const lineSpan = document.createElement("span");
-  lineSpan.classList.add("line");
-  pre.appendChild(lineSpan);
-
-  const addSpan = (content) => {
-    if (content === "")
-      return;
-
-    const span = document.createElement("span");
-    if (state.bold) span.classList.add("log-bold");
-    if (state.italic) span.classList.add("log-italic");
-    if (state.underline) span.classList.add("log-underline");
-    if (state.strikethrough) span.classList.add("log-strikethrough");
-    if (state.secret) span.classList.add("log-secret");
-    if (state.foregroundColor !== null) span.classList.add(`log-fg-${state.foregroundColor}`);
-    if (state.backgroundColor !== null) span.classList.add(`log-bg-${state.backgroundColor}`);
-    span.appendChild(document.createTextNode(content));
-    lineSpan.appendChild(span);
-
-    if (state.secret) {
-      const redacted = document.createElement("span");
-      redacted.classList.add("log-secret-redacted");
-      redacted.appendChild(document.createTextNode("[redacted]"));
-      lineSpan.appendChild(redacted);
-    }
-  };
-
-
-  while (true) {
-    const match = re.exec(text);
-    if (match === null)
-      break;
-
-    const j = match.index;
-    addSpan(text.substring(i, j));
-    i = j + match[0].length;
-
-    if (match[1] === undefined) continue;
-
-    for (const colorCode of match[1].split(";")) {
-      switch (parseInt(colorCode)) {
-        case 0:
-          // reset
-          state.bold = false;
-          state.italic = false;
-          state.underline = false;
-          state.strikethrough = false;
-          state.foregroundColor = null;
-          state.backgroundColor = null;
-          state.secret = false;
-          break;
-        case 1:
-          state.bold = true;
-          break;
-        case 3:
-          state.italic = true;
-          break;
-        case 4:
-          state.underline = true;
-          break;
-        case 5:
-          state.secret = true;
-          break;
-        case 6:
-          state.secret = false;
-          break;
-        case 9:
-          state.strikethrough = true;
-          break;
-        case 22:
-          state.bold = false;
-          break;
-        case 23:
-          state.italic = false;
-          break;
-        case 24:
-          state.underline = false;
-          break;
-        case 29:
-          state.strikethrough = false;
-          break;
-        case 30:
-          state.foregroundColor = "black";
-          break;
-        case 31:
-          state.foregroundColor = "red";
-          break;
-        case 32:
-          state.foregroundColor = "green";
-          break;
-        case 33:
-          state.foregroundColor = "yellow";
-          break;
-        case 34:
-          state.foregroundColor = "blue";
-          break;
-        case 35:
-          state.foregroundColor = "magenta";
-          break;
-        case 36:
-          state.foregroundColor = "cyan";
-          break;
-        case 37:
-          state.foregroundColor = "white";
-          break;
-        case 39:
-          state.foregroundColor = null;
-          break;
-        case 41:
-          state.backgroundColor = "red";
-          break;
-        case 42:
-          state.backgroundColor = "green";
-          break;
-        case 43:
-          state.backgroundColor = "yellow";
-          break;
-        case 44:
-          state.backgroundColor = "blue";
-          break;
-        case 45:
-          state.backgroundColor = "magenta";
-          break;
-        case 46:
-          state.backgroundColor = "cyan";
-          break;
-        case 47:
-          state.backgroundColor = "white";
-          break;
-        case 40:
-        case 49:
-          state.backgroundColor = null;
-          break;
-      }
-    }
-  }
-  addSpan(text.substring(i));
-  if (pre.scrollTop + 56 >= (pre.scrollHeight - pre.offsetHeight)) {
-    // at bottom
-    pre.scrollTop = pre.scrollHeight;
-  }
-};
-
-/**
- *  Serial Port Selection
- */
-
-const portSelect = document.querySelector('.nav-wrapper select');
-let ports = [];
-
-const fetchSerialPorts = (begin = false) => {
-  fetch(`./serial-ports`, { credentials: "same-origin" }).then(res => res.json())
-    .then(response => {
-      if (ports.length === response.length) {
-        let allEqual = true;
-        for (let i = 0; i < response.length; i++) {
-          if (ports[i].port !== response[i].port) {
-            allEqual = false;
-            break;
-          }
-        }
-        if (allEqual)
-          return;
-      }
-      const hasNewPort = response.length >= ports.length;
-
-      ports = response;
-
-      const inst = M.FormSelect.getInstance(portSelect);
-      if (inst !== undefined) {
-        inst.destroy();
-      }
-
-      portSelect.innerHTML = "";
-      const prevSelected = getUploadPort();
-      for (let i = 0; i < response.length; i++) {
-        const val = response[i];
-        if (val.port === prevSelected) {
-          portSelect.innerHTML += ``;
-        } else {
-          portSelect.innerHTML += ``;
-        }
-      }
-
-      M.FormSelect.init(portSelect, {});
-      if (!begin && hasNewPort)
-        M.toast({ html: "Discovered new serial port." });
-    });
-};
-
-const getUploadPort = () => {
-  const inst = M.FormSelect.getInstance(portSelect);
-  if (inst === undefined) {
-    return "OTA";
-  }
-
-  inst._setSelectedStates();
-  return inst.getSelectedValues()[0];
-};
-setInterval(fetchSerialPorts, 5000);
-fetchSerialPorts(true);
-
-/**
- * Log Elements
- */
-
-// Log Modal Class
-class LogModal {
-  constructor({
-    name,
-    onPrepare = (modalElement, config) => { },
-    onProcessExit = (modalElement, code) => { },
-    onSocketClose = (modalElement) => { },
-    dismissible = true
-  }) {
-    this.modalId = `js-${name}-modal`;
-    this.dataAction = `${name}`;
-    this.wsUrl = `${wsUrl}${name}`;
-    this.dismissible = dismissible;
-    this.activeFilename = null;
-
-    this.modalElement = document.getElementById(this.modalId);
-    this.nodeFilenameElement = document.querySelector(`#${this.modalId} #js-node-filename`);
-    this.logElement = document.querySelector(`#${this.modalId} #js-log-area`);
-    this.onPrepare = onPrepare;
-    this.onProcessExit = onProcessExit;
-    this.onSocketClose = onSocketClose;
-  }
-
-  setup() {
-    const boundOnPress = this._onPress.bind(this);
-    document.querySelectorAll(`[data-action="${this.dataAction}"]`).forEach((button) => {
-      button.addEventListener('click', boundOnPress);
-    });
-  }
-
-  _setupModalInstance() {
-    this.modalInstance = M.Modal.init(this.modalElement, {
-      onOpenStart: this._onOpenStart.bind(this),
-      onCloseStart: this._onCloseStart.bind(this),
-      dismissible: this.dismissible
-    })
-  }
-
-  _onOpenStart() {
-    document.addEventListener('keydown', this._boundKeydown);
-  }
-
-  _onCloseStart() {
-    document.removeEventListener('keydown', this._boundKeydown);
-    this.activeSocket.close();
-  }
-
-  open(event) {
-    this._onPress(event);
-  }
-
-  _onPress(event) {
-    this.activeFilename = event.target.getAttribute('data-filename');
-
-    this._setupModalInstance();
-    this.nodeFilenameElement.innerHTML = this.activeFilename;
-
-    this.logElement.innerHTML = "";
-    const colorLogState = initializeColorState();
-
-    this.onPrepare(this.modalElement, this.activeFilename);
-
-    let stopped = false;
-
-    this.modalInstance.open();
-
-    const socket = new WebSocket(this.wsUrl);
-    this.activeSocket = socket;
-    socket.addEventListener('message', (event) => {
-      const data = JSON.parse(event.data);
-      if (data.event === "line") {
-        colorReplace(this.logElement, colorLogState, data.data);
-      } else if (data.event === "exit") {
-        this.onProcessExit(this.modalElement, data.code);
-        stopped = true;
-      }
-    });
-
-    socket.addEventListener('open', () => {
-      const msg = JSON.stringify(this._encodeSpawnMessage(this.activeFilename));
-      socket.send(msg);
-    });
-
-    socket.addEventListener('close', () => {
-      if (!stopped) {
-        this.onSocketClose(this.modalElement);
-      }
-    });
-  }
-
-  _onKeyDown(event) {
-    // Close on escape key
-    if (event.keyCode === 27) {
-      this.modalInstance.close();
-    }
-  }
-
-  _encodeSpawnMessage(filename) {
-    return {
-      type: 'spawn',
-      configuration: filename,
-      port: getUploadPort(),
-    };
-  }
-}
-
-// Logs Modal
-const logsModal = new LogModal({
-  name: "logs",
-
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("[data-action='stop-logs']").innerHTML = "Stop";
-  },
-
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: "Program exited successfully",
-        displayLength: 10000
-      });
-    } else {
-      M.toast({
-        html: `Program failed with code ${code}`,
-        displayLength: 10000
-      });
-    }
-    modalElem.querySelector("data-action='stop-logs'").innerHTML = "Close";
-  },
-
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000
-    });
-  }
-})
-
-logsModal.setup();
-
-// Upload Modal
-const uploadModal = new LogModal({
-  name: "upload",
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("#js-upload-modal [data-action='download-binary']").classList.add('disabled');
-    modalElement.querySelector("#js-upload-modal [data-action='upload']").setAttribute('data-filename', activeFilename);
-    modalElement.querySelector("#js-upload-modal [data-action='upload']").classList.add('disabled');
-    modalElement.querySelector("#js-upload-modal [data-action='edit']").setAttribute('data-filename', activeFilename);
-    modalElement.querySelector("#js-upload-modal [data-action='stop-logs']").innerHTML = "Stop";
-  },
-
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: "Program exited successfully",
-        displayLength: 10000
-      });
-
-      modalElement.querySelector("#js-upload-modal [data-action='download-binary']").classList.remove('disabled');
-    } else {
-      M.toast({
-        html: `Program failed with code ${code}`,
-        displayLength: 10000
-      });
-
-      modalElement.querySelector("#js-upload-modal [data-action='upload']").classList.add('disabled');
-      modalElement.querySelector("#js-upload-modal [data-action='upload']").classList.remove('disabled');
-    }
-    modalElement.querySelector("#js-upload-modal [data-action='stop-logs']").innerHTML = "Close";
-  },
-
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000
-    });
-  },
-
-  dismissible: false
-})
-
-uploadModal.setup();
-
-const downloadAfterUploadButton = document.querySelector("#js-upload-modal [data-action='download-binary']");
-downloadAfterUploadButton.addEventListener('click', () => {
-  const link = document.createElement("a");
-  link.download = name;
-  link.href = `./download.bin?configuration=${encodeURIComponent(uploadModal.activeFilename)}`;
-  document.body.appendChild(link);
-  link.click();
-  link.remove();
-});
-
-// Validate Modal
-const validateModal = new LogModal({
-  name: 'validate',
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("#js-validate-modal [data-action='stop-logs']").innerHTML = "Stop";
-    modalElement.querySelector("#js-validate-modal [data-action='edit']").setAttribute('data-filename', activeFilename);
-    modalElement.querySelector("#js-validate-modal [data-action='upload']").setAttribute('data-filename', activeFilename);
-    modalElement.querySelector("#js-validate-modal [data-action='upload']").classList.add('disabled');
-  },
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: `${validateModal.activeFilename} is valid 👍`,
-        displayLength: 10000,
-      });
-      modalElement.querySelector("#js-validate-modal [data-action='upload']").classList.remove('disabled');
-    } else {
-      M.toast({
-        html: `${validateModal.activeFilename} is invalid 😕`,
-        displayLength: 10000,
-      });
-    }
-    modalElement.querySelector("#js-validate-modal [data-action='stop-logs']").innerHTML = "Close";
-  },
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000,
-    });
-  },
-});
-
-validateModal.setup();
-
-// Compile Modal
-const compileModal = new LogModal({
-  name: 'compile',
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("#js-compile-modal [data-action='stop-logs']").innerHTML = "Stop";
-    modalElement.querySelector("#js-compile-modal [data-action='download-binary']").classList.add('disabled');
-  },
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: "Program exited successfully",
-        displayLength: 10000,
-      });
-      modalElement.querySelector("#js-compile-modal [data-action='download-binary']").classList.remove('disabled');
-    } else {
-      M.toast({
-        html: `Program failed with code ${data.code}`,
-        displayLength: 10000,
-      });
-    }
-    modalElement.querySelector("#js-compile-modal [data-action='stop-logs']").innerHTML = "Close";
-  },
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000,
-    });
-  },
-  dismissible: false,
-});
-
-compileModal.setup();
-
-const downloadAfterCompileButton = document.querySelector("#js-compile-modal [data-action='download-binary']");
-downloadAfterCompileButton.addEventListener('click', () => {
-  const link = document.createElement("a");
-  link.download = name;
-  link.href = `./download.bin?configuration=${encodeURIComponent(compileModal.activeFilename)}`;
-  document.body.appendChild(link);
-  link.click();
-  link.remove();
-});
-
-// Clean MQTT Modal
-const cleanMqttModal = new LogModal({
-  name: 'clean-mqtt',
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("#js-clean-mqtt-modal [data-action='stop-logs']").innerHTML = "Stop";
-  },
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: "Program exited successfully",
-        displayLength: 10000,
-      });
-    } else {
-      M.toast({
-        html: `Program failed with code ${code}`,
-        displayLength: 10000,
-      });
-    }
-    modalElement.querySelector("#js-clean-mqtt-modal [data-action='stop-logs']").innerHTML = "Close";
-  },
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000,
-    });
-  },
-});
-
-cleanMqttModal.setup();
-
-// Clean Build Files Modal
-const cleanModal = new LogModal({
-  name: 'clean',
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("#js-clean-modal [data-action='stop-logs']").innerHTML = "Stop";
-  },
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: "Program exited successfully",
-        displayLength: 10000,
-      });
-    } else {
-      M.toast({
-        html: `Program failed with code ${code}`,
-        displayLength: 10000,
-      });
-    }
-    modalElement.querySelector("#js-clean-modal [data-action='stop-logs']").innerHTML = "Close";
-  },
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000,
-    });
-  },
-});
-
-cleanModal.setup();
-
-// Update All Modal
-const updateAllModal = new LogModal({
-  name: 'update-all',
-  onPrepare: (modalElement, activeFilename) => {
-    modalElement.querySelector("#js-update-all-modal [data-action='stop-logs']").innerHTML = "Stop";
-    modalElement.querySelector("#js-update-all-modal #js-node-filename").style.visibility = "hidden";
-  },
-  onProcessExit: (modalElement, code) => {
-    if (code === 0) {
-      M.toast({
-        html: "Program exited successfully",
-        displayLength: 10000,
-      });
-      downloadButton.classList.remove('disabled');
-    } else {
-      M.toast({
-        html: `Program failed with code ${data.code}`,
-        displayLength: 10000,
-      });
-    }
-    modalElement.querySelector("#js-update-all-modal [data-action='stop-logs']").innerHTML = "Close";
-  },
-  onSocketClose: (modalElement) => {
-    M.toast({
-      html: 'Terminated process',
-      displayLength: 10000,
-    });
-  },
-  dismissible: false,
-});
-
-updateAllModal.setup();
-
-/**
- *  Node Editing
- */
-
-let editorActiveFilename = null;
-let editorActiveSecrets = false;
-let editorActiveWebSocket = null;
-let editorValidationScheduled = false;
-let editorValidationRunning = false;
-
-// Setup Editor
-const editorElement = document.querySelector("#js-editor-modal #js-editor-area");
-const editor = ace.edit(editorElement);
-
-editor.setOptions({
-  highlightActiveLine: true,
-  showPrintMargin: true,
-  useSoftTabs: true,
-  tabSize: 2,
-  useWorker: false,
-  theme: 'ace/theme/dreamweaver',
-  mode: 'ace/mode/yaml'
-});
-
-editor.commands.addCommand({
-  name: 'saveCommand',
-  bindKey: { win: 'Ctrl-S', mac: 'Command-S' },
-  exec: function () {
-    saveFile(editorActiveFilename);
-  },
-  readOnly: false
-});
-
-// Edit Button Listener
-document.querySelectorAll("[data-action='edit']").forEach((button) => {
-  button.addEventListener('click', (event) => {
-
-    editorActiveFilename = event.target.getAttribute("data-filename");
-    const filenameField = document.querySelector("#js-editor-modal #js-node-filename");
-    filenameField.innerHTML = editorActiveFilename;
-
-    const saveButton = document.querySelector("#js-editor-modal [data-action='save']");
-    const uploadButton = document.querySelector("#js-editor-modal [data-action='upload']");
-    const closeButton = document.querySelector("#js-editor-modal [data-action='close']");
-    saveButton.setAttribute('data-filename', editorActiveFilename);
-    uploadButton.setAttribute('data-filename', editorActiveFilename);
-    if (editorActiveFilename === "secrets.yaml") {
-      uploadButton.classList.add("disabled");
-      editorActiveSecrets = true;
-    } else {
-      uploadButton.classList.remove("disabled");
-      editorActiveSecrets = false;
-    }
-    closeButton.setAttribute('data-filename', editorActiveFilename);
-
-    const loadingIndicator = document.querySelector("#js-editor-modal #js-loading-indicator");
-    const editorArea = document.querySelector("#js-editor-modal #js-editor-area");
-
-    loadingIndicator.style.display = "block";
-    editorArea.style.display = "none";
-
-    editor.setOption('readOnly', true);
-    fetch(`./edit?configuration=${editorActiveFilename}`, { credentials: "same-origin" })
-      .then(res => res.text()).then(response => {
-        editor.setValue(response, -1);
-        editor.setOption('readOnly', false);
-        loadingIndicator.style.display = "none";
-        editorArea.style.display = "block";
-      });
-    editor.focus();
-
-    const editModalElement = document.getElementById("js-editor-modal");
-    const editorModal = M.Modal.init(editModalElement, {
-      onOpenStart: function () {
-        editorModalOnOpen()
-      },
-      onCloseStart: function () {
-        editorModalOnClose()
-      },
-      dismissible: false
-    })
-
-    editorModal.open();
-
-  });
-});
-
-// Editor On Open
-const editorModalOnOpen = () => {
-  return
-}
-
-// Editor On Close
-const editorModalOnClose = () => {
-  editorActiveFilename = null;
-}
-
-// Editor WebSocket Validation
-const startAceWebsocket = () => {
-  editorActiveWebSocket = new WebSocket(`${wsUrl}ace`);
-
-  editorActiveWebSocket.addEventListener('message', (event) => {
-    const raw = JSON.parse(event.data);
-    if (raw.event === "line") {
-      const msg = JSON.parse(raw.data);
-      if (msg.type === "result") {
-        const arr = [];
-
-        for (const v of msg.validation_errors) {
-          let o = {
-            text: v.message,
-            type: 'error',
-            row: 0,
-            column: 0
-          };
-          if (v.range != null) {
-            o.row = v.range.start_line;
-            o.column = v.range.start_col;
-          }
-          arr.push(o);
-        }
-        for (const v of msg.yaml_errors) {
-          arr.push({
-            text: v.message,
-            type: 'error',
-            row: 0,
-            column: 0
-          });
-        }
-
-        editor.session.setAnnotations(arr);
-
-        if (arr.length) {
-          document.querySelector("#js-editor-modal [data-action='upload']").classList.add('disabled');
-        } else {
-          document.querySelector("#js-editor-modal [data-action='upload']").classList.remove('disabled');
-        }
-
-        editorValidationRunning = false;
-      } else if (msg.type === "read_file") {
-        sendAceStdin({
-          type: 'file_response',
-          content: editor.getValue()
-        });
-      }
-    }
-  })
-
-  editorActiveWebSocket.addEventListener('open', () => {
-    const msg = JSON.stringify({ type: 'spawn' });
-    editorActiveWebSocket.send(msg);
-  });
-
-  editorActiveWebSocket.addEventListener('close', () => {
-    editorActiveWebSocket = null;
-    setTimeout(startAceWebsocket, 5000);
-  });
-};
-
-const sendAceStdin = (data) => {
-  let send = JSON.stringify({
-    type: 'stdin',
-    data: JSON.stringify(data) + '\n',
-  });
-  editorActiveWebSocket.send(send);
-};
-
-const debounce = (func, wait) => {
-  let timeout;
-  return function () {
-    let context = this, args = arguments;
-    let later = function () {
-      timeout = null;
-      func.apply(context, args);
-    };
-    clearTimeout(timeout);
-    timeout = setTimeout(later, wait);
-  };
-};
-
-editor.session.on('change', debounce(() => {
-  editorValidationScheduled = !editorActiveSecrets;
-}, 250));
-
-setInterval(() => {
-  if (!editorValidationScheduled || editorValidationRunning)
-    return;
-  if (editorActiveWebSocket == null)
-    return;
-
-  sendAceStdin({
-    type: 'validate',
-    file: editorActiveFilename
-  });
-  editorValidationRunning = true;
-  editorValidationScheduled = false;
-}, 100);
-
-// Save File
-const saveFile = (filename) => {
-  const extensionRegex = new RegExp("(?:\.([^.]+))?$");
-
-  if (filename.match(extensionRegex)[0] !== ".yaml") {
-    M.toast({
-      html: `❌ File ${filename} cannot be saved as it is not a YAML file!`,
-      displayLength: 10000
-    });
-    return;
-  }
-
-  fetch(`./edit?configuration=${filename}`, {
-    credentials: "same-origin",
-    method: "POST",
-    body: editor.getValue()
-  })
-    .then((response) => {
-      response.text();
-    })
-    .then(() => {
-      M.toast({
-        html: `✅ Saved ${filename}`,
-        displayLength: 10000
-      });
-    })
-    .catch((error) => {
-      M.toast({
-        html: `❌ An error occured saving ${filename}`,
-        displayLength: 10000
-      });
-    })
-}
-
-document.querySelectorAll("[data-action='save']").forEach((btn) => {
-  btn.addEventListener("click", (e) => {
-    saveFile(editorActiveFilename);
-  });
-});
-
-// Delete Node
-document.querySelectorAll("[data-action='delete']").forEach((btn) => {
-  btn.addEventListener("click", (e) => {
-    const filename = e.target.getAttribute("data-filename");
-
-    fetch(`./delete?configuration=${filename}`, {
-      credentials: "same-origin",
-      method: "POST",
-    })
-      .then((res) => {
-        res.text()
-      })
-      .then(() => {
-        const toastHtml = `🗑️ Deleted ${filename}
-                           `;
-        const toast = M.toast({
-          html: toastHtml,
-          displayLength: 10000
-        });
-        const undoButton = toast.el.querySelector('.toast-action');
-
-        document.querySelector(`.card[data-filename="${filename}"]`).remove();
-
-        undoButton.addEventListener('click', () => {
-          fetch(`./undo-delete?configuration=${filename}`, {
-            credentials: "same-origin",
-            method: "POST",
-          })
-            .then((res) => {
-              res.text()
-            })
-            .then(() => {
-              window.location.reload(false);
-            });
-        });
-      });
-
-  });
-});
-
-/**
- *  Wizard
- */
-
-const wizardTriggerElement = document.querySelector("[data-action='wizard']");
-const wizardModal = document.getElementById("js-wizard-modal");
-const wizardCloseButton = document.querySelector("[data-action='wizard-close']");
-
-const wizardStepper = document.querySelector('#js-wizard-modal .stepper');
-const wizardStepperInstace = new MStepper(wizardStepper, {
-  firstActive: 0,
-  stepTitleNavigation: false,
-  autoFocusInput: true,
-  showFeedbackLoader: true,
-})
-
-const startWizard = () => {
-  M.Modal.init(wizardModal, {
-    dismissible: false
-  }).open();
-}
-
-document.querySelectorAll("[data-action='wizard']").forEach((btn) => {
-  btn.addEventListener("click", (event) => {
-    startWizard();
-  })
-});
-
-jQuery.validator.addMethod("nospaces", (value, element) => {
-  return value.indexOf(' ') < 0;
-}, "Name cannot contain any spaces!");
-
-jQuery.validator.addMethod("lowercase", (value, element) => {
-  return value === value.toLowerCase();
-}, "Name must be all lower case!");
-
diff --git a/esphome/dashboard/static/js/vendor/ace/ace.js b/esphome/dashboard/static/js/vendor/ace/ace.js
deleted file mode 100644
index 0a66043d67..0000000000
--- a/esphome/dashboard/static/js/vendor/ace/ace.js
+++ /dev/null
@@ -1,16 +0,0 @@
-(function () { function o(n) { var i = e; n && (e[n] || (e[n] = {}), i = e[n]); if (!i.define || !i.define.packaged) t.original = i.define, i.define = t, i.define.packaged = !0; if (!i.require || !i.require.packaged) r.original = i.require, i.require = r, i.require.packaged = !0 } var ACE_NAMESPACE = "", e = function () { return this }(); !e && typeof window != "undefined" && (e = window); if (!ACE_NAMESPACE && typeof requirejs != "undefined") return; var t = function (e, n, r) { if (typeof e != "string") { t.original ? t.original.apply(this, arguments) : (console.error("dropping module because define wasn't a string."), console.trace()); return } arguments.length == 2 && (r = n), t.modules[e] || (t.payloads[e] = r, t.modules[e] = null) }; t.modules = {}, t.payloads = {}; var n = function (e, t, n) { if (typeof t == "string") { var i = s(e, t); if (i != undefined) return n && n(), i } else if (Object.prototype.toString.call(t) === "[object Array]") { var o = []; for (var u = 0, a = t.length; u < a; ++u) { var f = s(e, t[u]); if (f == undefined && r.original) return; o.push(f) } return n && n.apply(null, o) || !0 } }, r = function (e, t) { var i = n("", e, t); return i == undefined && r.original ? r.original.apply(this, arguments) : i }, i = function (e, t) { if (t.indexOf("!") !== -1) { var n = t.split("!"); return i(e, n[0]) + "!" + i(e, n[1]) } if (t.charAt(0) == ".") { var r = e.split("/").slice(0, -1).join("/"); t = r + "/" + t; while (t.indexOf(".") !== -1 && s != t) { var s = t; t = t.replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "") } } return t }, s = function (e, r) { r = i(e, r); var s = t.modules[r]; if (!s) { s = t.payloads[r]; if (typeof s == "function") { var o = {}, u = { id: r, uri: "", exports: o, packaged: !0 }, a = function (e, t) { return n(r, e, t) }, f = s(a, o, u); o = f || u.exports, t.modules[r] = o, delete t.payloads[r] } s = t.modules[r] = o || s } return s }; o(ACE_NAMESPACE) })(), define("ace/lib/regexp", ["require", "exports", "module"], function (e, t, n) { "use strict"; function o(e) { return (e.global ? "g" : "") + (e.ignoreCase ? "i" : "") + (e.multiline ? "m" : "") + (e.extended ? "x" : "") + (e.sticky ? "y" : "") } function u(e, t, n) { if (Array.prototype.indexOf) return e.indexOf(t, n); for (var r = n || 0; r < e.length; r++)if (e[r] === t) return r; return -1 } var r = { exec: RegExp.prototype.exec, test: RegExp.prototype.test, match: String.prototype.match, replace: String.prototype.replace, split: String.prototype.split }, i = r.exec.call(/()??/, "")[1] === undefined, s = function () { var e = /^/g; return r.test.call(e, ""), !e.lastIndex }(); if (s && i) return; RegExp.prototype.exec = function (e) { var t = r.exec.apply(this, arguments), n, a; if (typeof e == "string" && t) { !i && t.length > 1 && u(t, "") > -1 && (a = RegExp(this.source, r.replace.call(o(this), "g", "")), r.replace.call(e.slice(t.index), a, function () { for (var e = 1; e < arguments.length - 2; e++)arguments[e] === undefined && (t[e] = undefined) })); if (this._xregexp && this._xregexp.captureNames) for (var f = 1; f < t.length; f++)n = this._xregexp.captureNames[f - 1], n && (t[n] = t[f]); !s && this.global && !t[0].length && this.lastIndex > t.index && this.lastIndex-- } return t }, s || (RegExp.prototype.test = function (e) { var t = r.exec.call(this, e); return t && this.global && !t[0].length && this.lastIndex > t.index && this.lastIndex--, !!t }) }), define("ace/lib/es5-shim", ["require", "exports", "module"], function (e, t, n) { function r() { } function w(e) { try { return Object.defineProperty(e, "sentinel", {}), "sentinel" in e } catch (t) { } } function H(e) { return e = +e, e !== e ? e = 0 : e !== 0 && e !== 1 / 0 && e !== -1 / 0 && (e = (e > 0 || -1) * Math.floor(Math.abs(e))), e } function B(e) { var t = typeof e; return e === null || t === "undefined" || t === "boolean" || t === "number" || t === "string" } function j(e) { var t, n, r; if (B(e)) return e; n = e.valueOf; if (typeof n == "function") { t = n.call(e); if (B(t)) return t } r = e.toString; if (typeof r == "function") { t = r.call(e); if (B(t)) return t } throw new TypeError } Function.prototype.bind || (Function.prototype.bind = function (t) { var n = this; if (typeof n != "function") throw new TypeError("Function.prototype.bind called on incompatible " + n); var i = u.call(arguments, 1), s = function () { if (this instanceof s) { var e = n.apply(this, i.concat(u.call(arguments))); return Object(e) === e ? e : this } return n.apply(t, i.concat(u.call(arguments))) }; return n.prototype && (r.prototype = n.prototype, s.prototype = new r, r.prototype = null), s }); var i = Function.prototype.call, s = Array.prototype, o = Object.prototype, u = s.slice, a = i.bind(o.toString), f = i.bind(o.hasOwnProperty), l, c, h, p, d; if (d = f(o, "__defineGetter__")) l = i.bind(o.__defineGetter__), c = i.bind(o.__defineSetter__), h = i.bind(o.__lookupGetter__), p = i.bind(o.__lookupSetter__); if ([1, 2].splice(0).length != 2) if (!function () { function e(e) { var t = new Array(e + 2); return t[0] = t[1] = 0, t } var t = [], n; t.splice.apply(t, e(20)), t.splice.apply(t, e(26)), n = t.length, t.splice(5, 0, "XXX"), n + 1 == t.length; if (n + 1 == t.length) return !0 }()) Array.prototype.splice = function (e, t) { var n = this.length; e > 0 ? e > n && (e = n) : e == void 0 ? e = 0 : e < 0 && (e = Math.max(n + e, 0)), e + t < n || (t = n - e); var r = this.slice(e, e + t), i = u.call(arguments, 2), s = i.length; if (e === n) s && this.push.apply(this, i); else { var o = Math.min(t, n - e), a = e + o, f = a + s - o, l = n - a, c = n - o; if (f < a) for (var h = 0; h < l; ++h)this[f + h] = this[a + h]; else if (f > a) for (h = l; h--;)this[f + h] = this[a + h]; if (s && e === c) this.length = c, this.push.apply(this, i); else { this.length = c + s; for (h = 0; h < s; ++h)this[e + h] = i[h] } } return r }; else { var v = Array.prototype.splice; Array.prototype.splice = function (e, t) { return arguments.length ? v.apply(this, [e === void 0 ? 0 : e, t === void 0 ? this.length - e : t].concat(u.call(arguments, 2))) : [] } } Array.isArray || (Array.isArray = function (t) { return a(t) == "[object Array]" }); var m = Object("a"), g = m[0] != "a" || !(0 in m); Array.prototype.forEach || (Array.prototype.forEach = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = arguments[1], s = -1, o = r.length >>> 0; if (a(t) != "[object Function]") throw new TypeError; while (++s < o) s in r && t.call(i, r[s], s, n) }), Array.prototype.map || (Array.prototype.map = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = r.length >>> 0, s = Array(i), o = arguments[1]; if (a(t) != "[object Function]") throw new TypeError(t + " is not a function"); for (var u = 0; u < i; u++)u in r && (s[u] = t.call(o, r[u], u, n)); return s }), Array.prototype.filter || (Array.prototype.filter = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = r.length >>> 0, s = [], o, u = arguments[1]; if (a(t) != "[object Function]") throw new TypeError(t + " is not a function"); for (var f = 0; f < i; f++)f in r && (o = r[f], t.call(u, o, f, n) && s.push(o)); return s }), Array.prototype.every || (Array.prototype.every = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = r.length >>> 0, s = arguments[1]; if (a(t) != "[object Function]") throw new TypeError(t + " is not a function"); for (var o = 0; o < i; o++)if (o in r && !t.call(s, r[o], o, n)) return !1; return !0 }), Array.prototype.some || (Array.prototype.some = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = r.length >>> 0, s = arguments[1]; if (a(t) != "[object Function]") throw new TypeError(t + " is not a function"); for (var o = 0; o < i; o++)if (o in r && t.call(s, r[o], o, n)) return !0; return !1 }), Array.prototype.reduce || (Array.prototype.reduce = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = r.length >>> 0; if (a(t) != "[object Function]") throw new TypeError(t + " is not a function"); if (!i && arguments.length == 1) throw new TypeError("reduce of empty array with no initial value"); var s = 0, o; if (arguments.length >= 2) o = arguments[1]; else do { if (s in r) { o = r[s++]; break } if (++s >= i) throw new TypeError("reduce of empty array with no initial value") } while (!0); for (; s < i; s++)s in r && (o = t.call(void 0, o, r[s], s, n)); return o }), Array.prototype.reduceRight || (Array.prototype.reduceRight = function (t) { var n = F(this), r = g && a(this) == "[object String]" ? this.split("") : n, i = r.length >>> 0; if (a(t) != "[object Function]") throw new TypeError(t + " is not a function"); if (!i && arguments.length == 1) throw new TypeError("reduceRight of empty array with no initial value"); var s, o = i - 1; if (arguments.length >= 2) s = arguments[1]; else do { if (o in r) { s = r[o--]; break } if (--o < 0) throw new TypeError("reduceRight of empty array with no initial value") } while (!0); do o in this && (s = t.call(void 0, s, r[o], o, n)); while (o--); return s }); if (!Array.prototype.indexOf || [0, 1].indexOf(1, 2) != -1) Array.prototype.indexOf = function (t) { var n = g && a(this) == "[object String]" ? this.split("") : F(this), r = n.length >>> 0; if (!r) return -1; var i = 0; arguments.length > 1 && (i = H(arguments[1])), i = i >= 0 ? i : Math.max(0, r + i); for (; i < r; i++)if (i in n && n[i] === t) return i; return -1 }; if (!Array.prototype.lastIndexOf || [0, 1].lastIndexOf(0, -3) != -1) Array.prototype.lastIndexOf = function (t) { var n = g && a(this) == "[object String]" ? this.split("") : F(this), r = n.length >>> 0; if (!r) return -1; var i = r - 1; arguments.length > 1 && (i = Math.min(i, H(arguments[1]))), i = i >= 0 ? i : r - Math.abs(i); for (; i >= 0; i--)if (i in n && t === n[i]) return i; return -1 }; Object.getPrototypeOf || (Object.getPrototypeOf = function (t) { return t.__proto__ || (t.constructor ? t.constructor.prototype : o) }); if (!Object.getOwnPropertyDescriptor) { var y = "Object.getOwnPropertyDescriptor called on a non-object: "; Object.getOwnPropertyDescriptor = function (t, n) { if (typeof t != "object" && typeof t != "function" || t === null) throw new TypeError(y + t); if (!f(t, n)) return; var r, i, s; r = { enumerable: !0, configurable: !0 }; if (d) { var u = t.__proto__; t.__proto__ = o; var i = h(t, n), s = p(t, n); t.__proto__ = u; if (i || s) return i && (r.get = i), s && (r.set = s), r } return r.value = t[n], r } } Object.getOwnPropertyNames || (Object.getOwnPropertyNames = function (t) { return Object.keys(t) }); if (!Object.create) { var b; Object.prototype.__proto__ === null ? b = function () { return { __proto__: null } } : b = function () { var e = {}; for (var t in e) e[t] = null; return e.constructor = e.hasOwnProperty = e.propertyIsEnumerable = e.isPrototypeOf = e.toLocaleString = e.toString = e.valueOf = e.__proto__ = null, e }, Object.create = function (t, n) { var r; if (t === null) r = b(); else { if (typeof t != "object") throw new TypeError("typeof prototype[" + typeof t + "] != 'object'"); var i = function () { }; i.prototype = t, r = new i, r.__proto__ = t } return n !== void 0 && Object.defineProperties(r, n), r } } if (Object.defineProperty) { var E = w({}), S = typeof document == "undefined" || w(document.createElement("div")); if (!E || !S) var x = Object.defineProperty } if (!Object.defineProperty || x) { var T = "Property description must be an object: ", N = "Object.defineProperty called on non-object: ", C = "getters & setters can not be defined on this javascript engine"; Object.defineProperty = function (t, n, r) { if (typeof t != "object" && typeof t != "function" || t === null) throw new TypeError(N + t); if (typeof r != "object" && typeof r != "function" || r === null) throw new TypeError(T + r); if (x) try { return x.call(Object, t, n, r) } catch (i) { } if (f(r, "value")) if (d && (h(t, n) || p(t, n))) { var s = t.__proto__; t.__proto__ = o, delete t[n], t[n] = r.value, t.__proto__ = s } else t[n] = r.value; else { if (!d) throw new TypeError(C); f(r, "get") && l(t, n, r.get), f(r, "set") && c(t, n, r.set) } return t } } Object.defineProperties || (Object.defineProperties = function (t, n) { for (var r in n) f(n, r) && Object.defineProperty(t, r, n[r]); return t }), Object.seal || (Object.seal = function (t) { return t }), Object.freeze || (Object.freeze = function (t) { return t }); try { Object.freeze(function () { }) } catch (k) { Object.freeze = function (t) { return function (n) { return typeof n == "function" ? n : t(n) } }(Object.freeze) } Object.preventExtensions || (Object.preventExtensions = function (t) { return t }), Object.isSealed || (Object.isSealed = function (t) { return !1 }), Object.isFrozen || (Object.isFrozen = function (t) { return !1 }), Object.isExtensible || (Object.isExtensible = function (t) { if (Object(t) === t) throw new TypeError; var n = ""; while (f(t, n)) n += "?"; t[n] = !0; var r = f(t, n); return delete t[n], r }); if (!Object.keys) { var L = !0, A = ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "constructor"], O = A.length; for (var M in { toString: null }) L = !1; Object.keys = function I(e) { if (typeof e != "object" && typeof e != "function" || e === null) throw new TypeError("Object.keys called on a non-object"); var I = []; for (var t in e) f(e, t) && I.push(t); if (L) for (var n = 0, r = O; n < r; n++) { var i = A[n]; f(e, i) && I.push(i) } return I } } Date.now || (Date.now = function () { return (new Date).getTime() }); var _ = "  \n\x0b\f\r \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff"; if (!String.prototype.trim) { _ = "[" + _ + "]"; var D = new RegExp("^" + _ + _ + "*"), P = new RegExp(_ + _ + "*$"); String.prototype.trim = function () { return String(this).replace(D, "").replace(P, "") } } var F = function (e) { if (e == null) throw new TypeError("can't convert " + e + " to object"); return Object(e) } }), define("ace/lib/fixoldbrowsers", ["require", "exports", "module", "ace/lib/regexp", "ace/lib/es5-shim"], function (e, t, n) { "use strict"; e("./regexp"), e("./es5-shim"), typeof Element != "undefined" && !Element.prototype.remove && Object.defineProperty(Element.prototype, "remove", { enumerable: !1, writable: !0, configurable: !0, value: function () { this.parentNode && this.parentNode.removeChild(this) } }) }), define("ace/lib/useragent", ["require", "exports", "module"], function (e, t, n) { "use strict"; t.OS = { LINUX: "LINUX", MAC: "MAC", WINDOWS: "WINDOWS" }, t.getOS = function () { return t.isMac ? t.OS.MAC : t.isLinux ? t.OS.LINUX : t.OS.WINDOWS }; var r = typeof navigator == "object" ? navigator : {}, i = (/mac|win|linux/i.exec(r.platform) || ["other"])[0].toLowerCase(), s = r.userAgent || "", o = r.appName || ""; t.isWin = i == "win", t.isMac = i == "mac", t.isLinux = i == "linux", t.isIE = o == "Microsoft Internet Explorer" || o.indexOf("MSAppHost") >= 0 ? parseFloat((s.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/) || [])[1]) : parseFloat((s.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/) || [])[1]), t.isOldIE = t.isIE && t.isIE < 9, t.isGecko = t.isMozilla = s.match(/ Gecko\/\d+/), t.isOpera = typeof opera == "object" && Object.prototype.toString.call(window.opera) == "[object Opera]", t.isWebKit = parseFloat(s.split("WebKit/")[1]) || undefined, t.isChrome = parseFloat(s.split(" Chrome/")[1]) || undefined, t.isEdge = parseFloat(s.split(" Edge/")[1]) || undefined, t.isAIR = s.indexOf("AdobeAIR") >= 0, t.isAndroid = s.indexOf("Android") >= 0, t.isChromeOS = s.indexOf(" CrOS ") >= 0, t.isIOS = /iPad|iPhone|iPod/.test(s) && !window.MSStream, t.isIOS && (t.isMac = !0), t.isMobile = t.isIOS || t.isAndroid }), define("ace/lib/dom", ["require", "exports", "module", "ace/lib/useragent"], function (e, t, n) { "use strict"; var r = e("./useragent"), i = "http://www.w3.org/1999/xhtml"; t.buildDom = function o(e, t, n) { if (typeof e == "string" && e) { var r = document.createTextNode(e); return t && t.appendChild(r), r } if (!Array.isArray(e)) return e && e.appendChild && t && t.appendChild(e), e; if (typeof e[0] != "string" || !e[0]) { var i = []; for (var s = 0; s < e.length; s++) { var u = o(e[s], t, n); u && i.push(u) } return i } var a = document.createElement(e[0]), f = e[1], l = 1; f && typeof f == "object" && !Array.isArray(f) && (l = 2); for (var s = l; s < e.length; s++)o(e[s], a, n); return l == 2 && Object.keys(f).forEach(function (e) { var t = f[e]; e === "class" ? a.className = Array.isArray(t) ? t.join(" ") : t : typeof t == "function" || e == "value" || e[0] == "$" ? a[e] = t : e === "ref" ? n && (n[t] = a) : t != null && a.setAttribute(e, t) }), t && t.appendChild(a), a }, t.getDocumentHead = function (e) { return e || (e = document), e.head || e.getElementsByTagName("head")[0] || e.documentElement }, t.createElement = function (e, t) { return document.createElementNS ? document.createElementNS(t || i, e) : document.createElement(e) }, t.removeChildren = function (e) { e.innerHTML = "" }, t.createTextNode = function (e, t) { var n = t ? t.ownerDocument : document; return n.createTextNode(e) }, t.createFragment = function (e) { var t = e ? e.ownerDocument : document; return t.createDocumentFragment() }, t.hasCssClass = function (e, t) { var n = (e.className + "").split(/\s+/g); return n.indexOf(t) !== -1 }, t.addCssClass = function (e, n) { t.hasCssClass(e, n) || (e.className += " " + n) }, t.removeCssClass = function (e, t) { var n = e.className.split(/\s+/g); for (; ;) { var r = n.indexOf(t); if (r == -1) break; n.splice(r, 1) } e.className = n.join(" ") }, t.toggleCssClass = function (e, t) { var n = e.className.split(/\s+/g), r = !0; for (; ;) { var i = n.indexOf(t); if (i == -1) break; r = !1, n.splice(i, 1) } return r && n.push(t), e.className = n.join(" "), r }, t.setCssClass = function (e, n, r) { r ? t.addCssClass(e, n) : t.removeCssClass(e, n) }, t.hasCssString = function (e, t) { var n = 0, r; t = t || document; if (r = t.querySelectorAll("style")) while (n < r.length) if (r[n++].id === e) return !0 }, t.importCssString = function (n, r, i) { var s = i; if (!i || !i.getRootNode) s = document; else { s = i.getRootNode(); if (!s || s == i) s = document } var o = s.ownerDocument || s; if (r && t.hasCssString(r, s)) return null; r && (n += "\n/*# sourceURL=ace/css/" + r + " */"); var u = t.createElement("style"); u.appendChild(o.createTextNode(n)), r && (u.id = r), s == o && (s = t.getDocumentHead(o)), s.insertBefore(u, s.firstChild) }, t.importCssStylsheet = function (e, n) { t.buildDom(["link", { rel: "stylesheet", href: e }], t.getDocumentHead(n)) }, t.scrollbarWidth = function (e) { var n = t.createElement("ace_inner"); n.style.width = "100%", n.style.minWidth = "0px", n.style.height = "200px", n.style.display = "block"; var r = t.createElement("ace_outer"), i = r.style; i.position = "absolute", i.left = "-10000px", i.overflow = "hidden", i.width = "200px", i.minWidth = "0px", i.height = "150px", i.display = "block", r.appendChild(n); var s = e.documentElement; s.appendChild(r); var o = n.offsetWidth; i.overflow = "scroll"; var u = n.offsetWidth; return o == u && (u = r.clientWidth), s.removeChild(r), o - u }, typeof document == "undefined" && (t.importCssString = function () { }), t.computedStyle = function (e, t) { return window.getComputedStyle(e, "") || {} }, t.setStyle = function (e, t, n) { e[t] !== n && (e[t] = n) }, t.HAS_CSS_ANIMATION = !1, t.HAS_CSS_TRANSFORMS = !1, t.HI_DPI = r.isWin ? typeof window != "undefined" && window.devicePixelRatio >= 1.5 : !0; if (typeof document != "undefined") { var s = document.createElement("div"); t.HI_DPI && s.style.transform !== undefined && (t.HAS_CSS_TRANSFORMS = !0), !r.isEdge && typeof s.style.animationName != "undefined" && (t.HAS_CSS_ANIMATION = !0), s = null } t.HAS_CSS_TRANSFORMS ? t.translate = function (e, t, n) { e.style.transform = "translate(" + Math.round(t) + "px, " + Math.round(n) + "px)" } : t.translate = function (e, t, n) { e.style.top = Math.round(n) + "px", e.style.left = Math.round(t) + "px" } }), define("ace/lib/oop", ["require", "exports", "module"], function (e, t, n) { "use strict"; t.inherits = function (e, t) { e.super_ = t, e.prototype = Object.create(t.prototype, { constructor: { value: e, enumerable: !1, writable: !0, configurable: !0 } }) }, t.mixin = function (e, t) { for (var n in t) e[n] = t[n]; return e }, t.implement = function (e, n) { t.mixin(e, n) } }), define("ace/lib/keys", ["require", "exports", "module", "ace/lib/oop"], function (e, t, n) { "use strict"; var r = e("./oop"), i = function () { var e = { MODIFIER_KEYS: { 16: "Shift", 17: "Ctrl", 18: "Alt", 224: "Meta", 91: "MetaLeft", 92: "MetaRight", 93: "ContextMenu" }, KEY_MODS: { ctrl: 1, alt: 2, option: 2, shift: 4, "super": 8, meta: 8, command: 8, cmd: 8, control: 1 }, FUNCTION_KEYS: { 8: "Backspace", 9: "Tab", 13: "Return", 19: "Pause", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "Print", 45: "Insert", 46: "Delete", 96: "Numpad0", 97: "Numpad1", 98: "Numpad2", 99: "Numpad3", 100: "Numpad4", 101: "Numpad5", 102: "Numpad6", 103: "Numpad7", 104: "Numpad8", 105: "Numpad9", "-13": "NumpadEnter", 112: "F1", 113: "F2", 114: "F3", 115: "F4", 116: "F5", 117: "F6", 118: "F7", 119: "F8", 120: "F9", 121: "F10", 122: "F11", 123: "F12", 144: "Numlock", 145: "Scrolllock" }, PRINTABLE_KEYS: { 32: " ", 48: "0", 49: "1", 50: "2", 51: "3", 52: "4", 53: "5", 54: "6", 55: "7", 56: "8", 57: "9", 59: ";", 61: "=", 65: "a", 66: "b", 67: "c", 68: "d", 69: "e", 70: "f", 71: "g", 72: "h", 73: "i", 74: "j", 75: "k", 76: "l", 77: "m", 78: "n", 79: "o", 80: "p", 81: "q", 82: "r", 83: "s", 84: "t", 85: "u", 86: "v", 87: "w", 88: "x", 89: "y", 90: "z", 107: "+", 109: "-", 110: ".", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'", 111: "/", 106: "*" } }, t, n; for (n in e.FUNCTION_KEYS) t = e.FUNCTION_KEYS[n].toLowerCase(), e[t] = parseInt(n, 10); for (n in e.PRINTABLE_KEYS) t = e.PRINTABLE_KEYS[n].toLowerCase(), e[t] = parseInt(n, 10); return r.mixin(e, e.MODIFIER_KEYS), r.mixin(e, e.PRINTABLE_KEYS), r.mixin(e, e.FUNCTION_KEYS), e.enter = e["return"], e.escape = e.esc, e.del = e["delete"], e[173] = "-", function () { var t = ["cmd", "ctrl", "alt", "shift"]; for (var n = Math.pow(2, t.length); n--;)e.KEY_MODS[n] = t.filter(function (t) { return n & e.KEY_MODS[t] }).join("-") + "-" }(), e.KEY_MODS[0] = "", e.KEY_MODS[-1] = "input-", e }(); r.mixin(t, i), t.keyCodeToString = function (e) { var t = i[e]; return typeof t != "string" && (t = String.fromCharCode(e)), t.toLowerCase() } }), define("ace/lib/event", ["require", "exports", "module", "ace/lib/keys", "ace/lib/useragent"], function (e, t, n) { "use strict"; function a() { u = !1; try { document.createComment("").addEventListener("test", function () { }, { get passive() { u = { passive: !1 } } }) } catch (e) { } } function f() { return u == undefined && a(), u } function l(e, t, n) { this.elem = e, this.type = t, this.callback = n } function d(e, t, n) { var u = p(t); if (!i.isMac && s) { t.getModifierState && (t.getModifierState("OS") || t.getModifierState("Win")) && (u |= 8); if (s.altGr) { if ((3 & u) == 3) return; s.altGr = 0 } if (n === 18 || n === 17) { var a = "location" in t ? t.location : t.keyLocation; if (n === 17 && a === 1) s[n] == 1 && (o = t.timeStamp); else if (n === 18 && u === 3 && a === 2) { var f = t.timeStamp - o; f < 50 && (s.altGr = !0) } } } n in r.MODIFIER_KEYS && (n = -1); if (!u && n === 13) { var a = "location" in t ? t.location : t.keyLocation; if (a === 3) { e(t, u, -n); if (t.defaultPrevented) return } } if (i.isChromeOS && u & 8) { e(t, u, n); if (t.defaultPrevented) return; u &= -9 } return !!u || n in r.FUNCTION_KEYS || n in r.PRINTABLE_KEYS ? e(t, u, n) : !1 } function v() { s = Object.create(null) } var r = e("./keys"), i = e("./useragent"), s = null, o = 0, u; l.prototype.destroy = function () { h(this.elem, this.type, this.callback), this.elem = this.type = this.callback = undefined }; var c = t.addListener = function (e, t, n, r) { e.addEventListener(t, n, f()), r && r.$toDestroy.push(new l(e, t, n)) }, h = t.removeListener = function (e, t, n) { e.removeEventListener(t, n, f()) }; t.stopEvent = function (e) { return t.stopPropagation(e), t.preventDefault(e), !1 }, t.stopPropagation = function (e) { e.stopPropagation && e.stopPropagation() }, t.preventDefault = function (e) { e.preventDefault && e.preventDefault() }, t.getButton = function (e) { return e.type == "dblclick" ? 0 : e.type == "contextmenu" || i.isMac && e.ctrlKey && !e.altKey && !e.shiftKey ? 2 : e.button }, t.capture = function (e, t, n) { function r(e) { t && t(e), n && n(e), h(document, "mousemove", t), h(document, "mouseup", r), h(document, "dragstart", r) } return c(document, "mousemove", t), c(document, "mouseup", r), c(document, "dragstart", r), r }, t.addMouseWheelListener = function (e, t, n) { "onmousewheel" in e ? c(e, "mousewheel", function (e) { var n = 8; e.wheelDeltaX !== undefined ? (e.wheelX = -e.wheelDeltaX / n, e.wheelY = -e.wheelDeltaY / n) : (e.wheelX = 0, e.wheelY = -e.wheelDelta / n), t(e) }, n) : "onwheel" in e ? c(e, "wheel", function (e) { var n = .35; switch (e.deltaMode) { case e.DOM_DELTA_PIXEL: e.wheelX = e.deltaX * n || 0, e.wheelY = e.deltaY * n || 0; break; case e.DOM_DELTA_LINE: case e.DOM_DELTA_PAGE: e.wheelX = (e.deltaX || 0) * 5, e.wheelY = (e.deltaY || 0) * 5 }t(e) }, n) : c(e, "DOMMouseScroll", function (e) { e.axis && e.axis == e.HORIZONTAL_AXIS ? (e.wheelX = (e.detail || 0) * 5, e.wheelY = 0) : (e.wheelX = 0, e.wheelY = (e.detail || 0) * 5), t(e) }, n) }, t.addMultiMouseDownListener = function (e, n, r, s, o) { function p(e) { t.getButton(e) !== 0 ? u = 0 : e.detail > 1 ? (u++, u > 4 && (u = 1)) : u = 1; if (i.isIE) { var o = Math.abs(e.clientX - a) > 5 || Math.abs(e.clientY - f) > 5; if (!l || o) u = 1; l && clearTimeout(l), l = setTimeout(function () { l = null }, n[u - 1] || 600), u == 1 && (a = e.clientX, f = e.clientY) } e._clicks = u, r[s]("mousedown", e); if (u > 4) u = 0; else if (u > 1) return r[s](h[u], e) } var u = 0, a, f, l, h = { 2: "dblclick", 3: "tripleclick", 4: "quadclick" }; Array.isArray(e) || (e = [e]), e.forEach(function (e) { c(e, "mousedown", p, o) }) }; var p = function (e) { return 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0) }; t.getModifierString = function (e) { return r.KEY_MODS[p(e)] }, t.addCommandKeyListener = function (e, n, r) { if (i.isOldGecko || i.isOpera && !("KeyboardEvent" in window)) { var o = null; c(e, "keydown", function (e) { o = e.keyCode }, r), c(e, "keypress", function (e) { return d(n, e, o) }, r) } else { var u = null; c(e, "keydown", function (e) { s[e.keyCode] = (s[e.keyCode] || 0) + 1; var t = d(n, e, e.keyCode); return u = e.defaultPrevented, t }, r), c(e, "keypress", function (e) { u && (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) && (t.stopEvent(e), u = null) }, r), c(e, "keyup", function (e) { s[e.keyCode] = null }, r), s || (v(), c(window, "focus", v)) } }; if (typeof window == "object" && window.postMessage && !i.isOldIE) { var m = 1; t.nextTick = function (e, n) { n = n || window; var r = "zero-timeout-message-" + m++, i = function (s) { s.data == r && (t.stopPropagation(s), h(n, "message", i), e()) }; c(n, "message", i), n.postMessage(r, "*") } } t.$idleBlocked = !1, t.onIdle = function (e, n) { return setTimeout(function r() { t.$idleBlocked ? setTimeout(r, 100) : e() }, n) }, t.$idleBlockId = null, t.blockIdle = function (e) { t.$idleBlockId && clearTimeout(t.$idleBlockId), t.$idleBlocked = !0, t.$idleBlockId = setTimeout(function () { t.$idleBlocked = !1 }, e || 100) }, t.nextFrame = typeof window == "object" && (window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame), t.nextFrame ? t.nextFrame = t.nextFrame.bind(window) : t.nextFrame = function (e) { setTimeout(e, 17) } }), define("ace/range", ["require", "exports", "module"], function (e, t, n) { "use strict"; var r = function (e, t) { return e.row - t.row || e.column - t.column }, i = function (e, t, n, r) { this.start = { row: e, column: t }, this.end = { row: n, column: r } }; (function () { this.isEqual = function (e) { return this.start.row === e.start.row && this.end.row === e.end.row && this.start.column === e.start.column && this.end.column === e.end.column }, this.toString = function () { return "Range: [" + this.start.row + "/" + this.start.column + "] -> [" + this.end.row + "/" + this.end.column + "]" }, this.contains = function (e, t) { return this.compare(e, t) == 0 }, this.compareRange = function (e) { var t, n = e.end, r = e.start; return t = this.compare(n.row, n.column), t == 1 ? (t = this.compare(r.row, r.column), t == 1 ? 2 : t == 0 ? 1 : 0) : t == -1 ? -2 : (t = this.compare(r.row, r.column), t == -1 ? -1 : t == 1 ? 42 : 0) }, this.comparePoint = function (e) { return this.compare(e.row, e.column) }, this.containsRange = function (e) { return this.comparePoint(e.start) == 0 && this.comparePoint(e.end) == 0 }, this.intersects = function (e) { var t = this.compareRange(e); return t == -1 || t == 0 || t == 1 }, this.isEnd = function (e, t) { return this.end.row == e && this.end.column == t }, this.isStart = function (e, t) { return this.start.row == e && this.start.column == t }, this.setStart = function (e, t) { typeof e == "object" ? (this.start.column = e.column, this.start.row = e.row) : (this.start.row = e, this.start.column = t) }, this.setEnd = function (e, t) { typeof e == "object" ? (this.end.column = e.column, this.end.row = e.row) : (this.end.row = e, this.end.column = t) }, this.inside = function (e, t) { return this.compare(e, t) == 0 ? this.isEnd(e, t) || this.isStart(e, t) ? !1 : !0 : !1 }, this.insideStart = function (e, t) { return this.compare(e, t) == 0 ? this.isEnd(e, t) ? !1 : !0 : !1 }, this.insideEnd = function (e, t) { return this.compare(e, t) == 0 ? this.isStart(e, t) ? !1 : !0 : !1 }, this.compare = function (e, t) { return !this.isMultiLine() && e === this.start.row ? t < this.start.column ? -1 : t > this.end.column ? 1 : 0 : e < this.start.row ? -1 : e > this.end.row ? 1 : this.start.row === e ? t >= this.start.column ? 0 : -1 : this.end.row === e ? t <= this.end.column ? 0 : 1 : 0 }, this.compareStart = function (e, t) { return this.start.row == e && this.start.column == t ? -1 : this.compare(e, t) }, this.compareEnd = function (e, t) { return this.end.row == e && this.end.column == t ? 1 : this.compare(e, t) }, this.compareInside = function (e, t) { return this.end.row == e && this.end.column == t ? 1 : this.start.row == e && this.start.column == t ? -1 : this.compare(e, t) }, this.clipRows = function (e, t) { if (this.end.row > t) var n = { row: t + 1, column: 0 }; else if (this.end.row < e) var n = { row: e, column: 0 }; if (this.start.row > t) var r = { row: t + 1, column: 0 }; else if (this.start.row < e) var r = { row: e, column: 0 }; return i.fromPoints(r || this.start, n || this.end) }, this.extend = function (e, t) { var n = this.compare(e, t); if (n == 0) return this; if (n == -1) var r = { row: e, column: t }; else var s = { row: e, column: t }; return i.fromPoints(r || this.start, s || this.end) }, this.isEmpty = function () { return this.start.row === this.end.row && this.start.column === this.end.column }, this.isMultiLine = function () { return this.start.row !== this.end.row }, this.clone = function () { return i.fromPoints(this.start, this.end) }, this.collapseRows = function () { return this.end.column == 0 ? new i(this.start.row, 0, Math.max(this.start.row, this.end.row - 1), 0) : new i(this.start.row, 0, this.end.row, 0) }, this.toScreenRange = function (e) { var t = e.documentToScreenPosition(this.start), n = e.documentToScreenPosition(this.end); return new i(t.row, t.column, n.row, n.column) }, this.moveBy = function (e, t) { this.start.row += e, this.start.column += t, this.end.row += e, this.end.column += t } }).call(i.prototype), i.fromPoints = function (e, t) { return new i(e.row, e.column, t.row, t.column) }, i.comparePoints = r, i.comparePoints = function (e, t) { return e.row - t.row || e.column - t.column }, t.Range = i }), define("ace/lib/lang", ["require", "exports", "module"], function (e, t, n) { "use strict"; t.last = function (e) { return e[e.length - 1] }, t.stringReverse = function (e) { return e.split("").reverse().join("") }, t.stringRepeat = function (e, t) { var n = ""; while (t > 0) { t & 1 && (n += e); if (t >>= 1) e += e } return n }; var r = /^\s\s*/, i = /\s\s*$/; t.stringTrimLeft = function (e) { return e.replace(r, "") }, t.stringTrimRight = function (e) { return e.replace(i, "") }, t.copyObject = function (e) { var t = {}; for (var n in e) t[n] = e[n]; return t }, t.copyArray = function (e) { var t = []; for (var n = 0, r = e.length; n < r; n++)e[n] && typeof e[n] == "object" ? t[n] = this.copyObject(e[n]) : t[n] = e[n]; return t }, t.deepCopy = function s(e) { if (typeof e != "object" || !e) return e; var t; if (Array.isArray(e)) { t = []; for (var n = 0; n < e.length; n++)t[n] = s(e[n]); return t } if (Object.prototype.toString.call(e) !== "[object Object]") return e; t = {}; for (var n in e) t[n] = s(e[n]); return t }, t.arrayToMap = function (e) { var t = {}; for (var n = 0; n < e.length; n++)t[e[n]] = 1; return t }, t.createMap = function (e) { var t = Object.create(null); for (var n in e) t[n] = e[n]; return t }, t.arrayRemove = function (e, t) { for (var n = 0; n <= e.length; n++)t === e[n] && e.splice(n, 1) }, t.escapeRegExp = function (e) { return e.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1") }, t.escapeHTML = function (e) { return ("" + e).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/ Date.now() - 50 ? !0 : r = !1 }, cancel: function () { r = Date.now() } } }), define("ace/keyboard/textinput", ["require", "exports", "module", "ace/lib/event", "ace/lib/useragent", "ace/lib/dom", "ace/lib/lang", "ace/clipboard", "ace/lib/keys"], function (e, t, n) { "use strict"; var r = e("../lib/event"), i = e("../lib/useragent"), s = e("../lib/dom"), o = e("../lib/lang"), u = e("../clipboard"), a = i.isChrome < 18, f = i.isIE, l = i.isChrome > 63, c = 400, h = e("../lib/keys"), p = h.KEY_MODS, d = i.isIOS, v = d ? /\s/ : /\n/, m = i.isMobile, g = function (e, t) { function X() { x = !0, n.blur(), n.focus(), x = !1 } function $(e) { e.keyCode == 27 && n.value.length < n.selectionStart && (b || (T = n.value), N = C = -1, O()), V() } function K() { clearTimeout(J), J = setTimeout(function () { E && (n.style.cssText = E, E = ""), t.renderer.$isMousePressed = !1, t.renderer.$keepTextAreaAtCursor && t.renderer.$moveTextAreaToCursor() }, 0) } function G(e, t, n) { var r = null, i = !1; n.addEventListener("keydown", function (e) { r && clearTimeout(r), i = !0 }, !0), n.addEventListener("keyup", function (e) { r = setTimeout(function () { i = !1 }, 100) }, !0); var s = function (e) { if (document.activeElement !== n) return; if (i || b || t.$mouseHandler.isMousePressed) return; if (g) return; var r = n.selectionStart, s = n.selectionEnd, o = null, u = 0; if (r == 0) o = h.up; else if (r == 1) o = h.home; else if (s > C && T[s] == "\n") o = h.end; else if (r < N && T[r - 1] == " ") o = h.left, u = p.option; else if (r < N || r == N && C != N && r == s) o = h.left; else if (s > C && T.slice(0, s).split("\n").length > 2) o = h.down; else if (s > C && T[s - 1] == " ") o = h.right, u = p.option; else if (s > C || s == C && C != N && r == s) o = h.right; r !== s && (u |= p.shift); if (o) { var a = t.onCommandKey({}, u, o); if (!a && t.commands) { o = h.keyCodeToString(o); var f = t.commands.findKeyCommand(u, o); f && t.execCommand(f) } N = r, C = s, O("") } }; document.addEventListener("selectionchange", s), t.on("destroy", function () { document.removeEventListener("selectionchange", s) }) } var n = s.createElement("textarea"); n.className = "ace_text-input", n.setAttribute("wrap", "off"), n.setAttribute("autocorrect", "off"), n.setAttribute("autocapitalize", "off"), n.setAttribute("spellcheck", !1), n.style.opacity = "0", e.insertBefore(n, e.firstChild); var g = !1, y = !1, b = !1, w = !1, E = ""; m || (n.style.fontSize = "1px"); var S = !1, x = !1, T = "", N = 0, C = 0, k = 0; try { var L = document.activeElement === n } catch (A) { } r.addListener(n, "blur", function (e) { if (x) return; t.onBlur(e), L = !1 }, t), r.addListener(n, "focus", function (e) { if (x) return; L = !0; if (i.isEdge) try { if (!document.hasFocus()) return } catch (e) { } t.onFocus(e), i.isEdge ? setTimeout(O) : O() }, t), this.$focusScroll = !1, this.focus = function () { if (E || l || this.$focusScroll == "browser") return n.focus({ preventScroll: !0 }); var e = n.style.top; n.style.position = "fixed", n.style.top = "0px"; try { var t = n.getBoundingClientRect().top != 0 } catch (r) { return } var i = []; if (t) { var s = n.parentElement; while (s && s.nodeType == 1) i.push(s), s.setAttribute("ace_nocontext", !0), !s.parentElement && s.getRootNode ? s = s.getRootNode().host : s = s.parentElement } n.focus({ preventScroll: !0 }), t && i.forEach(function (e) { e.removeAttribute("ace_nocontext") }), setTimeout(function () { n.style.position = "", n.style.top == "0px" && (n.style.top = e) }, 0) }, this.blur = function () { n.blur() }, this.isFocused = function () { return L }, t.on("beforeEndOperation", function () { var e = t.curOp, r = e && e.command && e.command.name; if (r == "insertstring") return; var i = r && (e.docChanged || e.selectionChanged); b && i && (T = n.value = "", W()), O() }); var O = d ? function (e) { if (!L || g && !e || w) return; e || (e = ""); var r = "\n ab" + e + "cde fg\n"; r != n.value && (n.value = T = r); var i = 4, s = 4 + (e.length || (t.selection.isEmpty() ? 0 : 1)); (N != i || C != s) && n.setSelectionRange(i, s), N = i, C = s } : function () { if (b || w) return; if (!L && !P) return; b = !0; var e = 0, r = 0, i = ""; if (t.session) { var s = t.selection, o = s.getRange(), u = s.cursor.row; e = o.start.column, r = o.end.column, i = t.session.getLine(u); if (o.start.row != u) { var a = t.session.getLine(u - 1); e = o.start.row < u - 1 ? 0 : e, r += a.length + 1, i = a + "\n" + i } else if (o.end.row != u) { var f = t.session.getLine(u + 1); r = o.end.row > u + 1 ? f.length : r, r += i.length + 1, i = i + "\n" + f } else m && u > 0 && (i = "\n" + i, r += 1, e += 1); i.length > c && (e < c && r < c ? i = i.slice(0, c) : (i = "\n", e = 0, r = 1)) } var l = i + "\n\n"; l != T && (n.value = T = l, N = C = l.length), P && (N = n.selectionStart, C = n.selectionEnd); if (C != r || N != e || n.selectionEnd != C) try { n.setSelectionRange(e, r), N = e, C = r } catch (h) { } b = !1 }; this.resetSelection = O, L && t.onFocus(); var M = function (e) { return e.selectionStart === 0 && e.selectionEnd >= T.length && e.value === T && T && e.selectionEnd !== C }, _ = function (e) { if (b) return; g ? g = !1 : M(n) ? (t.selectAll(), O()) : m && n.selectionStart != N && O() }, D = null; this.setInputHandler = function (e) { D = e }, this.getInputHandler = function () { return D }; var P = !1, H = function (e, r) { P && (P = !1); if (y) return O(), e && t.onPaste(e), y = !1, ""; var i = n.selectionStart, s = n.selectionEnd, o = N, u = T.length - C, a = e, f = e.length - i, l = e.length - s, c = 0; while (o > 0 && T[c] == e[c]) c++, o--; a = a.slice(c), c = 1; while (u > 0 && T.length - c > N - 1 && T[T.length - c] == e[e.length - c]) c++, u--; f -= c - 1, l -= c - 1; var h = a.length - c + 1; return h < 0 && (o = -h, h = 0), a = a.slice(0, h), !r && !a && !f && !o && !u && !l ? "" : (w = !0, a && !o && !u && !f && !l || S ? t.onTextInput(a) : t.onTextInput(a, { extendLeft: o, extendRight: u, restoreStart: f, restoreEnd: l }), w = !1, T = e, N = i, C = s, k = l, a) }, B = function (e) { if (b) return z(); if (e && e.inputType) { if (e.inputType == "historyUndo") return t.execCommand("undo"); if (e.inputType == "historyRedo") return t.execCommand("redo") } var r = n.value, i = H(r, !0); (r.length > c + 100 || v.test(i) || m && N < 1 && N == C) && O() }, j = function (e, t, n) { var r = e.clipboardData || window.clipboardData; if (!r || a) return; var i = f || n ? "Text" : "text/plain"; try { return t ? r.setData(i, t) !== !1 : r.getData(i) } catch (e) { if (!n) return j(e, t, !0) } }, F = function (e, i) { var s = t.getCopyText(); if (!s) return r.preventDefault(e); j(e, s) ? (d && (O(s), g = s, setTimeout(function () { g = !1 }, 10)), i ? t.onCut() : t.onCopy(), r.preventDefault(e)) : (g = !0, n.value = s, n.select(), setTimeout(function () { g = !1, O(), i ? t.onCut() : t.onCopy() })) }, I = function (e) { F(e, !0) }, q = function (e) { F(e, !1) }, R = function (e) { var s = j(e); if (u.pasteCancelled()) return; typeof s == "string" ? (s && t.onPaste(s, e), i.isIE && setTimeout(O), r.preventDefault(e)) : (n.value = "", y = !0) }; r.addCommandKeyListener(n, t.onCommandKey.bind(t), t), r.addListener(n, "select", _, t), r.addListener(n, "input", B, t), r.addListener(n, "cut", I, t), r.addListener(n, "copy", q, t), r.addListener(n, "paste", R, t), (!("oncut" in n) || !("oncopy" in n) || !("onpaste" in n)) && r.addListener(e, "keydown", function (e) { if (i.isMac && !e.metaKey || !e.ctrlKey) return; switch (e.keyCode) { case 67: q(e); break; case 86: R(e); break; case 88: I(e) } }, t); var U = function (e) { if (b || !t.onCompositionStart || t.$readOnly) return; b = {}; if (S) return; e.data && (b.useTextareaForIME = !1), setTimeout(z, 0), t._signal("compositionStart"), t.on("mousedown", X); var r = t.getSelectionRange(); r.end.row = r.start.row, r.end.column = r.start.column, b.markerRange = r, b.selectionStart = N, t.onCompositionStart(b), b.useTextareaForIME ? (T = n.value = "", N = 0, C = 0) : (n.msGetInputContext && (b.context = n.msGetInputContext()), n.getInputContext && (b.context = n.getInputContext())) }, z = function () { if (!b || !t.onCompositionUpdate || t.$readOnly) return; if (S) return X(); if (b.useTextareaForIME) t.onCompositionUpdate(n.value); else { var e = n.value; H(e), b.markerRange && (b.context && (b.markerRange.start.column = b.selectionStart = b.context.compositionStartOffset), b.markerRange.end.column = b.markerRange.start.column + C - b.selectionStart + k) } }, W = function (e) { if (!t.onCompositionEnd || t.$readOnly) return; b = !1, t.onCompositionEnd(), t.off("mousedown", X), e && B() }, V = o.delayedCall(z, 50).schedule.bind(null, null); r.addListener(n, "compositionstart", U, t), r.addListener(n, "compositionupdate", z, t), r.addListener(n, "keyup", $, t), r.addListener(n, "keydown", V, t), r.addListener(n, "compositionend", W, t), this.getElement = function () { return n }, this.setCommandMode = function (e) { S = e, n.readOnly = !1 }, this.setReadOnly = function (e) { S || (n.readOnly = e) }, this.setCopyWithEmptySelection = function (e) { }, this.onContextMenu = function (e) { P = !0, O(), t._emit("nativecontextmenu", { target: t, domEvent: e }), this.moveToMouse(e, !0) }, this.moveToMouse = function (e, o) { E || (E = n.style.cssText), n.style.cssText = (o ? "z-index:100000;" : "") + (i.isIE ? "opacity:0.1;" : "") + "text-indent: -" + (N + C) * t.renderer.characterWidth * .5 + "px;"; var u = t.container.getBoundingClientRect(), a = s.computedStyle(t.container), f = u.top + (parseInt(a.borderTopWidth) || 0), l = u.left + (parseInt(u.borderLeftWidth) || 0), c = u.bottom - f - n.clientHeight - 2, h = function (e) { s.translate(n, e.clientX - l - 2, Math.min(e.clientY - f - 2, c)) }; h(e); if (e.type != "mousedown") return; t.renderer.$isMousePressed = !0, clearTimeout(J), i.isWin && r.capture(t.container, h, K) }, this.onContextMenuClose = K; var J, Q = function (e) { t.textInput.onContextMenu(e), K() }; r.addListener(n, "mouseup", Q, t), r.addListener(n, "mousedown", function (e) { e.preventDefault(), K() }, t), r.addListener(t.renderer.scroller, "contextmenu", Q, t), r.addListener(n, "contextmenu", Q, t), d && G(e, t, n) }; t.TextInput = g, t.$setUserAgentForTests = function (e, t) { m = e, d = t } }), define("ace/mouse/default_handlers", ["require", "exports", "module", "ace/lib/useragent"], function (e, t, n) { "use strict"; function o(e) { e.$clickSelection = null; var t = e.editor; t.setDefaultHandler("mousedown", this.onMouseDown.bind(e)), t.setDefaultHandler("dblclick", this.onDoubleClick.bind(e)), t.setDefaultHandler("tripleclick", this.onTripleClick.bind(e)), t.setDefaultHandler("quadclick", this.onQuadClick.bind(e)), t.setDefaultHandler("mousewheel", this.onMouseWheel.bind(e)); var n = ["select", "startSelect", "selectEnd", "selectAllEnd", "selectByWordsEnd", "selectByLinesEnd", "dragWait", "dragWaitEnd", "focusWait"]; n.forEach(function (t) { e[t] = this[t] }, this), e.selectByLines = this.extendSelectionBy.bind(e, "getLineRange"), e.selectByWords = this.extendSelectionBy.bind(e, "getWordRange") } function u(e, t, n, r) { return Math.sqrt(Math.pow(n - e, 2) + Math.pow(r - t, 2)) } function a(e, t) { if (e.start.row == e.end.row) var n = 2 * t.column - e.start.column - e.end.column; else if (e.start.row == e.end.row - 1 && !e.start.column && !e.end.column) var n = t.column - 4; else var n = 2 * t.row - e.start.row - e.end.row; return n < 0 ? { cursor: e.start, anchor: e.end } : { cursor: e.end, anchor: e.start } } var r = e("../lib/useragent"), i = 0, s = 550; (function () { this.onMouseDown = function (e) { var t = e.inSelection(), n = e.getDocumentPosition(); this.mousedownEvent = e; var i = this.editor, s = e.getButton(); if (s !== 0) { var o = i.getSelectionRange(), u = o.isEmpty(); (u || s == 1) && i.selection.moveToPosition(n), s == 2 && (i.textInput.onContextMenu(e.domEvent), r.isMozilla || e.preventDefault()); return } this.mousedownEvent.time = Date.now(); if (t && !i.isFocused()) { i.focus(); if (this.$focusTimeout && !this.$clickSelection && !i.inMultiSelectMode) { this.setState("focusWait"), this.captureMouse(e); return } } return this.captureMouse(e), this.startSelect(n, e.domEvent._clicks > 1), e.preventDefault() }, this.startSelect = function (e, t) { e = e || this.editor.renderer.screenToTextCoordinates(this.x, this.y); var n = this.editor; if (!this.mousedownEvent) return; this.mousedownEvent.getShiftKey() ? n.selection.selectToPosition(e) : t || n.selection.moveToPosition(e), t || this.select(), n.renderer.scroller.setCapture && n.renderer.scroller.setCapture(), n.setStyle("ace_selecting"), this.setState("select") }, this.select = function () { var e, t = this.editor, n = t.renderer.screenToTextCoordinates(this.x, this.y); if (this.$clickSelection) { var r = this.$clickSelection.comparePoint(n); if (r == -1) e = this.$clickSelection.end; else if (r == 1) e = this.$clickSelection.start; else { var i = a(this.$clickSelection, n); n = i.cursor, e = i.anchor } t.selection.setSelectionAnchor(e.row, e.column) } t.selection.selectToPosition(n), t.renderer.scrollCursorIntoView() }, this.extendSelectionBy = function (e) { var t, n = this.editor, r = n.renderer.screenToTextCoordinates(this.x, this.y), i = n.selection[e](r.row, r.column); if (this.$clickSelection) { var s = this.$clickSelection.comparePoint(i.start), o = this.$clickSelection.comparePoint(i.end); if (s == -1 && o <= 0) { t = this.$clickSelection.end; if (i.end.row != r.row || i.end.column != r.column) r = i.start } else if (o == 1 && s >= 0) { t = this.$clickSelection.start; if (i.start.row != r.row || i.start.column != r.column) r = i.end } else if (s == -1 && o == 1) r = i.end, t = i.start; else { var u = a(this.$clickSelection, r); r = u.cursor, t = u.anchor } n.selection.setSelectionAnchor(t.row, t.column) } n.selection.selectToPosition(r), n.renderer.scrollCursorIntoView() }, this.selectEnd = this.selectAllEnd = this.selectByWordsEnd = this.selectByLinesEnd = function () { this.$clickSelection = null, this.editor.unsetStyle("ace_selecting"), this.editor.renderer.scroller.releaseCapture && this.editor.renderer.scroller.releaseCapture() }, this.focusWait = function () { var e = u(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y), t = Date.now(); (e > i || t - this.mousedownEvent.time > this.$focusTimeout) && this.startSelect(this.mousedownEvent.getDocumentPosition()) }, this.onDoubleClick = function (e) { var t = e.getDocumentPosition(), n = this.editor, r = n.session, i = r.getBracketRange(t); i ? (i.isEmpty() && (i.start.column--, i.end.column++), this.setState("select")) : (i = n.selection.getWordRange(t.row, t.column), this.setState("selectByWords")), this.$clickSelection = i, this.select() }, this.onTripleClick = function (e) { var t = e.getDocumentPosition(), n = this.editor; this.setState("selectByLines"); var r = n.getSelectionRange(); r.isMultiLine() && r.contains(t.row, t.column) ? (this.$clickSelection = n.selection.getLineRange(r.start.row), this.$clickSelection.end = n.selection.getLineRange(r.end.row).end) : this.$clickSelection = n.selection.getLineRange(t.row), this.select() }, this.onQuadClick = function (e) { var t = this.editor; t.selectAll(), this.$clickSelection = t.getSelectionRange(), this.setState("selectAll") }, this.onMouseWheel = function (e) { if (e.getAccelKey()) return; e.getShiftKey() && e.wheelY && !e.wheelX && (e.wheelX = e.wheelY, e.wheelY = 0); var t = this.editor; this.$lastScroll || (this.$lastScroll = { t: 0, vx: 0, vy: 0, allowed: 0 }); var n = this.$lastScroll, r = e.domEvent.timeStamp, i = r - n.t, o = i ? e.wheelX / i : n.vx, u = i ? e.wheelY / i : n.vy; i < s && (o = (o + n.vx) / 2, u = (u + n.vy) / 2); var a = Math.abs(o / u), f = !1; a >= 1 && t.renderer.isScrollableBy(e.wheelX * e.speed, 0) && (f = !0), a <= 1 && t.renderer.isScrollableBy(0, e.wheelY * e.speed) && (f = !0); if (f) n.allowed = r; else if (r - n.allowed < s) { var l = Math.abs(o) <= 1.5 * Math.abs(n.vx) && Math.abs(u) <= 1.5 * Math.abs(n.vy); l ? (f = !0, n.allowed = r) : n.allowed = 0 } n.t = r, n.vx = o, n.vy = u; if (f) return t.renderer.scrollBy(e.wheelX * e.speed, e.wheelY * e.speed), e.stop() } }).call(o.prototype), t.DefaultHandlers = o }), define("ace/tooltip", ["require", "exports", "module", "ace/lib/oop", "ace/lib/dom"], function (e, t, n) { "use strict"; function s(e) { this.isOpen = !1, this.$element = null, this.$parentNode = e } var r = e("./lib/oop"), i = e("./lib/dom"); (function () { this.$init = function () { return this.$element = i.createElement("div"), this.$element.className = "ace_tooltip", this.$element.style.display = "none", this.$parentNode.appendChild(this.$element), this.$element }, this.getElement = function () { return this.$element || this.$init() }, this.setText = function (e) { this.getElement().textContent = e }, this.setHtml = function (e) { this.getElement().innerHTML = e }, this.setPosition = function (e, t) { this.getElement().style.left = e + "px", this.getElement().style.top = t + "px" }, this.setClassName = function (e) { i.addCssClass(this.getElement(), e) }, this.show = function (e, t, n) { e != null && this.setText(e), t != null && n != null && this.setPosition(t, n), this.isOpen || (this.getElement().style.display = "block", this.isOpen = !0) }, this.hide = function () { this.isOpen && (this.getElement().style.display = "none", this.isOpen = !1) }, this.getHeight = function () { return this.getElement().offsetHeight }, this.getWidth = function () { return this.getElement().offsetWidth }, this.destroy = function () { this.isOpen = !1, this.$element && this.$element.parentNode && this.$element.parentNode.removeChild(this.$element) } }).call(s.prototype), t.Tooltip = s }), define("ace/mouse/default_gutter_handler", ["require", "exports", "module", "ace/lib/dom", "ace/lib/oop", "ace/lib/event", "ace/tooltip"], function (e, t, n) { "use strict"; function u(e) { function l() { var r = u.getDocumentPosition().row, s = n.$annotations[r]; if (!s) return c(); var o = t.session.getLength(); if (r == o) { var a = t.renderer.pixelToScreenCoordinates(0, u.y).row, l = u.$pos; if (a > t.session.documentToScreenRow(l.row, l.column)) return c() } if (f == s) return; f = s.text.join("
"), i.setHtml(f), i.show(), t._signal("showGutterTooltip", i), t.on("mousewheel", c); if (e.$tooltipFollowsMouse) h(u); else { var p = u.domEvent.target, d = p.getBoundingClientRect(), v = i.getElement().style; v.left = d.right + "px", v.top = d.bottom + "px" } } function c() { o && (o = clearTimeout(o)), f && (i.hide(), f = null, t._signal("hideGutterTooltip", i), t.off("mousewheel", c)) } function h(e) { i.setPosition(e.x, e.y) } var t = e.editor, n = t.renderer.$gutterLayer, i = new a(t.container); e.editor.setDefaultHandler("guttermousedown", function (r) { if (!t.isFocused() || r.getButton() != 0) return; var i = n.getRegion(r); if (i == "foldWidgets") return; var s = r.getDocumentPosition().row, o = t.session.selection; if (r.getShiftKey()) o.selectTo(s, 0); else { if (r.domEvent.detail == 2) return t.selectAll(), r.preventDefault(); e.$clickSelection = t.selection.getLineRange(s) } return e.setState("selectByLines"), e.captureMouse(r), r.preventDefault() }); var o, u, f; e.editor.setDefaultHandler("guttermousemove", function (t) { var n = t.domEvent.target || t.domEvent.srcElement; if (r.hasCssClass(n, "ace_fold-widget")) return c(); f && e.$tooltipFollowsMouse && h(t), u = t; if (o) return; o = setTimeout(function () { o = null, u && !e.isMousePressed ? l() : c() }, 50) }), s.addListener(t.renderer.$gutter, "mouseout", function (e) { u = null; if (!f || o) return; o = setTimeout(function () { o = null, c() }, 50) }, t), t.on("changeSession", c) } function a(e) { o.call(this, e) } var r = e("../lib/dom"), i = e("../lib/oop"), s = e("../lib/event"), o = e("../tooltip").Tooltip; i.inherits(a, o), function () { this.setPosition = function (e, t) { var n = window.innerWidth || document.documentElement.clientWidth, r = window.innerHeight || document.documentElement.clientHeight, i = this.getWidth(), s = this.getHeight(); e += 15, t += 15, e + i > n && (e -= e + i - n), t + s > r && (t -= 20 + s), o.prototype.setPosition.call(this, e, t) } }.call(a.prototype), t.GutterHandler = u }), define("ace/mouse/mouse_event", ["require", "exports", "module", "ace/lib/event", "ace/lib/useragent"], function (e, t, n) { "use strict"; var r = e("../lib/event"), i = e("../lib/useragent"), s = t.MouseEvent = function (e, t) { this.domEvent = e, this.editor = t, this.x = this.clientX = e.clientX, this.y = this.clientY = e.clientY, this.$pos = null, this.$inSelection = null, this.propagationStopped = !1, this.defaultPrevented = !1 }; (function () { this.stopPropagation = function () { r.stopPropagation(this.domEvent), this.propagationStopped = !0 }, this.preventDefault = function () { r.preventDefault(this.domEvent), this.defaultPrevented = !0 }, this.stop = function () { this.stopPropagation(), this.preventDefault() }, this.getDocumentPosition = function () { return this.$pos ? this.$pos : (this.$pos = this.editor.renderer.screenToTextCoordinates(this.clientX, this.clientY), this.$pos) }, this.inSelection = function () { if (this.$inSelection !== null) return this.$inSelection; var e = this.editor, t = e.getSelectionRange(); if (t.isEmpty()) this.$inSelection = !1; else { var n = this.getDocumentPosition(); this.$inSelection = t.contains(n.row, n.column) } return this.$inSelection }, this.getButton = function () { return r.getButton(this.domEvent) }, this.getShiftKey = function () { return this.domEvent.shiftKey }, this.getAccelKey = i.isMac ? function () { return this.domEvent.metaKey } : function () { return this.domEvent.ctrlKey } }).call(s.prototype) }), define("ace/mouse/dragdrop_handler", ["require", "exports", "module", "ace/lib/dom", "ace/lib/event", "ace/lib/useragent"], function (e, t, n) { "use strict"; function f(e) { function T(e, n) { var r = Date.now(), i = !n || e.row != n.row, s = !n || e.column != n.column; if (!S || i || s) t.moveCursorToPosition(e), S = r, x = { x: p, y: d }; else { var o = l(x.x, x.y, p, d); o > a ? S = null : r - S >= u && (t.renderer.scrollCursorIntoView(), S = null) } } function N(e, n) { var r = Date.now(), i = t.renderer.layerConfig.lineHeight, s = t.renderer.layerConfig.characterWidth, u = t.renderer.scroller.getBoundingClientRect(), a = { x: { left: p - u.left, right: u.right - p }, y: { top: d - u.top, bottom: u.bottom - d } }, f = Math.min(a.x.left, a.x.right), l = Math.min(a.y.top, a.y.bottom), c = { row: e.row, column: e.column }; f / s <= 2 && (c.column += a.x.left < a.x.right ? -3 : 2), l / i <= 1 && (c.row += a.y.top < a.y.bottom ? -1 : 1); var h = e.row != c.row, v = e.column != c.column, m = !n || e.row != n.row; h || v && !m ? E ? r - E >= o && t.renderer.scrollCursorIntoView(c) : E = r : E = null } function C() { var e = g; g = t.renderer.screenToTextCoordinates(p, d), T(g, e), N(g, e) } function k() { m = t.selection.toOrientedRange(), h = t.session.addMarker(m, "ace_selection", t.getSelectionStyle()), t.clearSelection(), t.isFocused() && t.renderer.$cursorLayer.setBlinking(!1), clearInterval(v), C(), v = setInterval(C, 20), y = 0, i.addListener(document, "mousemove", O) } function L() { clearInterval(v), t.session.removeMarker(h), h = null, t.selection.fromOrientedRange(m), t.isFocused() && !w && t.$resetCursorStyle(), m = null, g = null, y = 0, E = null, S = null, i.removeListener(document, "mousemove", O) } function O() { A == null && (A = setTimeout(function () { A != null && h && L() }, 20)) } function M(e) { var t = e.types; return !t || Array.prototype.some.call(t, function (e) { return e == "text/plain" || e == "Text" }) } function _(e) { var t = ["copy", "copymove", "all", "uninitialized"], n = ["move", "copymove", "linkmove", "all", "uninitialized"], r = s.isMac ? e.altKey : e.ctrlKey, i = "uninitialized"; try { i = e.dataTransfer.effectAllowed.toLowerCase() } catch (e) { } var o = "none"; return r && t.indexOf(i) >= 0 ? o = "copy" : n.indexOf(i) >= 0 ? o = "move" : t.indexOf(i) >= 0 && (o = "copy"), o } var t = e.editor, n = r.createElement("img"); n.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", s.isOpera && (n.style.cssText = "width:1px;height:1px;position:fixed;top:0;left:0;z-index:2147483647;opacity:0;"); var f = ["dragWait", "dragWaitEnd", "startDrag", "dragReadyEnd", "onMouseDrag"]; f.forEach(function (t) { e[t] = this[t] }, this), t.on("mousedown", this.onMouseDown.bind(e)); var c = t.container, h, p, d, v, m, g, y = 0, b, w, E, S, x; this.onDragStart = function (e) { if (this.cancelDrag || !c.draggable) { var r = this; return setTimeout(function () { r.startSelect(), r.captureMouse(e) }, 0), e.preventDefault() } m = t.getSelectionRange(); var i = e.dataTransfer; i.effectAllowed = t.getReadOnly() ? "copy" : "copyMove", s.isOpera && (t.container.appendChild(n), n.scrollTop = 0), i.setDragImage && i.setDragImage(n, 0, 0), s.isOpera && t.container.removeChild(n), i.clearData(), i.setData("Text", t.session.getTextRange()), w = !0, this.setState("drag") }, this.onDragEnd = function (e) { c.draggable = !1, w = !1, this.setState(null); if (!t.getReadOnly()) { var n = e.dataTransfer.dropEffect; !b && n == "move" && t.session.remove(t.getSelectionRange()), t.$resetCursorStyle() } this.editor.unsetStyle("ace_dragging"), this.editor.renderer.setCursorStyle("") }, this.onDragEnter = function (e) { if (t.getReadOnly() || !M(e.dataTransfer)) return; return p = e.clientX, d = e.clientY, h || k(), y++, e.dataTransfer.dropEffect = b = _(e), i.preventDefault(e) }, this.onDragOver = function (e) { if (t.getReadOnly() || !M(e.dataTransfer)) return; return p = e.clientX, d = e.clientY, h || (k(), y++), A !== null && (A = null), e.dataTransfer.dropEffect = b = _(e), i.preventDefault(e) }, this.onDragLeave = function (e) { y--; if (y <= 0 && h) return L(), b = null, i.preventDefault(e) }, this.onDrop = function (e) { if (!g) return; var n = e.dataTransfer; if (w) switch (b) { case "move": m.contains(g.row, g.column) ? m = { start: g, end: g } : m = t.moveText(m, g); break; case "copy": m = t.moveText(m, g, !0) } else { var r = n.getData("Text"); m = { start: g, end: t.session.insert(g, r) }, t.focus(), b = null } return L(), i.preventDefault(e) }, i.addListener(c, "dragstart", this.onDragStart.bind(e), t), i.addListener(c, "dragend", this.onDragEnd.bind(e), t), i.addListener(c, "dragenter", this.onDragEnter.bind(e), t), i.addListener(c, "dragover", this.onDragOver.bind(e), t), i.addListener(c, "dragleave", this.onDragLeave.bind(e), t), i.addListener(c, "drop", this.onDrop.bind(e), t); var A = null } function l(e, t, n, r) { return Math.sqrt(Math.pow(n - e, 2) + Math.pow(r - t, 2)) } var r = e("../lib/dom"), i = e("../lib/event"), s = e("../lib/useragent"), o = 200, u = 200, a = 5; (function () { this.dragWait = function () { var e = Date.now() - this.mousedownEvent.time; e > this.editor.getDragDelay() && this.startDrag() }, this.dragWaitEnd = function () { var e = this.editor.container; e.draggable = !1, this.startSelect(this.mousedownEvent.getDocumentPosition()), this.selectEnd() }, this.dragReadyEnd = function (e) { this.editor.$resetCursorStyle(), this.editor.unsetStyle("ace_dragging"), this.editor.renderer.setCursorStyle(""), this.dragWaitEnd() }, this.startDrag = function () { this.cancelDrag = !1; var e = this.editor, t = e.container; t.draggable = !0, e.renderer.$cursorLayer.setBlinking(!1), e.setStyle("ace_dragging"); var n = s.isWin ? "default" : "move"; e.renderer.setCursorStyle(n), this.setState("dragReady") }, this.onMouseDrag = function (e) { var t = this.editor.container; if (s.isIE && this.state == "dragReady") { var n = l(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y); n > 3 && t.dragDrop() } if (this.state === "dragWait") { var n = l(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y); n > 0 && (t.draggable = !1, this.startSelect(this.mousedownEvent.getDocumentPosition())) } }, this.onMouseDown = function (e) { if (!this.$dragEnabled) return; this.mousedownEvent = e; var t = this.editor, n = e.inSelection(), r = e.getButton(), i = e.domEvent.detail || 1; if (i === 1 && r === 0 && n) { if (e.editor.inMultiSelectMode && (e.getAccelKey() || e.getShiftKey())) return; this.mousedownEvent.time = Date.now(); var o = e.domEvent.target || e.domEvent.srcElement; "unselectable" in o && (o.unselectable = "on"); if (t.getDragDelay()) { if (s.isWebKit) { this.cancelDrag = !0; var u = t.container; u.draggable = !0 } this.setState("dragWait") } else this.startDrag(); this.captureMouse(e, this.onMouseDrag.bind(this)), e.defaultPrevented = !0 } } }).call(f.prototype), t.DragdropHandler = f }), define("ace/mouse/touch_handler", ["require", "exports", "module", "ace/mouse/mouse_event", "ace/lib/event", "ace/lib/dom"], function (e, t, n) { "use strict"; var r = e("./mouse_event").MouseEvent, i = e("../lib/event"), s = e("../lib/dom"); t.addTouchListeners = function (e, t) { function b() { var e = window.navigator && window.navigator.clipboard, r = !1, i = function () { var n = t.getCopyText(), i = t.session.getUndoManager().hasUndo(); y.replaceChild(s.buildDom(r ? ["span", !n && ["span", { "class": "ace_mobile-button", action: "selectall" }, "Select All"], n && ["span", { "class": "ace_mobile-button", action: "copy" }, "Copy"], n && ["span", { "class": "ace_mobile-button", action: "cut" }, "Cut"], e && ["span", { "class": "ace_mobile-button", action: "paste" }, "Paste"], i && ["span", { "class": "ace_mobile-button", action: "undo" }, "Undo"], ["span", { "class": "ace_mobile-button", action: "find" }, "Find"], ["span", { "class": "ace_mobile-button", action: "openCommandPallete" }, "Pallete"]] : ["span"]), y.firstChild) }, o = function (n) { var s = n.target.getAttribute("action"); if (s == "more" || !r) return r = !r, i(); if (s == "paste") e.readText().then(function (e) { t.execCommand(s, e) }); else if (s) { if (s == "cut" || s == "copy") e ? e.writeText(t.getCopyText()) : document.execCommand("copy"); t.execCommand(s) } y.firstChild.style.display = "none", r = !1, s != "openCommandPallete" && t.focus() }; y = s.buildDom(["div", { "class": "ace_mobile-menu", ontouchstart: function (e) { n = "menu", e.stopPropagation(), e.preventDefault(), t.textInput.focus() }, ontouchend: function (e) { e.stopPropagation(), e.preventDefault(), o(e) }, onclick: o }, ["span"], ["span", { "class": "ace_mobile-button", action: "more" }, "..."]], t.container) } function w() { y || b(); var e = t.selection.cursor, n = t.renderer.textToScreenCoordinates(e.row, e.column), r = t.container.getBoundingClientRect(); y.style.top = n.pageY - r.top - 3 + "px", y.style.right = "10px", y.style.display = "", y.firstChild.style.display = "none", t.on("input", E) } function E(e) { y && (y.style.display = "none"), t.off("input", E) } function S() { l = null, clearTimeout(l); var e = t.selection.getRange(), r = e.contains(p.row, p.column); if (e.isEmpty() || !r) t.selection.moveToPosition(p), t.selection.selectWord(); n = "wait", w() } function x() { l = null, clearTimeout(l), t.selection.moveToPosition(p); var e = d >= 2 ? t.selection.getLineRange(p.row) : t.session.getBracketRange(p); e && !e.isEmpty() ? t.selection.setRange(e) : t.selection.selectWord(), n = "wait" } function T() { h += 60, c = setInterval(function () { h-- <= 0 && (clearInterval(c), c = null), Math.abs(v) < .01 && (v = 0), Math.abs(m) < .01 && (m = 0), h < 20 && (v = .9 * v), h < 20 && (m = .9 * m); var e = t.session.getScrollTop(); t.renderer.scrollBy(10 * v, 10 * m), e == t.session.getScrollTop() && (h = 0) }, 10) } var n = "scroll", o, u, a, f, l, c, h = 0, p, d = 0, v = 0, m = 0, g, y; i.addListener(e, "contextmenu", function (e) { if (!g) return; var n = t.textInput.getElement(); n.focus() }, t), i.addListener(e, "touchstart", function (e) { var i = e.touches; if (l || i.length > 1) { clearTimeout(l), l = null, a = -1, n = "zoom"; return } g = t.$mouseHandler.isMousePressed = !0; var s = t.renderer.layerConfig.lineHeight, c = t.renderer.layerConfig.lineHeight, y = e.timeStamp; f = y; var b = i[0], w = b.clientX, E = b.clientY; Math.abs(o - w) + Math.abs(u - E) > s && (a = -1), o = e.clientX = w, u = e.clientY = E, v = m = 0; var T = new r(e, t); p = T.getDocumentPosition(); if (y - a < 500 && i.length == 1 && !h) d++, e.preventDefault(), e.button = 0, x(); else { d = 0; var N = t.selection.cursor, C = t.selection.isEmpty() ? N : t.selection.anchor, k = t.renderer.$cursorLayer.getPixelPosition(N, !0), L = t.renderer.$cursorLayer.getPixelPosition(C, !0), A = t.renderer.scroller.getBoundingClientRect(), O = function (e, t) { return e /= c, t = t / s - .75, e * e + t * t }; if (e.clientX < A.left) { n = "zoom"; return } var M = O(e.clientX - A.left - k.left, e.clientY - A.top - k.top), _ = O(e.clientX - A.left - L.left, e.clientY - A.top - L.top); M < 3.5 && _ < 3.5 && (n = M > _ ? "cursor" : "anchor"), _ < 3.5 ? n = "anchor" : M < 3.5 ? n = "cursor" : n = "scroll", l = setTimeout(S, 450) } a = y }, t), i.addListener(e, "touchend", function (e) { g = t.$mouseHandler.isMousePressed = !1, c && clearInterval(c), n == "zoom" ? (n = "", h = 0) : l ? (t.selection.moveToPosition(p), h = 0, w()) : n == "scroll" ? (T(), E()) : w(), clearTimeout(l), l = null }, t), i.addListener(e, "touchmove", function (e) { l && (clearTimeout(l), l = null); var i = e.touches; if (i.length > 1 || n == "zoom") return; var s = i[0], a = o - s.clientX, c = u - s.clientY; if (n == "wait") { if (!(a * a + c * c > 4)) return e.preventDefault(); n = "cursor" } o = s.clientX, u = s.clientY, e.clientX = s.clientX, e.clientY = s.clientY; var h = e.timeStamp, p = h - f; f = h; if (n == "scroll") { var d = new r(e, t); d.speed = 1, d.wheelX = a, d.wheelY = c, 10 * Math.abs(a) < Math.abs(c) && (a = 0), 10 * Math.abs(c) < Math.abs(a) && (c = 0), p != 0 && (v = a / p, m = c / p), t._emit("mousewheel", d), d.propagationStopped || (v = m = 0) } else { var g = new r(e, t), y = g.getDocumentPosition(); n == "cursor" ? t.selection.moveCursorToPosition(y) : n == "anchor" && t.selection.setSelectionAnchor(y.row, y.column), t.renderer.scrollCursorIntoView(y), e.preventDefault() } }, t) } }), define("ace/lib/net", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { "use strict"; var r = e("./dom"); t.get = function (e, t) { var n = new XMLHttpRequest; n.open("GET", e, !0), n.onreadystatechange = function () { n.readyState === 4 && t(n.responseText) }, n.send(null) }, t.loadScript = function (e, t) { var n = r.getDocumentHead(), i = document.createElement("script"); i.src = e, n.appendChild(i), i.onload = i.onreadystatechange = function (e, n) { if (n || !i.readyState || i.readyState == "loaded" || i.readyState == "complete") i = i.onload = i.onreadystatechange = null, n || t() } }, t.qualifyURL = function (e) { var t = document.createElement("a"); return t.href = e, t.href } }), define("ace/lib/event_emitter", ["require", "exports", "module"], function (e, t, n) { "use strict"; var r = {}, i = function () { this.propagationStopped = !0 }, s = function () { this.defaultPrevented = !0 }; r._emit = r._dispatchEvent = function (e, t) { this._eventRegistry || (this._eventRegistry = {}), this._defaultHandlers || (this._defaultHandlers = {}); var n = this._eventRegistry[e] || [], r = this._defaultHandlers[e]; if (!n.length && !r) return; if (typeof t != "object" || !t) t = {}; t.type || (t.type = e), t.stopPropagation || (t.stopPropagation = i), t.preventDefault || (t.preventDefault = s), n = n.slice(); for (var o = 0; o < n.length; o++) { n[o](t, this); if (t.propagationStopped) break } if (r && !t.defaultPrevented) return r(t, this) }, r._signal = function (e, t) { var n = (this._eventRegistry || {})[e]; if (!n) return; n = n.slice(); for (var r = 0; r < n.length; r++)n[r](t, this) }, r.once = function (e, t) { var n = this; this.on(e, function r() { n.off(e, r), t.apply(null, arguments) }); if (!t) return new Promise(function (e) { t = e }) }, r.setDefaultHandler = function (e, t) { var n = this._defaultHandlers; n || (n = this._defaultHandlers = { _disabled_: {} }); if (n[e]) { var r = n[e], i = n._disabled_[e]; i || (n._disabled_[e] = i = []), i.push(r); var s = i.indexOf(t); s != -1 && i.splice(s, 1) } n[e] = t }, r.removeDefaultHandler = function (e, t) { var n = this._defaultHandlers; if (!n) return; var r = n._disabled_[e]; if (n[e] == t) r && this.setDefaultHandler(e, r.pop()); else if (r) { var i = r.indexOf(t); i != -1 && r.splice(i, 1) } }, r.on = r.addEventListener = function (e, t, n) { this._eventRegistry = this._eventRegistry || {}; var r = this._eventRegistry[e]; return r || (r = this._eventRegistry[e] = []), r.indexOf(t) == -1 && r[n ? "unshift" : "push"](t), t }, r.off = r.removeListener = r.removeEventListener = function (e, t) { this._eventRegistry = this._eventRegistry || {}; var n = this._eventRegistry[e]; if (!n) return; var r = n.indexOf(t); r !== -1 && n.splice(r, 1) }, r.removeAllListeners = function (e) { e || (this._eventRegistry = this._defaultHandlers = undefined), this._eventRegistry && (this._eventRegistry[e] = undefined), this._defaultHandlers && (this._defaultHandlers[e] = undefined) }, t.EventEmitter = r }), define("ace/lib/app_config", ["require", "exports", "module", "ace/lib/oop", "ace/lib/event_emitter"], function (e, t, n) { "no use strict"; function o(e) { typeof console != "undefined" && console.warn && console.warn.apply(console, arguments) } function u(e, t) { var n = new Error(e); n.data = t, typeof console == "object" && console.error && console.error(n), setTimeout(function () { throw n }) } var r = e("./oop"), i = e("./event_emitter").EventEmitter, s = { setOptions: function (e) { Object.keys(e).forEach(function (t) { this.setOption(t, e[t]) }, this) }, getOptions: function (e) { var t = {}; if (!e) { var n = this.$options; e = Object.keys(n).filter(function (e) { return !n[e].hidden }) } else Array.isArray(e) || (t = e, e = Object.keys(t)); return e.forEach(function (e) { t[e] = this.getOption(e) }, this), t }, setOption: function (e, t) { if (this["$" + e] === t) return; var n = this.$options[e]; if (!n) return o('misspelled option "' + e + '"'); if (n.forwardTo) return this[n.forwardTo] && this[n.forwardTo].setOption(e, t); n.handlesSet || (this["$" + e] = t), n && n.set && n.set.call(this, t) }, getOption: function (e) { var t = this.$options[e]; return t ? t.forwardTo ? this[t.forwardTo] && this[t.forwardTo].getOption(e) : t && t.get ? t.get.call(this) : this["$" + e] : o('misspelled option "' + e + '"') } }, a = function () { this.$defaultOptions = {} }; (function () { r.implement(this, i), this.defineOptions = function (e, t, n) { return e.$options || (this.$defaultOptions[t] = e.$options = {}), Object.keys(n).forEach(function (t) { var r = n[t]; typeof r == "string" && (r = { forwardTo: r }), r.name || (r.name = t), e.$options[r.name] = r, "initialValue" in r && (e["$" + r.name] = r.initialValue) }), r.implement(e, s), this }, this.resetOptions = function (e) { Object.keys(e.$options).forEach(function (t) { var n = e.$options[t]; "value" in n && e.setOption(t, n.value) }) }, this.setDefaultValue = function (e, t, n) { if (!e) { for (e in this.$defaultOptions) if (this.$defaultOptions[e][t]) break; if (!this.$defaultOptions[e][t]) return !1 } var r = this.$defaultOptions[e] || (this.$defaultOptions[e] = {}); r[t] && (r.forwardTo ? this.setDefaultValue(r.forwardTo, t, n) : r[t].value = n) }, this.setDefaultValues = function (e, t) { Object.keys(t).forEach(function (n) { this.setDefaultValue(e, n, t[n]) }, this) }, this.warn = o, this.reportError = u }).call(a.prototype), t.AppConfig = a }), define("ace/config", ["require", "exports", "module", "ace/lib/lang", "ace/lib/oop", "ace/lib/net", "ace/lib/app_config"], function (e, t, n) { "no use strict"; function l(r) { if (!u || !u.document) return; a.packaged = r || e.packaged || n.packaged || u.define && define.packaged; var i = {}, s = "", o = document.currentScript || document._currentScript, f = o && o.ownerDocument || document, l = f.getElementsByTagName("script"); for (var h = 0; h < l.length; h++) { var p = l[h], d = p.src || p.getAttribute("src"); if (!d) continue; var v = p.attributes; for (var m = 0, g = v.length; m < g; m++) { var y = v[m]; y.name.indexOf("data-ace-") === 0 && (i[c(y.name.replace(/^data-ace-/, ""))] = y.value) } var b = d.match(/^(.*)\/ace(\-\w+)?\.js(\?|$)/); b && (s = b[1]) } s && (i.base = i.base || s, i.packaged = !0), i.basePath = i.base, i.workerPath = i.workerPath || i.base, i.modePath = i.modePath || i.base, i.themePath = i.themePath || i.base, delete i.base; for (var w in i) typeof i[w] != "undefined" && t.set(w, i[w]) } function c(e) { return e.replace(/-(.)/g, function (e, t) { return t.toUpperCase() }) } var r = e("./lib/lang"), i = e("./lib/oop"), s = e("./lib/net"), o = e("./lib/app_config").AppConfig; n.exports = t = new o; var u = function () { return this || typeof window != "undefined" && window }(), a = { packaged: !1, workerPath: null, modePath: null, themePath: null, basePath: "", suffix: ".js", $moduleUrls: {}, loadWorkerFromBlob: !0, sharedPopups: !1 }; t.get = function (e) { if (!a.hasOwnProperty(e)) throw new Error("Unknown config key: " + e); return a[e] }, t.set = function (e, t) { if (a.hasOwnProperty(e)) a[e] = t; else if (this.setDefaultValue("", e, t) == 0) throw new Error("Unknown config key: " + e) }, t.all = function () { return r.copyObject(a) }, t.$modes = {}, t.moduleUrl = function (e, t) { if (a.$moduleUrls[e]) return a.$moduleUrls[e]; var n = e.split("/"); t = t || n[n.length - 2] || ""; var r = t == "snippets" ? "/" : "-", i = n[n.length - 1]; if (t == "worker" && r == "-") { var s = new RegExp("^" + t + "[\\-_]|[\\-_]" + t + "$", "g"); i = i.replace(s, "") } (!i || i == t) && n.length > 1 && (i = n[n.length - 2]); var o = a[t + "Path"]; return o == null ? o = a.basePath : r == "/" && (t = r = ""), o && o.slice(-1) != "/" && (o += "/"), o + t + r + i + this.get("suffix") }, t.setModuleUrl = function (e, t) { return a.$moduleUrls[e] = t }, t.$loading = {}, t.loadModule = function (n, r) { var i, o; Array.isArray(n) && (o = n[0], n = n[1]); try { i = e(n) } catch (u) { } if (i && !t.$loading[n]) return r && r(i); t.$loading[n] || (t.$loading[n] = []), t.$loading[n].push(r); if (t.$loading[n].length > 1) return; var a = function () { e([n], function (e) { t._emit("load.module", { name: n, module: e }); var r = t.$loading[n]; t.$loading[n] = null, r.forEach(function (t) { t && t(e) }) }) }; if (!t.get("packaged")) return a(); s.loadScript(t.moduleUrl(n, o), a), f() }; var f = function () { !a.basePath && !a.workerPath && !a.modePath && !a.themePath && !Object.keys(a.$moduleUrls).length && (console.error("Unable to infer path to ace from script src,", "use ace.config.set('basePath', 'path') to enable dynamic loading of modes and themes", "or with webpack use ace/webpack-resolver"), f = function () { }) }; t.init = l, t.version = "1.4.10" }), define("ace/mouse/mouse_handler", ["require", "exports", "module", "ace/lib/event", "ace/lib/useragent", "ace/mouse/default_handlers", "ace/mouse/default_gutter_handler", "ace/mouse/mouse_event", "ace/mouse/dragdrop_handler", "ace/mouse/touch_handler", "ace/config"], function (e, t, n) { "use strict"; var r = e("../lib/event"), i = e("../lib/useragent"), s = e("./default_handlers").DefaultHandlers, o = e("./default_gutter_handler").GutterHandler, u = e("./mouse_event").MouseEvent, a = e("./dragdrop_handler").DragdropHandler, f = e("./touch_handler").addTouchListeners, l = e("../config"), c = function (e) { var t = this; this.editor = e, new s(this), new o(this), new a(this); var n = function (t) { var n = !document.hasFocus || !document.hasFocus() || !e.isFocused() && document.activeElement == (e.textInput && e.textInput.getElement()); n && window.focus(), e.focus() }, u = e.renderer.getMouseEventTarget(); r.addListener(u, "click", this.onMouseEvent.bind(this, "click"), e), r.addListener(u, "mousemove", this.onMouseMove.bind(this, "mousemove"), e), r.addMultiMouseDownListener([u, e.renderer.scrollBarV && e.renderer.scrollBarV.inner, e.renderer.scrollBarH && e.renderer.scrollBarH.inner, e.textInput && e.textInput.getElement()].filter(Boolean), [400, 300, 250], this, "onMouseEvent", e), r.addMouseWheelListener(e.container, this.onMouseWheel.bind(this, "mousewheel"), e), f(e.container, e); var l = e.renderer.$gutter; r.addListener(l, "mousedown", this.onMouseEvent.bind(this, "guttermousedown"), e), r.addListener(l, "click", this.onMouseEvent.bind(this, "gutterclick"), e), r.addListener(l, "dblclick", this.onMouseEvent.bind(this, "gutterdblclick"), e), r.addListener(l, "mousemove", this.onMouseEvent.bind(this, "guttermousemove"), e), r.addListener(u, "mousedown", n, e), r.addListener(l, "mousedown", n, e), i.isIE && e.renderer.scrollBarV && (r.addListener(e.renderer.scrollBarV.element, "mousedown", n, e), r.addListener(e.renderer.scrollBarH.element, "mousedown", n, e)), e.on("mousemove", function (n) { if (t.state || t.$dragDelay || !t.$dragEnabled) return; var r = e.renderer.screenToTextCoordinates(n.x, n.y), i = e.session.selection.getRange(), s = e.renderer; !i.isEmpty() && i.insideStart(r.row, r.column) ? s.setCursorStyle("default") : s.setCursorStyle("") }, e) }; (function () { this.onMouseEvent = function (e, t) { this.editor._emit(e, new u(t, this.editor)) }, this.onMouseMove = function (e, t) { var n = this.editor._eventRegistry && this.editor._eventRegistry.mousemove; if (!n || !n.length) return; this.editor._emit(e, new u(t, this.editor)) }, this.onMouseWheel = function (e, t) { var n = new u(t, this.editor); n.speed = this.$scrollSpeed * 2, n.wheelX = t.wheelX, n.wheelY = t.wheelY, this.editor._emit(e, n) }, this.setState = function (e) { this.state = e }, this.captureMouse = function (e, t) { this.x = e.x, this.y = e.y, this.isMousePressed = !0; var n = this.editor, s = this.editor.renderer; s.$isMousePressed = !0; var o = this, a = function (e) { if (!e) return; if (i.isWebKit && !e.which && o.releaseMouse) return o.releaseMouse(); o.x = e.clientX, o.y = e.clientY, t && t(e), o.mouseEvent = new u(e, o.editor), o.$mouseMoved = !0 }, f = function (e) { n.off("beforeEndOperation", c), clearInterval(h), l(), o[o.state + "End"] && o[o.state + "End"](e), o.state = "", o.isMousePressed = s.$isMousePressed = !1, s.$keepTextAreaAtCursor && s.$moveTextAreaToCursor(), o.$onCaptureMouseMove = o.releaseMouse = null, e && o.onMouseEvent("mouseup", e), n.endOperation() }, l = function () { o[o.state] && o[o.state](), o.$mouseMoved = !1 }; if (i.isOldIE && e.domEvent.type == "dblclick") return setTimeout(function () { f(e) }); var c = function (e) { if (!o.releaseMouse) return; n.curOp.command.name && n.curOp.selectionChanged && (o[o.state + "End"] && o[o.state + "End"](), o.state = "", o.releaseMouse()) }; n.on("beforeEndOperation", c), n.startOperation({ command: { name: "mouse" } }), o.$onCaptureMouseMove = a, o.releaseMouse = r.capture(this.editor.container, a, f); var h = setInterval(l, 20) }, this.releaseMouse = null, this.cancelContextMenu = function () { var e = function (t) { if (t && t.domEvent && t.domEvent.type != "contextmenu") return; this.editor.off("nativecontextmenu", e), t && t.domEvent && r.stopEvent(t.domEvent) }.bind(this); setTimeout(e, 10), this.editor.on("nativecontextmenu", e) } }).call(c.prototype), l.defineOptions(c.prototype, "mouseHandler", { scrollSpeed: { initialValue: 2 }, dragDelay: { initialValue: i.isMac ? 150 : 0 }, dragEnabled: { initialValue: !0 }, focusTimeout: { initialValue: 0 }, tooltipFollowsMouse: { initialValue: !0 } }), t.MouseHandler = c }), define("ace/mouse/fold_handler", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { "use strict"; function i(e) { e.on("click", function (t) { var n = t.getDocumentPosition(), i = e.session, s = i.getFoldAt(n.row, n.column, 1); s && (t.getAccelKey() ? i.removeFold(s) : i.expandFold(s), t.stop()); var o = t.domEvent && t.domEvent.target; o && r.hasCssClass(o, "ace_inline_button") && r.hasCssClass(o, "ace_toggle_wrap") && (i.setOption("wrap", !i.getUseWrapMode()), e.renderer.scrollCursorIntoView()) }), e.on("gutterclick", function (t) { var n = e.renderer.$gutterLayer.getRegion(t); if (n == "foldWidgets") { var r = t.getDocumentPosition().row, i = e.session; i.foldWidgets && i.foldWidgets[r] && e.session.onFoldWidgetClick(r, t), e.isFocused() || e.focus(), t.stop() } }), e.on("gutterdblclick", function (t) { var n = e.renderer.$gutterLayer.getRegion(t); if (n == "foldWidgets") { var r = t.getDocumentPosition().row, i = e.session, s = i.getParentFoldRangeData(r, !0), o = s.range || s.firstRange; if (o) { r = o.start.row; var u = i.getFoldAt(r, i.getLine(r).length, 1); u ? i.removeFold(u) : (i.addFold("...", o), e.renderer.scrollCursorIntoView({ row: o.start.row, column: 0 })) } t.stop() } }) } var r = e("../lib/dom"); t.FoldHandler = i }), define("ace/keyboard/keybinding", ["require", "exports", "module", "ace/lib/keys", "ace/lib/event"], function (e, t, n) { "use strict"; var r = e("../lib/keys"), i = e("../lib/event"), s = function (e) { this.$editor = e, this.$data = { editor: e }, this.$handlers = [], this.setDefaultHandler(e.commands) }; (function () { this.setDefaultHandler = function (e) { this.removeKeyboardHandler(this.$defaultHandler), this.$defaultHandler = e, this.addKeyboardHandler(e, 0) }, this.setKeyboardHandler = function (e) { var t = this.$handlers; if (t[t.length - 1] == e) return; while (t[t.length - 1] && t[t.length - 1] != this.$defaultHandler) this.removeKeyboardHandler(t[t.length - 1]); this.addKeyboardHandler(e, 1) }, this.addKeyboardHandler = function (e, t) { if (!e) return; typeof e == "function" && !e.handleKeyboard && (e.handleKeyboard = e); var n = this.$handlers.indexOf(e); n != -1 && this.$handlers.splice(n, 1), t == undefined ? this.$handlers.push(e) : this.$handlers.splice(t, 0, e), n == -1 && e.attach && e.attach(this.$editor) }, this.removeKeyboardHandler = function (e) { var t = this.$handlers.indexOf(e); return t == -1 ? !1 : (this.$handlers.splice(t, 1), e.detach && e.detach(this.$editor), !0) }, this.getKeyboardHandler = function () { return this.$handlers[this.$handlers.length - 1] }, this.getStatusText = function () { var e = this.$data, t = e.editor; return this.$handlers.map(function (n) { return n.getStatusText && n.getStatusText(t, e) || "" }).filter(Boolean).join(" ") }, this.$callKeyboardHandlers = function (e, t, n, r) { var s, o = !1, u = this.$editor.commands; for (var a = this.$handlers.length; a--;) { s = this.$handlers[a].handleKeyboard(this.$data, e, t, n, r); if (!s || !s.command) continue; s.command == "null" ? o = !0 : o = u.exec(s.command, this.$editor, s.args, r), o && r && e != -1 && s.passEvent != 1 && s.command.passEvent != 1 && i.stopEvent(r); if (o) break } return !o && e == -1 && (s = { command: "insertstring" }, o = u.exec("insertstring", this.$editor, t)), o && this.$editor._signal && this.$editor._signal("keyboardActivity", s), o }, this.onCommandKey = function (e, t, n) { var i = r.keyCodeToString(n); return this.$callKeyboardHandlers(t, i, n, e) }, this.onTextInput = function (e) { return this.$callKeyboardHandlers(-1, e) } }).call(s.prototype), t.KeyBinding = s }), define("ace/lib/bidiutil", ["require", "exports", "module"], function (e, t, n) { "use strict"; function F(e, t, n, r) { var i = s ? d : p, c = null, h = null, v = null, m = 0, g = null, y = null, b = -1, w = null, E = null, T = []; if (!r) for (w = 0, r = []; w < n; w++)r[w] = R(e[w]); o = s, u = !1, a = !1, f = !1, l = !1; for (E = 0; E < n; E++) { c = m, T[E] = h = q(e, r, T, E), m = i[c][h], g = m & 240, m &= 15, t[E] = v = i[m][5]; if (g > 0) if (g == 16) { for (w = b; w < E; w++)t[w] = 1; b = -1 } else b = -1; y = i[m][6]; if (y) b == -1 && (b = E); else if (b > -1) { for (w = b; w < E; w++)t[w] = v; b = -1 } r[E] == S && (t[E] = 0), o |= v } if (l) for (w = 0; w < n; w++)if (r[w] == x) { t[w] = s; for (var C = w - 1; C >= 0; C--) { if (r[C] != N) break; t[C] = s } } } function I(e, t, n) { if (o < e) return; if (e == 1 && s == m && !f) { n.reverse(); return } var r = n.length, i = 0, u, a, l, c; while (i < r) { if (t[i] >= e) { u = i + 1; while (u < r && t[u] >= e) u++; for (a = i, l = u - 1; a < l; a++, l--)c = n[a], n[a] = n[l], n[l] = c; i = u } i++ } } function q(e, t, n, r) { var i = t[r], o, c, h, p; switch (i) { case g: case y: u = !1; case E: case w: return i; case b: return u ? w : b; case T: return u = !0, a = !0, y; case N: return E; case C: if (r < 1 || r + 1 >= t.length || (o = n[r - 1]) != b && o != w || (c = t[r + 1]) != b && c != w) return E; return u && (c = w), c == o ? c : E; case k: o = r > 0 ? n[r - 1] : S; if (o == b && r + 1 < t.length && t[r + 1] == b) return b; return E; case L: if (r > 0 && n[r - 1] == b) return b; if (u) return E; p = r + 1, h = t.length; while (p < h && t[p] == L) p++; if (p < h && t[p] == b) return b; return E; case A: h = t.length, p = r + 1; while (p < h && t[p] == A) p++; if (p < h) { var d = e[r], v = d >= 1425 && d <= 2303 || d == 64286; o = t[p]; if (v && (o == y || o == T)) return y } if (r < 1 || (o = t[r - 1]) == S) return E; return n[r - 1]; case S: return u = !1, f = !0, s; case x: return l = !0, E; case O: case M: case D: case P: case _: u = !1; case H: return E } } function R(e) { var t = e.charCodeAt(0), n = t >> 8; return n == 0 ? t > 191 ? g : B[t] : n == 5 ? /[\u0591-\u05f4]/.test(e) ? y : g : n == 6 ? /[\u0610-\u061a\u064b-\u065f\u06d6-\u06e4\u06e7-\u06ed]/.test(e) ? A : /[\u0660-\u0669\u066b-\u066c]/.test(e) ? w : t == 1642 ? L : /[\u06f0-\u06f9]/.test(e) ? b : T : n == 32 && t <= 8287 ? j[t & 255] : n == 254 ? t >= 65136 ? T : E : E } function U(e) { return e >= "\u064b" && e <= "\u0655" } var r = ["\u0621", "\u0641"], i = ["\u063a", "\u064a"], s = 0, o = 0, u = !1, a = !1, f = !1, l = !1, c = !1, h = !1, p = [[0, 3, 0, 1, 0, 0, 0], [0, 3, 0, 1, 2, 2, 0], [0, 3, 0, 17, 2, 0, 1], [0, 3, 5, 5, 4, 1, 0], [0, 3, 21, 21, 4, 0, 1], [0, 3, 5, 5, 4, 2, 0]], d = [[2, 0, 1, 1, 0, 1, 0], [2, 0, 1, 1, 0, 2, 0], [2, 0, 2, 1, 3, 2, 0], [2, 0, 2, 33, 3, 1, 1]], v = 0, m = 1, g = 0, y = 1, b = 2, w = 3, E = 4, S = 5, x = 6, T = 7, N = 8, C = 9, k = 10, L = 11, A = 12, O = 13, M = 14, _ = 15, D = 16, P = 17, H = 18, B = [H, H, H, H, H, H, H, H, H, x, S, x, N, S, H, H, H, H, H, H, H, H, H, H, H, H, H, H, S, S, S, x, N, E, E, L, L, L, E, E, E, E, E, k, C, k, C, C, b, b, b, b, b, b, b, b, b, b, C, E, E, E, E, E, E, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, E, E, E, E, E, E, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, g, E, E, E, E, H, H, H, H, H, H, S, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, H, C, E, L, L, L, L, E, E, E, E, g, E, E, H, E, E, L, L, b, b, E, g, E, E, E, b, g, E, E, E, E, E], j = [N, N, N, N, N, N, N, N, N, N, N, H, H, H, g, y, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, N, S, O, M, _, D, P, C, L, L, L, L, L, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, C, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, N]; t.L = g, t.R = y, t.EN = b, t.ON_R = 3, t.AN = 4, t.R_H = 5, t.B = 6, t.RLE = 7, t.DOT = "\u00b7", t.doBidiReorder = function (e, n, r) { if (e.length < 2) return {}; var i = e.split(""), o = new Array(i.length), u = new Array(i.length), a = []; s = r ? m : v, F(i, a, i.length, n); for (var f = 0; f < o.length; o[f] = f, f++); I(2, a, o), I(1, a, o); for (var f = 0; f < o.length - 1; f++)n[f] === w ? a[f] = t.AN : a[f] === y && (n[f] > T && n[f] < O || n[f] === E || n[f] === H) ? a[f] = t.ON_R : f > 0 && i[f - 1] === "\u0644" && /\u0622|\u0623|\u0625|\u0627/.test(i[f]) && (a[f - 1] = a[f] = t.R_H, f++); i[i.length - 1] === t.DOT && (a[i.length - 1] = t.B), i[0] === "\u202b" && (a[0] = t.RLE); for (var f = 0; f < o.length; f++)u[f] = a[o[f]]; return { logicalFromVisual: o, bidiLevels: u } }, t.hasBidiCharacters = function (e, t) { var n = !1; for (var r = 0; r < e.length; r++)t[r] = R(e.charAt(r)), !n && (t[r] == y || t[r] == T || t[r] == w) && (n = !0); return n }, t.getVisualFromLogicalIdx = function (e, t) { for (var n = 0; n < t.logicalFromVisual.length; n++)if (t.logicalFromVisual[n] == e) return n; return 0 } }), define("ace/bidihandler", ["require", "exports", "module", "ace/lib/bidiutil", "ace/lib/lang"], function (e, t, n) { "use strict"; var r = e("./lib/bidiutil"), i = e("./lib/lang"), s = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\u202B]/, o = function (e) { this.session = e, this.bidiMap = {}, this.currentRow = null, this.bidiUtil = r, this.charWidths = [], this.EOL = "\u00ac", this.showInvisibles = !0, this.isRtlDir = !1, this.$isRtl = !1, this.line = "", this.wrapIndent = 0, this.EOF = "\u00b6", this.RLE = "\u202b", this.contentWidth = 0, this.fontMetrics = null, this.rtlLineOffset = 0, this.wrapOffset = 0, this.isMoveLeftOperation = !1, this.seenBidi = s.test(e.getValue()) }; (function () { this.isBidiRow = function (e, t, n) { return this.seenBidi ? (e !== this.currentRow && (this.currentRow = e, this.updateRowLine(t, n), this.updateBidiMap()), this.bidiMap.bidiLevels) : !1 }, this.onChange = function (e) { this.seenBidi ? this.currentRow = null : e.action == "insert" && s.test(e.lines.join("\n")) && (this.seenBidi = !0, this.currentRow = null) }, this.getDocumentRow = function () { var e = 0, t = this.session.$screenRowCache; if (t.length) { var n = this.session.$getRowCacheIndex(t, this.currentRow); n >= 0 && (e = this.session.$docRowCache[n]) } return e }, this.getSplitIndex = function () { var e = 0, t = this.session.$screenRowCache; if (t.length) { var n, r = this.session.$getRowCacheIndex(t, this.currentRow); while (this.currentRow - e > 0) { n = this.session.$getRowCacheIndex(t, this.currentRow - e - 1); if (n !== r) break; r = n, e++ } } else e = this.currentRow; return e }, this.updateRowLine = function (e, t) { e === undefined && (e = this.getDocumentRow()); var n = e === this.session.getLength() - 1, s = n ? this.EOF : this.EOL; this.wrapIndent = 0, this.line = this.session.getLine(e), this.isRtlDir = this.$isRtl || this.line.charAt(0) === this.RLE; if (this.session.$useWrapMode) { var o = this.session.$wrapData[e]; o && (t === undefined && (t = this.getSplitIndex()), t > 0 && o.length ? (this.wrapIndent = o.indent, this.wrapOffset = this.wrapIndent * this.charWidths[r.L], this.line = t < o.length ? this.line.substring(o[t - 1], o[t]) : this.line.substring(o[o.length - 1])) : this.line = this.line.substring(0, o[t])), t == o.length && (this.line += this.showInvisibles ? s : r.DOT) } else this.line += this.showInvisibles ? s : r.DOT; var u = this.session, a = 0, f; this.line = this.line.replace(/\t|[\u1100-\u2029, \u202F-\uFFE6]/g, function (e, t) { return e === " " || u.isFullWidth(e.charCodeAt(0)) ? (f = e === " " ? u.getScreenTabSize(t + a) : 2, a += f - 1, i.stringRepeat(r.DOT, f)) : e }), this.isRtlDir && (this.fontMetrics.$main.textContent = this.line.charAt(this.line.length - 1) == r.DOT ? this.line.substr(0, this.line.length - 1) : this.line, this.rtlLineOffset = this.contentWidth - this.fontMetrics.$main.getBoundingClientRect().width) }, this.updateBidiMap = function () { var e = []; r.hasBidiCharacters(this.line, e) || this.isRtlDir ? this.bidiMap = r.doBidiReorder(this.line, e, this.isRtlDir) : this.bidiMap = {} }, this.markAsDirty = function () { this.currentRow = null }, this.updateCharacterWidths = function (e) { if (this.characterWidth === e.$characterSize.width) return; this.fontMetrics = e; var t = this.characterWidth = e.$characterSize.width, n = e.$measureCharWidth("\u05d4"); this.charWidths[r.L] = this.charWidths[r.EN] = this.charWidths[r.ON_R] = t, this.charWidths[r.R] = this.charWidths[r.AN] = n, this.charWidths[r.R_H] = n * .45, this.charWidths[r.B] = this.charWidths[r.RLE] = 0, this.currentRow = null }, this.setShowInvisibles = function (e) { this.showInvisibles = e, this.currentRow = null }, this.setEolChar = function (e) { this.EOL = e }, this.setContentWidth = function (e) { this.contentWidth = e }, this.isRtlLine = function (e) { return this.$isRtl ? !0 : e != undefined ? this.session.getLine(e).charAt(0) == this.RLE : this.isRtlDir }, this.setRtlDirection = function (e, t) { var n = e.getCursorPosition(); for (var r = e.selection.getSelectionAnchor().row; r <= n.row; r++)!t && e.session.getLine(r).charAt(0) === e.session.$bidiHandler.RLE ? e.session.doc.removeInLine(r, 0, 1) : t && e.session.getLine(r).charAt(0) !== e.session.$bidiHandler.RLE && e.session.doc.insert({ column: 0, row: r }, e.session.$bidiHandler.RLE) }, this.getPosLeft = function (e) { e -= this.wrapIndent; var t = this.line.charAt(0) === this.RLE ? 1 : 0, n = e > t ? this.session.getOverwrite() ? e : e - 1 : t, i = r.getVisualFromLogicalIdx(n, this.bidiMap), s = this.bidiMap.bidiLevels, o = 0; !this.session.getOverwrite() && e <= t && s[i] % 2 !== 0 && i++; for (var u = 0; u < i; u++)o += this.charWidths[s[u]]; return !this.session.getOverwrite() && e > t && s[i] % 2 === 0 && (o += this.charWidths[s[i]]), this.wrapIndent && (o += this.isRtlDir ? -1 * this.wrapOffset : this.wrapOffset), this.isRtlDir && (o += this.rtlLineOffset), o }, this.getSelections = function (e, t) { var n = this.bidiMap, r = n.bidiLevels, i, s = [], o = 0, u = Math.min(e, t) - this.wrapIndent, a = Math.max(e, t) - this.wrapIndent, f = !1, l = !1, c = 0; this.wrapIndent && (o += this.isRtlDir ? -1 * this.wrapOffset : this.wrapOffset); for (var h, p = 0; p < r.length; p++)h = n.logicalFromVisual[p], i = r[p], f = h >= u && h < a, f && !l ? c = o : !f && l && s.push({ left: c, width: o - c }), o += this.charWidths[i], l = f; f && p === r.length && s.push({ left: c, width: o - c }); if (this.isRtlDir) for (var d = 0; d < s.length; d++)s[d].left += this.rtlLineOffset; return s }, this.offsetToCol = function (e) { this.isRtlDir && (e -= this.rtlLineOffset); var t = 0, e = Math.max(e, 0), n = 0, r = 0, i = this.bidiMap.bidiLevels, s = this.charWidths[i[r]]; this.wrapIndent && (e -= this.isRtlDir ? -1 * this.wrapOffset : this.wrapOffset); while (e > n + s / 2) { n += s; if (r === i.length - 1) { s = 0; break } s = this.charWidths[i[++r]] } return r > 0 && i[r - 1] % 2 !== 0 && i[r] % 2 === 0 ? (e < n && r--, t = this.bidiMap.logicalFromVisual[r]) : r > 0 && i[r - 1] % 2 === 0 && i[r] % 2 !== 0 ? t = 1 + (e > n ? this.bidiMap.logicalFromVisual[r] : this.bidiMap.logicalFromVisual[r - 1]) : this.isRtlDir && r === i.length - 1 && s === 0 && i[r - 1] % 2 === 0 || !this.isRtlDir && r === 0 && i[r] % 2 !== 0 ? t = 1 + this.bidiMap.logicalFromVisual[r] : (r > 0 && i[r - 1] % 2 !== 0 && s !== 0 && r--, t = this.bidiMap.logicalFromVisual[r]), t === 0 && this.isRtlDir && t++, t + this.wrapIndent } }).call(o.prototype), t.BidiHandler = o }), define("ace/selection", ["require", "exports", "module", "ace/lib/oop", "ace/lib/lang", "ace/lib/event_emitter", "ace/range"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./lib/lang"), s = e("./lib/event_emitter").EventEmitter, o = e("./range").Range, u = function (e) { this.session = e, this.doc = e.getDocument(), this.clearSelection(), this.cursor = this.lead = this.doc.createAnchor(0, 0), this.anchor = this.doc.createAnchor(0, 0), this.$silent = !1; var t = this; this.cursor.on("change", function (e) { t.$cursorChanged = !0, t.$silent || t._emit("changeCursor"), !t.$isEmpty && !t.$silent && t._emit("changeSelection"), !t.$keepDesiredColumnOnChange && e.old.column != e.value.column && (t.$desiredColumn = null) }), this.anchor.on("change", function () { t.$anchorChanged = !0, !t.$isEmpty && !t.$silent && t._emit("changeSelection") }) }; (function () { r.implement(this, s), this.isEmpty = function () { return this.$isEmpty || this.anchor.row == this.lead.row && this.anchor.column == this.lead.column }, this.isMultiLine = function () { return !this.$isEmpty && this.anchor.row != this.cursor.row }, this.getCursor = function () { return this.lead.getPosition() }, this.setSelectionAnchor = function (e, t) { this.$isEmpty = !1, this.anchor.setPosition(e, t) }, this.getAnchor = this.getSelectionAnchor = function () { return this.$isEmpty ? this.getSelectionLead() : this.anchor.getPosition() }, this.getSelectionLead = function () { return this.lead.getPosition() }, this.isBackwards = function () { var e = this.anchor, t = this.lead; return e.row > t.row || e.row == t.row && e.column > t.column }, this.getRange = function () { var e = this.anchor, t = this.lead; return this.$isEmpty ? o.fromPoints(t, t) : this.isBackwards() ? o.fromPoints(t, e) : o.fromPoints(e, t) }, this.clearSelection = function () { this.$isEmpty || (this.$isEmpty = !0, this._emit("changeSelection")) }, this.selectAll = function () { this.$setSelection(0, 0, Number.MAX_VALUE, Number.MAX_VALUE) }, this.setRange = this.setSelectionRange = function (e, t) { var n = t ? e.end : e.start, r = t ? e.start : e.end; this.$setSelection(n.row, n.column, r.row, r.column) }, this.$setSelection = function (e, t, n, r) { if (this.$silent) return; var i = this.$isEmpty, s = this.inMultiSelectMode; this.$silent = !0, this.$cursorChanged = this.$anchorChanged = !1, this.anchor.setPosition(e, t), this.cursor.setPosition(n, r), this.$isEmpty = !o.comparePoints(this.anchor, this.cursor), this.$silent = !1, this.$cursorChanged && this._emit("changeCursor"), (this.$cursorChanged || this.$anchorChanged || i != this.$isEmpty || s) && this._emit("changeSelection") }, this.$moveSelection = function (e) { var t = this.lead; this.$isEmpty && this.setSelectionAnchor(t.row, t.column), e.call(this) }, this.selectTo = function (e, t) { this.$moveSelection(function () { this.moveCursorTo(e, t) }) }, this.selectToPosition = function (e) { this.$moveSelection(function () { this.moveCursorToPosition(e) }) }, this.moveTo = function (e, t) { this.clearSelection(), this.moveCursorTo(e, t) }, this.moveToPosition = function (e) { this.clearSelection(), this.moveCursorToPosition(e) }, this.selectUp = function () { this.$moveSelection(this.moveCursorUp) }, this.selectDown = function () { this.$moveSelection(this.moveCursorDown) }, this.selectRight = function () { this.$moveSelection(this.moveCursorRight) }, this.selectLeft = function () { this.$moveSelection(this.moveCursorLeft) }, this.selectLineStart = function () { this.$moveSelection(this.moveCursorLineStart) }, this.selectLineEnd = function () { this.$moveSelection(this.moveCursorLineEnd) }, this.selectFileEnd = function () { this.$moveSelection(this.moveCursorFileEnd) }, this.selectFileStart = function () { this.$moveSelection(this.moveCursorFileStart) }, this.selectWordRight = function () { this.$moveSelection(this.moveCursorWordRight) }, this.selectWordLeft = function () { this.$moveSelection(this.moveCursorWordLeft) }, this.getWordRange = function (e, t) { if (typeof t == "undefined") { var n = e || this.lead; e = n.row, t = n.column } return this.session.getWordRange(e, t) }, this.selectWord = function () { this.setSelectionRange(this.getWordRange()) }, this.selectAWord = function () { var e = this.getCursor(), t = this.session.getAWordRange(e.row, e.column); this.setSelectionRange(t) }, this.getLineRange = function (e, t) { var n = typeof e == "number" ? e : this.lead.row, r, i = this.session.getFoldLine(n); return i ? (n = i.start.row, r = i.end.row) : r = n, t === !0 ? new o(n, 0, r, this.session.getLine(r).length) : new o(n, 0, r + 1, 0) }, this.selectLine = function () { this.setSelectionRange(this.getLineRange()) }, this.moveCursorUp = function () { this.moveCursorBy(-1, 0) }, this.moveCursorDown = function () { this.moveCursorBy(1, 0) }, this.wouldMoveIntoSoftTab = function (e, t, n) { var r = e.column, i = e.column + t; return n < 0 && (r = e.column - t, i = e.column), this.session.isTabStop(e) && this.doc.getLine(e.row).slice(r, i).split(" ").length - 1 == t }, this.moveCursorLeft = function () { var e = this.lead.getPosition(), t; if (t = this.session.getFoldAt(e.row, e.column, -1)) this.moveCursorTo(t.start.row, t.start.column); else if (e.column === 0) e.row > 0 && this.moveCursorTo(e.row - 1, this.doc.getLine(e.row - 1).length); else { var n = this.session.getTabSize(); this.wouldMoveIntoSoftTab(e, n, -1) && !this.session.getNavigateWithinSoftTabs() ? this.moveCursorBy(0, -n) : this.moveCursorBy(0, -1) } }, this.moveCursorRight = function () { var e = this.lead.getPosition(), t; if (t = this.session.getFoldAt(e.row, e.column, 1)) this.moveCursorTo(t.end.row, t.end.column); else if (this.lead.column == this.doc.getLine(this.lead.row).length) this.lead.row < this.doc.getLength() - 1 && this.moveCursorTo(this.lead.row + 1, 0); else { var n = this.session.getTabSize(), e = this.lead; this.wouldMoveIntoSoftTab(e, n, 1) && !this.session.getNavigateWithinSoftTabs() ? this.moveCursorBy(0, n) : this.moveCursorBy(0, 1) } }, this.moveCursorLineStart = function () { var e = this.lead.row, t = this.lead.column, n = this.session.documentToScreenRow(e, t), r = this.session.screenToDocumentPosition(n, 0), i = this.session.getDisplayLine(e, null, r.row, r.column), s = i.match(/^\s*/); s[0].length != t && !this.session.$useEmacsStyleLineStart && (r.column += s[0].length), this.moveCursorToPosition(r) }, this.moveCursorLineEnd = function () { var e = this.lead, t = this.session.getDocumentLastRowColumnPosition(e.row, e.column); if (this.lead.column == t.column) { var n = this.session.getLine(t.row); if (t.column == n.length) { var r = n.search(/\s+$/); r > 0 && (t.column = r) } } this.moveCursorTo(t.row, t.column) }, this.moveCursorFileEnd = function () { var e = this.doc.getLength() - 1, t = this.doc.getLine(e).length; this.moveCursorTo(e, t) }, this.moveCursorFileStart = function () { this.moveCursorTo(0, 0) }, this.moveCursorLongWordRight = function () { var e = this.lead.row, t = this.lead.column, n = this.doc.getLine(e), r = n.substring(t); this.session.nonTokenRe.lastIndex = 0, this.session.tokenRe.lastIndex = 0; var i = this.session.getFoldAt(e, t, 1); if (i) { this.moveCursorTo(i.end.row, i.end.column); return } this.session.nonTokenRe.exec(r) && (t += this.session.nonTokenRe.lastIndex, this.session.nonTokenRe.lastIndex = 0, r = n.substring(t)); if (t >= n.length) { this.moveCursorTo(e, n.length), this.moveCursorRight(), e < this.doc.getLength() - 1 && this.moveCursorWordRight(); return } this.session.tokenRe.exec(r) && (t += this.session.tokenRe.lastIndex, this.session.tokenRe.lastIndex = 0), this.moveCursorTo(e, t) }, this.moveCursorLongWordLeft = function () { var e = this.lead.row, t = this.lead.column, n; if (n = this.session.getFoldAt(e, t, -1)) { this.moveCursorTo(n.start.row, n.start.column); return } var r = this.session.getFoldStringAt(e, t, -1); r == null && (r = this.doc.getLine(e).substring(0, t)); var s = i.stringReverse(r); this.session.nonTokenRe.lastIndex = 0, this.session.tokenRe.lastIndex = 0, this.session.nonTokenRe.exec(s) && (t -= this.session.nonTokenRe.lastIndex, s = s.slice(this.session.nonTokenRe.lastIndex), this.session.nonTokenRe.lastIndex = 0); if (t <= 0) { this.moveCursorTo(e, 0), this.moveCursorLeft(), e > 0 && this.moveCursorWordLeft(); return } this.session.tokenRe.exec(s) && (t -= this.session.tokenRe.lastIndex, this.session.tokenRe.lastIndex = 0), this.moveCursorTo(e, t) }, this.$shortWordEndIndex = function (e) { var t = 0, n, r = /\s/, i = this.session.tokenRe; i.lastIndex = 0; if (this.session.tokenRe.exec(e)) t = this.session.tokenRe.lastIndex; else { while ((n = e[t]) && r.test(n)) t++; if (t < 1) { i.lastIndex = 0; while ((n = e[t]) && !i.test(n)) { i.lastIndex = 0, t++; if (r.test(n)) { if (t > 2) { t--; break } while ((n = e[t]) && r.test(n)) t++; if (t > 2) break } } } } return i.lastIndex = 0, t }, this.moveCursorShortWordRight = function () { var e = this.lead.row, t = this.lead.column, n = this.doc.getLine(e), r = n.substring(t), i = this.session.getFoldAt(e, t, 1); if (i) return this.moveCursorTo(i.end.row, i.end.column); if (t == n.length) { var s = this.doc.getLength(); do e++, r = this.doc.getLine(e); while (e < s && /^\s*$/.test(r)); /^\s+/.test(r) || (r = ""), t = 0 } var o = this.$shortWordEndIndex(r); this.moveCursorTo(e, t + o) }, this.moveCursorShortWordLeft = function () { var e = this.lead.row, t = this.lead.column, n; if (n = this.session.getFoldAt(e, t, -1)) return this.moveCursorTo(n.start.row, n.start.column); var r = this.session.getLine(e).substring(0, t); if (t === 0) { do e--, r = this.doc.getLine(e); while (e > 0 && /^\s*$/.test(r)); t = r.length, /\s+$/.test(r) || (r = "") } var s = i.stringReverse(r), o = this.$shortWordEndIndex(s); return this.moveCursorTo(e, t - o) }, this.moveCursorWordRight = function () { this.session.$selectLongWords ? this.moveCursorLongWordRight() : this.moveCursorShortWordRight() }, this.moveCursorWordLeft = function () { this.session.$selectLongWords ? this.moveCursorLongWordLeft() : this.moveCursorShortWordLeft() }, this.moveCursorBy = function (e, t) { var n = this.session.documentToScreenPosition(this.lead.row, this.lead.column), r; t === 0 && (e !== 0 && (this.session.$bidiHandler.isBidiRow(n.row, this.lead.row) ? (r = this.session.$bidiHandler.getPosLeft(n.column), n.column = Math.round(r / this.session.$bidiHandler.charWidths[0])) : r = n.column * this.session.$bidiHandler.charWidths[0]), this.$desiredColumn ? n.column = this.$desiredColumn : this.$desiredColumn = n.column); if (e != 0 && this.session.lineWidgets && this.session.lineWidgets[this.lead.row]) { var i = this.session.lineWidgets[this.lead.row]; e < 0 ? e -= i.rowsAbove || 0 : e > 0 && (e += i.rowCount - (i.rowsAbove || 0)) } var s = this.session.screenToDocumentPosition(n.row + e, n.column, r); e !== 0 && t === 0 && s.row === this.lead.row && s.column === this.lead.column, this.moveCursorTo(s.row, s.column + t, t === 0) }, this.moveCursorToPosition = function (e) { this.moveCursorTo(e.row, e.column) }, this.moveCursorTo = function (e, t, n) { var r = this.session.getFoldAt(e, t, 1); r && (e = r.start.row, t = r.start.column), this.$keepDesiredColumnOnChange = !0; var i = this.session.getLine(e); /[\uDC00-\uDFFF]/.test(i.charAt(t)) && i.charAt(t - 1) && (this.lead.row == e && this.lead.column == t + 1 ? t -= 1 : t += 1), this.lead.setPosition(e, t), this.$keepDesiredColumnOnChange = !1, n || (this.$desiredColumn = null) }, this.moveCursorToScreen = function (e, t, n) { var r = this.session.screenToDocumentPosition(e, t); this.moveCursorTo(r.row, r.column, n) }, this.detach = function () { this.lead.detach(), this.anchor.detach(), this.session = this.doc = null }, this.fromOrientedRange = function (e) { this.setSelectionRange(e, e.cursor == e.start), this.$desiredColumn = e.desiredColumn || this.$desiredColumn }, this.toOrientedRange = function (e) { var t = this.getRange(); return e ? (e.start.column = t.start.column, e.start.row = t.start.row, e.end.column = t.end.column, e.end.row = t.end.row) : e = t, e.cursor = this.isBackwards() ? e.start : e.end, e.desiredColumn = this.$desiredColumn, e }, this.getRangeOfMovements = function (e) { var t = this.getCursor(); try { e(this); var n = this.getCursor(); return o.fromPoints(t, n) } catch (r) { return o.fromPoints(t, t) } finally { this.moveCursorToPosition(t) } }, this.toJSON = function () { if (this.rangeCount) var e = this.ranges.map(function (e) { var t = e.clone(); return t.isBackwards = e.cursor == e.start, t }); else { var e = this.getRange(); e.isBackwards = this.isBackwards() } return e }, this.fromJSON = function (e) { if (e.start == undefined) { if (this.rangeList && e.length > 1) { this.toSingleRange(e[0]); for (var t = e.length; t--;) { var n = o.fromPoints(e[t].start, e[t].end); e[t].isBackwards && (n.cursor = n.start), this.addRange(n, !0) } return } e = e[0] } this.rangeList && this.toSingleRange(e), this.setSelectionRange(e, e.isBackwards) }, this.isEqual = function (e) { if ((e.length || this.rangeCount) && e.length != this.rangeCount) return !1; if (!e.length || !this.ranges) return this.getRange().isEqual(e); for (var t = this.ranges.length; t--;)if (!this.ranges[t].isEqual(e[t])) return !1; return !0 } }).call(u.prototype), t.Selection = u }), define("ace/tokenizer", ["require", "exports", "module", "ace/config"], function (e, t, n) { "use strict"; var r = e("./config"), i = 2e3, s = function (e) { this.states = e, this.regExps = {}, this.matchMappings = {}; for (var t in this.states) { var n = this.states[t], r = [], i = 0, s = this.matchMappings[t] = { defaultToken: "text" }, o = "g", u = []; for (var a = 0; a < n.length; a++) { var f = n[a]; f.defaultToken && (s.defaultToken = f.defaultToken), f.caseInsensitive && (o = "gi"); if (f.regex == null) continue; f.regex instanceof RegExp && (f.regex = f.regex.toString().slice(1, -1)); var l = f.regex, c = (new RegExp("(?:(" + l + ")|(.))")).exec("a").length - 2; Array.isArray(f.token) ? f.token.length == 1 || c == 1 ? f.token = f.token[0] : c - 1 != f.token.length ? (this.reportError("number of classes and regexp groups doesn't match", { rule: f, groupCount: c - 1 }), f.token = f.token[0]) : (f.tokenArray = f.token, f.token = null, f.onMatch = this.$arrayTokens) : typeof f.token == "function" && !f.onMatch && (c > 1 ? f.onMatch = this.$applyToken : f.onMatch = f.token), c > 1 && (/\\\d/.test(f.regex) ? l = f.regex.replace(/\\([0-9]+)/g, function (e, t) { return "\\" + (parseInt(t, 10) + i + 1) }) : (c = 1, l = this.removeCapturingGroups(f.regex)), !f.splitRegex && typeof f.token != "string" && u.push(f)), s[i] = a, i += c, r.push(l), f.onMatch || (f.onMatch = null) } r.length || (s[0] = 0, r.push("$")), u.forEach(function (e) { e.splitRegex = this.createSplitterRegexp(e.regex, o) }, this), this.regExps[t] = new RegExp("(" + r.join(")|(") + ")|($)", o) } }; (function () { this.$setMaxTokenCount = function (e) { i = e | 0 }, this.$applyToken = function (e) { var t = this.splitRegex.exec(e).slice(1), n = this.token.apply(this, t); if (typeof n == "string") return [{ type: n, value: e }]; var r = []; for (var i = 0, s = n.length; i < s; i++)t[i] && (r[r.length] = { type: n[i], value: t[i] }); return r }, this.$arrayTokens = function (e) { if (!e) return []; var t = this.splitRegex.exec(e); if (!t) return "text"; var n = [], r = this.tokenArray; for (var i = 0, s = r.length; i < s; i++)t[i + 1] && (n[n.length] = { type: r[i], value: t[i + 1] }); return n }, this.removeCapturingGroups = function (e) { var t = e.replace(/\\.|\[(?:\\.|[^\\\]])*|\(\?[:=!]|(\()/g, function (e, t) { return t ? "(?:" : e }); return t }, this.createSplitterRegexp = function (e, t) { if (e.indexOf("(?=") != -1) { var n = 0, r = !1, i = {}; e.replace(/(\\.)|(\((?:\?[=!])?)|(\))|([\[\]])/g, function (e, t, s, o, u, a) { return r ? r = u != "]" : u ? r = !0 : o ? (n == i.stack && (i.end = a + 1, i.stack = -1), n--) : s && (n++, s.length != 1 && (i.stack = n, i.start = a)), e }), i.end != null && /^\)*$/.test(e.substr(i.end)) && (e = e.substring(0, i.start) + e.substr(i.end)) } return e.charAt(0) != "^" && (e = "^" + e), e.charAt(e.length - 1) != "$" && (e += "$"), new RegExp(e, (t || "").replace("g", "")) }, this.getLineTokens = function (e, t) { if (t && typeof t != "string") { var n = t.slice(0); t = n[0], t === "#tmp" && (n.shift(), t = n.shift()) } else var n = []; var r = t || "start", s = this.states[r]; s || (r = "start", s = this.states[r]); var o = this.matchMappings[r], u = this.regExps[r]; u.lastIndex = 0; var a, f = [], l = 0, c = 0, h = { type: null, value: "" }; while (a = u.exec(e)) { var p = o.defaultToken, d = null, v = a[0], m = u.lastIndex; if (m - v.length > l) { var g = e.substring(l, m - v.length); h.type == p ? h.value += g : (h.type && f.push(h), h = { type: p, value: g }) } for (var y = 0; y < a.length - 2; y++) { if (a[y + 1] === undefined) continue; d = s[o[y]], d.onMatch ? p = d.onMatch(v, r, n, e) : p = d.token, d.next && (typeof d.next == "string" ? r = d.next : r = d.next(r, n), s = this.states[r], s || (this.reportError("state doesn't exist", r), r = "start", s = this.states[r]), o = this.matchMappings[r], l = m, u = this.regExps[r], u.lastIndex = m), d.consumeLineEnd && (l = m); break } if (v) if (typeof p == "string") !!d && d.merge === !1 || h.type !== p ? (h.type && f.push(h), h = { type: p, value: v }) : h.value += v; else if (p) { h.type && f.push(h), h = { type: null, value: "" }; for (var y = 0; y < p.length; y++)f.push(p[y]) } if (l == e.length) break; l = m; if (c++ > i) { c > 2 * e.length && this.reportError("infinite loop with in ace tokenizer", { startState: t, line: e }); while (l < e.length) h.type && f.push(h), h = { value: e.substring(l, l += 500), type: "overflow" }; r = "start", n = []; break } } return h.type && f.push(h), n.length > 1 && n[0] !== r && n.unshift("#tmp", r), { tokens: f, state: n.length ? n : r } }, this.reportError = r.reportError }).call(s.prototype), t.Tokenizer = s }), define("ace/mode/text_highlight_rules", ["require", "exports", "module", "ace/lib/lang"], function (e, t, n) { "use strict"; var r = e("../lib/lang"), i = function () { this.$rules = { start: [{ token: "empty_line", regex: "^$" }, { defaultToken: "text" }] } }; (function () { this.addRules = function (e, t) { if (!t) { for (var n in e) this.$rules[n] = e[n]; return } for (var n in e) { var r = e[n]; for (var i = 0; i < r.length; i++) { var s = r[i]; if (s.next || s.onMatch) typeof s.next == "string" && s.next.indexOf(t) !== 0 && (s.next = t + s.next), s.nextState && s.nextState.indexOf(t) !== 0 && (s.nextState = t + s.nextState) } this.$rules[t + n] = r } }, this.getRules = function () { return this.$rules }, this.embedRules = function (e, t, n, i, s) { var o = typeof e == "function" ? (new e).getRules() : e; if (i) for (var u = 0; u < i.length; u++)i[u] = t + i[u]; else { i = []; for (var a in o) i.push(t + a) } this.addRules(o, t); if (n) { var f = Array.prototype[s ? "push" : "unshift"]; for (var u = 0; u < i.length; u++)f.apply(this.$rules[i[u]], r.deepCopy(n)) } this.$embeds || (this.$embeds = []), this.$embeds.push(t) }, this.getEmbeds = function () { return this.$embeds }; var e = function (e, t) { return (e != "start" || t.length) && t.unshift(this.nextState, e), this.nextState }, t = function (e, t) { return t.shift(), t.shift() || "start" }; this.normalizeRules = function () { function i(s) { var o = r[s]; o.processed = !0; for (var u = 0; u < o.length; u++) { var a = o[u], f = null; Array.isArray(a) && (f = a, a = {}), !a.regex && a.start && (a.regex = a.start, a.next || (a.next = []), a.next.push({ defaultToken: a.token }, { token: a.token + ".end", regex: a.end || a.start, next: "pop" }), a.token = a.token + ".start", a.push = !0); var l = a.next || a.push; if (l && Array.isArray(l)) { var c = a.stateName; c || (c = a.token, typeof c != "string" && (c = c[0] || ""), r[c] && (c += n++)), r[c] = l, a.next = c, i(c) } else l == "pop" && (a.next = t); a.push && (a.nextState = a.next || a.push, a.next = e, delete a.push); if (a.rules) for (var h in a.rules) r[h] ? r[h].push && r[h].push.apply(r[h], a.rules[h]) : r[h] = a.rules[h]; var p = typeof a == "string" ? a : a.include; p && (Array.isArray(p) ? f = p.map(function (e) { return r[e] }) : f = r[p]); if (f) { var d = [u, 1].concat(f); a.noEscape && (d = d.filter(function (e) { return !e.next })), o.splice.apply(o, d), u-- } a.keywordMap && (a.token = this.createKeywordMapper(a.keywordMap, a.defaultToken || "text", a.caseInsensitive), delete a.defaultToken) } } var n = 0, r = this.$rules; Object.keys(r).forEach(i, this) }, this.createKeywordMapper = function (e, t, n, r) { var i = Object.create(null); return Object.keys(e).forEach(function (t) { var s = e[t]; n && (s = s.toLowerCase()); var o = s.split(r || "|"); for (var u = o.length; u--;)i[o[u]] = t }), Object.getPrototypeOf(i) && (i.__proto__ = null), this.$keywordList = Object.keys(i), e = null, n ? function (e) { return i[e.toLowerCase()] || t } : function (e) { return i[e] || t } }, this.getKeywords = function () { return this.$keywords } }).call(i.prototype), t.TextHighlightRules = i }), define("ace/mode/behaviour", ["require", "exports", "module"], function (e, t, n) { "use strict"; var r = function () { this.$behaviours = {} }; (function () { this.add = function (e, t, n) { switch (undefined) { case this.$behaviours: this.$behaviours = {}; case this.$behaviours[e]: this.$behaviours[e] = {} }this.$behaviours[e][t] = n }, this.addBehaviours = function (e) { for (var t in e) for (var n in e[t]) this.add(t, n, e[t][n]) }, this.remove = function (e) { this.$behaviours && this.$behaviours[e] && delete this.$behaviours[e] }, this.inherit = function (e, t) { if (typeof e == "function") var n = (new e).getBehaviours(t); else var n = e.getBehaviours(t); this.addBehaviours(n) }, this.getBehaviours = function (e) { if (!e) return this.$behaviours; var t = {}; for (var n = 0; n < e.length; n++)this.$behaviours[e[n]] && (t[e[n]] = this.$behaviours[e[n]]); return t } }).call(r.prototype), t.Behaviour = r }), define("ace/token_iterator", ["require", "exports", "module", "ace/range"], function (e, t, n) { "use strict"; var r = e("./range").Range, i = function (e, t, n) { this.$session = e, this.$row = t, this.$rowTokens = e.getTokens(t); var r = e.getTokenAt(t, n); this.$tokenIndex = r ? r.index : -1 }; (function () { this.stepBackward = function () { this.$tokenIndex -= 1; while (this.$tokenIndex < 0) { this.$row -= 1; if (this.$row < 0) return this.$row = 0, null; this.$rowTokens = this.$session.getTokens(this.$row), this.$tokenIndex = this.$rowTokens.length - 1 } return this.$rowTokens[this.$tokenIndex] }, this.stepForward = function () { this.$tokenIndex += 1; var e; while (this.$tokenIndex >= this.$rowTokens.length) { this.$row += 1, e || (e = this.$session.getLength()); if (this.$row >= e) return this.$row = e - 1, null; this.$rowTokens = this.$session.getTokens(this.$row), this.$tokenIndex = 0 } return this.$rowTokens[this.$tokenIndex] }, this.getCurrentToken = function () { return this.$rowTokens[this.$tokenIndex] }, this.getCurrentTokenRow = function () { return this.$row }, this.getCurrentTokenColumn = function () { var e = this.$rowTokens, t = this.$tokenIndex, n = e[t].start; if (n !== undefined) return n; n = 0; while (t > 0) t -= 1, n += e[t].value.length; return n }, this.getCurrentTokenPosition = function () { return { row: this.$row, column: this.getCurrentTokenColumn() } }, this.getCurrentTokenRange = function () { var e = this.$rowTokens[this.$tokenIndex], t = this.getCurrentTokenColumn(); return new r(this.$row, t, this.$row, t + e.value.length) } }).call(i.prototype), t.TokenIterator = i }), define("ace/mode/behaviour/cstyle", ["require", "exports", "module", "ace/lib/oop", "ace/mode/behaviour", "ace/token_iterator", "ace/lib/lang"], function (e, t, n) { "use strict"; var r = e("../../lib/oop"), i = e("../behaviour").Behaviour, s = e("../../token_iterator").TokenIterator, o = e("../../lib/lang"), u = ["text", "paren.rparen", "rparen", "paren", "punctuation.operator"], a = ["text", "paren.rparen", "rparen", "paren", "punctuation.operator", "comment"], f, l = {}, c = { '"': '"', "'": "'" }, h = function (e) { var t = -1; e.multiSelect && (t = e.selection.index, l.rangeCount != e.multiSelect.rangeCount && (l = { rangeCount: e.multiSelect.rangeCount })); if (l[t]) return f = l[t]; f = l[t] = { autoInsertedBrackets: 0, autoInsertedRow: -1, autoInsertedLineEnd: "", maybeInsertedBrackets: 0, maybeInsertedRow: -1, maybeInsertedLineStart: "", maybeInsertedLineEnd: "" } }, p = function (e, t, n, r) { var i = e.end.row - e.start.row; return { text: n + t + r, selection: [0, e.start.column + 1, i, e.end.column + (i ? 0 : 1)] } }, d = function (e) { this.add("braces", "insertion", function (t, n, r, i, s) { var u = r.getCursorPosition(), a = i.doc.getLine(u.row); if (s == "{") { h(r); var l = r.getSelectionRange(), c = i.doc.getTextRange(l); if (c !== "" && c !== "{" && r.getWrapBehavioursEnabled()) return p(l, c, "{", "}"); if (d.isSaneInsertion(r, i)) return /[\]\}\)]/.test(a[u.column]) || r.inMultiSelectMode || e && e.braces ? (d.recordAutoInsert(r, i, "}"), { text: "{}", selection: [1, 1] }) : (d.recordMaybeInsert(r, i, "{"), { text: "{", selection: [1, 1] }) } else if (s == "}") { h(r); var v = a.substring(u.column, u.column + 1); if (v == "}") { var m = i.$findOpeningBracket("}", { column: u.column + 1, row: u.row }); if (m !== null && d.isAutoInsertedClosing(u, a, s)) return d.popAutoInsertedClosing(), { text: "", selection: [1, 1] } } } else { if (s == "\n" || s == "\r\n") { h(r); var g = ""; d.isMaybeInsertedClosing(u, a) && (g = o.stringRepeat("}", f.maybeInsertedBrackets), d.clearMaybeInsertedClosing()); var v = a.substring(u.column, u.column + 1); if (v === "}") { var y = i.findMatchingBracket({ row: u.row, column: u.column + 1 }, "}"); if (!y) return null; var b = this.$getIndent(i.getLine(y.row)) } else { if (!g) { d.clearMaybeInsertedClosing(); return } var b = this.$getIndent(a) } var w = b + i.getTabString(); return { text: "\n" + w + "\n" + b + g, selection: [1, w.length, 1, w.length] } } d.clearMaybeInsertedClosing() } }), this.add("braces", "deletion", function (e, t, n, r, i) { var s = r.doc.getTextRange(i); if (!i.isMultiLine() && s == "{") { h(n); var o = r.doc.getLine(i.start.row), u = o.substring(i.end.column, i.end.column + 1); if (u == "}") return i.end.column++, i; f.maybeInsertedBrackets-- } }), this.add("parens", "insertion", function (e, t, n, r, i) { if (i == "(") { h(n); var s = n.getSelectionRange(), o = r.doc.getTextRange(s); if (o !== "" && n.getWrapBehavioursEnabled()) return p(s, o, "(", ")"); if (d.isSaneInsertion(n, r)) return d.recordAutoInsert(n, r, ")"), { text: "()", selection: [1, 1] } } else if (i == ")") { h(n); var u = n.getCursorPosition(), a = r.doc.getLine(u.row), f = a.substring(u.column, u.column + 1); if (f == ")") { var l = r.$findOpeningBracket(")", { column: u.column + 1, row: u.row }); if (l !== null && d.isAutoInsertedClosing(u, a, i)) return d.popAutoInsertedClosing(), { text: "", selection: [1, 1] } } } }), this.add("parens", "deletion", function (e, t, n, r, i) { var s = r.doc.getTextRange(i); if (!i.isMultiLine() && s == "(") { h(n); var o = r.doc.getLine(i.start.row), u = o.substring(i.start.column + 1, i.start.column + 2); if (u == ")") return i.end.column++, i } }), this.add("brackets", "insertion", function (e, t, n, r, i) { if (i == "[") { h(n); var s = n.getSelectionRange(), o = r.doc.getTextRange(s); if (o !== "" && n.getWrapBehavioursEnabled()) return p(s, o, "[", "]"); if (d.isSaneInsertion(n, r)) return d.recordAutoInsert(n, r, "]"), { text: "[]", selection: [1, 1] } } else if (i == "]") { h(n); var u = n.getCursorPosition(), a = r.doc.getLine(u.row), f = a.substring(u.column, u.column + 1); if (f == "]") { var l = r.$findOpeningBracket("]", { column: u.column + 1, row: u.row }); if (l !== null && d.isAutoInsertedClosing(u, a, i)) return d.popAutoInsertedClosing(), { text: "", selection: [1, 1] } } } }), this.add("brackets", "deletion", function (e, t, n, r, i) { var s = r.doc.getTextRange(i); if (!i.isMultiLine() && s == "[") { h(n); var o = r.doc.getLine(i.start.row), u = o.substring(i.start.column + 1, i.start.column + 2); if (u == "]") return i.end.column++, i } }), this.add("string_dquotes", "insertion", function (e, t, n, r, i) { var s = r.$mode.$quotes || c; if (i.length == 1 && s[i]) { if (this.lineCommentStart && this.lineCommentStart.indexOf(i) != -1) return; h(n); var o = i, u = n.getSelectionRange(), a = r.doc.getTextRange(u); if (a !== "" && (a.length != 1 || !s[a]) && n.getWrapBehavioursEnabled()) return p(u, a, o, o); if (!a) { var f = n.getCursorPosition(), l = r.doc.getLine(f.row), d = l.substring(f.column - 1, f.column), v = l.substring(f.column, f.column + 1), m = r.getTokenAt(f.row, f.column), g = r.getTokenAt(f.row, f.column + 1); if (d == "\\" && m && /escape/.test(m.type)) return null; var y = m && /string|escape/.test(m.type), b = !g || /string|escape/.test(g.type), w; if (v == o) w = y !== b, w && /string\.end/.test(g.type) && (w = !1); else { if (y && !b) return null; if (y && b) return null; var E = r.$mode.tokenRe; E.lastIndex = 0; var S = E.test(d); E.lastIndex = 0; var x = E.test(d); if (S || x) return null; if (v && !/[\s;,.})\]\\]/.test(v)) return null; var T = l[f.column - 2]; if (!(d != o || T != o && !E.test(T))) return null; w = !0 } return { text: w ? o + o : "", selection: [1, 1] } } } }), this.add("string_dquotes", "deletion", function (e, t, n, r, i) { var s = r.$mode.$quotes || c, o = r.doc.getTextRange(i); if (!i.isMultiLine() && s.hasOwnProperty(o)) { h(n); var u = r.doc.getLine(i.start.row), a = u.substring(i.start.column + 1, i.start.column + 2); if (a == o) return i.end.column++, i } }) }; d.isSaneInsertion = function (e, t) { var n = e.getCursorPosition(), r = new s(t, n.row, n.column); if (!this.$matchTokenType(r.getCurrentToken() || "text", u)) { if (/[)}\]]/.test(e.session.getLine(n.row)[n.column])) return !0; var i = new s(t, n.row, n.column + 1); if (!this.$matchTokenType(i.getCurrentToken() || "text", u)) return !1 } return r.stepForward(), r.getCurrentTokenRow() !== n.row || this.$matchTokenType(r.getCurrentToken() || "text", a) }, d.$matchTokenType = function (e, t) { return t.indexOf(e.type || e) > -1 }, d.recordAutoInsert = function (e, t, n) { var r = e.getCursorPosition(), i = t.doc.getLine(r.row); this.isAutoInsertedClosing(r, i, f.autoInsertedLineEnd[0]) || (f.autoInsertedBrackets = 0), f.autoInsertedRow = r.row, f.autoInsertedLineEnd = n + i.substr(r.column), f.autoInsertedBrackets++ }, d.recordMaybeInsert = function (e, t, n) { var r = e.getCursorPosition(), i = t.doc.getLine(r.row); this.isMaybeInsertedClosing(r, i) || (f.maybeInsertedBrackets = 0), f.maybeInsertedRow = r.row, f.maybeInsertedLineStart = i.substr(0, r.column) + n, f.maybeInsertedLineEnd = i.substr(r.column), f.maybeInsertedBrackets++ }, d.isAutoInsertedClosing = function (e, t, n) { return f.autoInsertedBrackets > 0 && e.row === f.autoInsertedRow && n === f.autoInsertedLineEnd[0] && t.substr(e.column) === f.autoInsertedLineEnd }, d.isMaybeInsertedClosing = function (e, t) { return f.maybeInsertedBrackets > 0 && e.row === f.maybeInsertedRow && t.substr(e.column) === f.maybeInsertedLineEnd && t.substr(0, e.column) == f.maybeInsertedLineStart }, d.popAutoInsertedClosing = function () { f.autoInsertedLineEnd = f.autoInsertedLineEnd.substr(1), f.autoInsertedBrackets-- }, d.clearMaybeInsertedClosing = function () { f && (f.maybeInsertedBrackets = 0, f.maybeInsertedRow = -1) }, r.inherits(d, i), t.CstyleBehaviour = d }), define("ace/unicode", ["require", "exports", "module"], function (e, t, n) { "use strict"; var r = [48, 9, 8, 25, 5, 0, 2, 25, 48, 0, 11, 0, 5, 0, 6, 22, 2, 30, 2, 457, 5, 11, 15, 4, 8, 0, 2, 0, 18, 116, 2, 1, 3, 3, 9, 0, 2, 2, 2, 0, 2, 19, 2, 82, 2, 138, 2, 4, 3, 155, 12, 37, 3, 0, 8, 38, 10, 44, 2, 0, 2, 1, 2, 1, 2, 0, 9, 26, 6, 2, 30, 10, 7, 61, 2, 9, 5, 101, 2, 7, 3, 9, 2, 18, 3, 0, 17, 58, 3, 100, 15, 53, 5, 0, 6, 45, 211, 57, 3, 18, 2, 5, 3, 11, 3, 9, 2, 1, 7, 6, 2, 2, 2, 7, 3, 1, 3, 21, 2, 6, 2, 0, 4, 3, 3, 8, 3, 1, 3, 3, 9, 0, 5, 1, 2, 4, 3, 11, 16, 2, 2, 5, 5, 1, 3, 21, 2, 6, 2, 1, 2, 1, 2, 1, 3, 0, 2, 4, 5, 1, 3, 2, 4, 0, 8, 3, 2, 0, 8, 15, 12, 2, 2, 8, 2, 2, 2, 21, 2, 6, 2, 1, 2, 4, 3, 9, 2, 2, 2, 2, 3, 0, 16, 3, 3, 9, 18, 2, 2, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 3, 8, 3, 1, 3, 2, 9, 1, 5, 1, 2, 4, 3, 9, 2, 0, 17, 1, 2, 5, 4, 2, 2, 3, 4, 1, 2, 0, 2, 1, 4, 1, 4, 2, 4, 11, 5, 4, 4, 2, 2, 3, 3, 0, 7, 0, 15, 9, 18, 2, 2, 7, 2, 2, 2, 22, 2, 9, 2, 4, 4, 7, 2, 2, 2, 3, 8, 1, 2, 1, 7, 3, 3, 9, 19, 1, 2, 7, 2, 2, 2, 22, 2, 9, 2, 4, 3, 8, 2, 2, 2, 3, 8, 1, 8, 0, 2, 3, 3, 9, 19, 1, 2, 7, 2, 2, 2, 22, 2, 15, 4, 7, 2, 2, 2, 3, 10, 0, 9, 3, 3, 9, 11, 5, 3, 1, 2, 17, 4, 23, 2, 8, 2, 0, 3, 6, 4, 0, 5, 5, 2, 0, 2, 7, 19, 1, 14, 57, 6, 14, 2, 9, 40, 1, 2, 0, 3, 1, 2, 0, 3, 0, 7, 3, 2, 6, 2, 2, 2, 0, 2, 0, 3, 1, 2, 12, 2, 2, 3, 4, 2, 0, 2, 5, 3, 9, 3, 1, 35, 0, 24, 1, 7, 9, 12, 0, 2, 0, 2, 0, 5, 9, 2, 35, 5, 19, 2, 5, 5, 7, 2, 35, 10, 0, 58, 73, 7, 77, 3, 37, 11, 42, 2, 0, 4, 328, 2, 3, 3, 6, 2, 0, 2, 3, 3, 40, 2, 3, 3, 32, 2, 3, 3, 6, 2, 0, 2, 3, 3, 14, 2, 56, 2, 3, 3, 66, 5, 0, 33, 15, 17, 84, 13, 619, 3, 16, 2, 25, 6, 74, 22, 12, 2, 6, 12, 20, 12, 19, 13, 12, 2, 2, 2, 1, 13, 51, 3, 29, 4, 0, 5, 1, 3, 9, 34, 2, 3, 9, 7, 87, 9, 42, 6, 69, 11, 28, 4, 11, 5, 11, 11, 39, 3, 4, 12, 43, 5, 25, 7, 10, 38, 27, 5, 62, 2, 28, 3, 10, 7, 9, 14, 0, 89, 75, 5, 9, 18, 8, 13, 42, 4, 11, 71, 55, 9, 9, 4, 48, 83, 2, 2, 30, 14, 230, 23, 280, 3, 5, 3, 37, 3, 5, 3, 7, 2, 0, 2, 0, 2, 0, 2, 30, 3, 52, 2, 6, 2, 0, 4, 2, 2, 6, 4, 3, 3, 5, 5, 12, 6, 2, 2, 6, 67, 1, 20, 0, 29, 0, 14, 0, 17, 4, 60, 12, 5, 0, 4, 11, 18, 0, 5, 0, 3, 9, 2, 0, 4, 4, 7, 0, 2, 0, 2, 0, 2, 3, 2, 10, 3, 3, 6, 4, 5, 0, 53, 1, 2684, 46, 2, 46, 2, 132, 7, 6, 15, 37, 11, 53, 10, 0, 17, 22, 10, 6, 2, 6, 2, 6, 2, 6, 2, 6, 2, 6, 2, 6, 2, 6, 2, 31, 48, 0, 470, 1, 36, 5, 2, 4, 6, 1, 5, 85, 3, 1, 3, 2, 2, 89, 2, 3, 6, 40, 4, 93, 18, 23, 57, 15, 513, 6581, 75, 20939, 53, 1164, 68, 45, 3, 268, 4, 27, 21, 31, 3, 13, 13, 1, 2, 24, 9, 69, 11, 1, 38, 8, 3, 102, 3, 1, 111, 44, 25, 51, 13, 68, 12, 9, 7, 23, 4, 0, 5, 45, 3, 35, 13, 28, 4, 64, 15, 10, 39, 54, 10, 13, 3, 9, 7, 22, 4, 1, 5, 66, 25, 2, 227, 42, 2, 1, 3, 9, 7, 11171, 13, 22, 5, 48, 8453, 301, 3, 61, 3, 105, 39, 6, 13, 4, 6, 11, 2, 12, 2, 4, 2, 0, 2, 1, 2, 1, 2, 107, 34, 362, 19, 63, 3, 53, 41, 11, 5, 15, 17, 6, 13, 1, 25, 2, 33, 4, 2, 134, 20, 9, 8, 25, 5, 0, 2, 25, 12, 88, 4, 5, 3, 5, 3, 5, 3, 2], i = 0, s = []; for (var o = 0; o < r.length; o += 2)s.push(i += r[o]), r[o + 1] && s.push(45, i += r[o + 1]); t.wordChars = String.fromCharCode.apply(null, s) }), define("ace/mode/text", ["require", "exports", "module", "ace/config", "ace/tokenizer", "ace/mode/text_highlight_rules", "ace/mode/behaviour/cstyle", "ace/unicode", "ace/lib/lang", "ace/token_iterator", "ace/range"], function (e, t, n) { "use strict"; var r = e("../config"), i = e("../tokenizer").Tokenizer, s = e("./text_highlight_rules").TextHighlightRules, o = e("./behaviour/cstyle").CstyleBehaviour, u = e("../unicode"), a = e("../lib/lang"), f = e("../token_iterator").TokenIterator, l = e("../range").Range, c = function () { this.HighlightRules = s }; (function () { this.$defaultBehaviour = new o, this.tokenRe = new RegExp("^[" + u.wordChars + "\\$_]+", "g"), this.nonTokenRe = new RegExp("^(?:[^" + u.wordChars + "\\$_]|\\s])+", "g"), this.getTokenizer = function () { return this.$tokenizer || (this.$highlightRules = this.$highlightRules || new this.HighlightRules(this.$highlightRuleConfig), this.$tokenizer = new i(this.$highlightRules.getRules())), this.$tokenizer }, this.lineCommentStart = "", this.blockComment = "", this.toggleCommentLines = function (e, t, n, r) { function w(e) { for (var t = n; t <= r; t++)e(i.getLine(t), t) } var i = t.doc, s = !0, o = !0, u = Infinity, f = t.getTabSize(), l = !1; if (!this.lineCommentStart) { if (!this.blockComment) return !1; var c = this.blockComment.start, h = this.blockComment.end, p = new RegExp("^(\\s*)(?:" + a.escapeRegExp(c) + ")"), d = new RegExp("(?:" + a.escapeRegExp(h) + ")\\s*$"), v = function (e, t) { if (g(e, t)) return; if (!s || /\S/.test(e)) i.insertInLine({ row: t, column: e.length }, h), i.insertInLine({ row: t, column: u }, c) }, m = function (e, t) { var n; (n = e.match(d)) && i.removeInLine(t, e.length - n[0].length, e.length), (n = e.match(p)) && i.removeInLine(t, n[1].length, n[0].length) }, g = function (e, n) { if (p.test(e)) return !0; var r = t.getTokens(n); for (var i = 0; i < r.length; i++)if (r[i].type === "comment") return !0 } } else { if (Array.isArray(this.lineCommentStart)) var p = this.lineCommentStart.map(a.escapeRegExp).join("|"), c = this.lineCommentStart[0]; else var p = a.escapeRegExp(this.lineCommentStart), c = this.lineCommentStart; p = new RegExp("^(\\s*)(?:" + p + ") ?"), l = t.getUseSoftTabs(); var m = function (e, t) { var n = e.match(p); if (!n) return; var r = n[1].length, s = n[0].length; !b(e, r, s) && n[0][s - 1] == " " && s--, i.removeInLine(t, r, s) }, y = c + " ", v = function (e, t) { if (!s || /\S/.test(e)) b(e, u, u) ? i.insertInLine({ row: t, column: u }, y) : i.insertInLine({ row: t, column: u }, c) }, g = function (e, t) { return p.test(e) }, b = function (e, t, n) { var r = 0; while (t-- && e.charAt(t) == " ") r++; if (r % f != 0) return !1; var r = 0; while (e.charAt(n++) == " ") r++; return f > 2 ? r % f != f - 1 : r % f == 0 } } var E = Infinity; w(function (e, t) { var n = e.search(/\S/); n !== -1 ? (n < u && (u = n), o && !g(e, t) && (o = !1)) : E > e.length && (E = e.length) }), u == Infinity && (u = E, s = !1, o = !1), l && u % f != 0 && (u = Math.floor(u / f) * f), w(o ? m : v) }, this.toggleBlockComment = function (e, t, n, r) { var i = this.blockComment; if (!i) return; !i.start && i[0] && (i = i[0]); var s = new f(t, r.row, r.column), o = s.getCurrentToken(), u = t.selection, a = t.selection.toOrientedRange(), c, h; if (o && /comment/.test(o.type)) { var p, d; while (o && /comment/.test(o.type)) { var v = o.value.indexOf(i.start); if (v != -1) { var m = s.getCurrentTokenRow(), g = s.getCurrentTokenColumn() + v; p = new l(m, g, m, g + i.start.length); break } o = s.stepBackward() } var s = new f(t, r.row, r.column), o = s.getCurrentToken(); while (o && /comment/.test(o.type)) { var v = o.value.indexOf(i.end); if (v != -1) { var m = s.getCurrentTokenRow(), g = s.getCurrentTokenColumn() + v; d = new l(m, g, m, g + i.end.length); break } o = s.stepForward() } d && t.remove(d), p && (t.remove(p), c = p.start.row, h = -i.start.length) } else h = i.start.length, c = n.start.row, t.insert(n.end, i.end), t.insert(n.start, i.start); a.start.row == c && (a.start.column += h), a.end.row == c && (a.end.column += h), t.selection.fromOrientedRange(a) }, this.getNextLineIndent = function (e, t, n) { return this.$getIndent(t) }, this.checkOutdent = function (e, t, n) { return !1 }, this.autoOutdent = function (e, t, n) { }, this.$getIndent = function (e) { return e.match(/^\s*/)[0] }, this.createWorker = function (e) { return null }, this.createModeDelegates = function (e) { this.$embeds = [], this.$modes = {}; for (var t in e) if (e[t]) { var n = e[t], i = n.prototype.$id, s = r.$modes[i]; s || (r.$modes[i] = s = new n), r.$modes[t] || (r.$modes[t] = s), this.$embeds.push(t), this.$modes[t] = s } var o = ["toggleBlockComment", "toggleCommentLines", "getNextLineIndent", "checkOutdent", "autoOutdent", "transformAction", "getCompletions"]; for (var t = 0; t < o.length; t++)(function (e) { var n = o[t], r = e[n]; e[o[t]] = function () { return this.$delegator(n, arguments, r) } })(this) }, this.$delegator = function (e, t, n) { var r = t[0] || "start"; if (typeof r != "string") { if (Array.isArray(r[2])) { var i = r[2][r[2].length - 1], s = this.$modes[i]; if (s) return s[e].apply(s, [r[1]].concat([].slice.call(t, 1))) } r = r[0] || "start" } for (var o = 0; o < this.$embeds.length; o++) { if (!this.$modes[this.$embeds[o]]) continue; var u = r.split(this.$embeds[o]); if (!u[0] && u[1]) { t[0] = u[1]; var s = this.$modes[this.$embeds[o]]; return s[e].apply(s, t) } } var a = n.apply(this, t); return n ? a : undefined }, this.transformAction = function (e, t, n, r, i) { if (this.$behaviour) { var s = this.$behaviour.getBehaviours(); for (var o in s) if (s[o][t]) { var u = s[o][t].apply(this, arguments); if (u) return u } } }, this.getKeywords = function (e) { if (!this.completionKeywords) { var t = this.$tokenizer.rules, n = []; for (var r in t) { var i = t[r]; for (var s = 0, o = i.length; s < o; s++)if (typeof i[s].token == "string") /keyword|support|storage/.test(i[s].token) && n.push(i[s].regex); else if (typeof i[s].token == "object") for (var u = 0, a = i[s].token.length; u < a; u++)if (/keyword|support|storage/.test(i[s].token[u])) { var r = i[s].regex.match(/\(.+?\)/g)[u]; n.push(r.substr(1, r.length - 2)) } } this.completionKeywords = n } return e ? n.concat(this.$keywordList || []) : this.$keywordList }, this.$createKeywordList = function () { return this.$highlightRules || this.getTokenizer(), this.$keywordList = this.$highlightRules.$keywordList || [] }, this.getCompletions = function (e, t, n, r) { var i = this.$keywordList || this.$createKeywordList(); return i.map(function (e) { return { name: e, value: e, score: 0, meta: "keyword" } }) }, this.$id = "ace/mode/text" }).call(c.prototype), t.Mode = c }), define("ace/apply_delta", ["require", "exports", "module"], function (e, t, n) { "use strict"; function r(e, t) { throw console.log("Invalid Delta:", e), "Invalid Delta: " + t } function i(e, t) { return t.row >= 0 && t.row < e.length && t.column >= 0 && t.column <= e[t.row].length } function s(e, t) { t.action != "insert" && t.action != "remove" && r(t, "delta.action must be 'insert' or 'remove'"), t.lines instanceof Array || r(t, "delta.lines must be an Array"), (!t.start || !t.end) && r(t, "delta.start/end must be an present"); var n = t.start; i(e, t.start) || r(t, "delta.start must be contained in document"); var s = t.end; t.action == "remove" && !i(e, s) && r(t, "delta.end must contained in document for 'remove' actions"); var o = s.row - n.row, u = s.column - (o == 0 ? n.column : 0); (o != t.lines.length - 1 || t.lines[o].length != u) && r(t, "delta.range must match delta lines") } t.applyDelta = function (e, t, n) { var r = t.start.row, i = t.start.column, s = e[r] || ""; switch (t.action) { case "insert": var o = t.lines; if (o.length === 1) e[r] = s.substring(0, i) + t.lines[0] + s.substring(i); else { var u = [r, 1].concat(t.lines); e.splice.apply(e, u), e[r] = s.substring(0, i) + e[r], e[r + t.lines.length - 1] += s.substring(i) } break; case "remove": var a = t.end.column, f = t.end.row; r === f ? e[r] = s.substring(0, i) + s.substring(a) : e.splice(r, f - r + 1, s.substring(0, i) + e[f].substring(a)) } } }), define("ace/anchor", ["require", "exports", "module", "ace/lib/oop", "ace/lib/event_emitter"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./lib/event_emitter").EventEmitter, s = t.Anchor = function (e, t, n) { this.$onChange = this.onChange.bind(this), this.attach(e), typeof n == "undefined" ? this.setPosition(t.row, t.column) : this.setPosition(t, n) }; (function () { function e(e, t, n) { var r = n ? e.column <= t.column : e.column < t.column; return e.row < t.row || e.row == t.row && r } function t(t, n, r) { var i = t.action == "insert", s = (i ? 1 : -1) * (t.end.row - t.start.row), o = (i ? 1 : -1) * (t.end.column - t.start.column), u = t.start, a = i ? u : t.end; return e(n, u, r) ? { row: n.row, column: n.column } : e(a, n, !r) ? { row: n.row + s, column: n.column + (n.row == a.row ? o : 0) } : { row: u.row, column: u.column } } r.implement(this, i), this.getPosition = function () { return this.$clipPositionToDocument(this.row, this.column) }, this.getDocument = function () { return this.document }, this.$insertRight = !1, this.onChange = function (e) { if (e.start.row == e.end.row && e.start.row != this.row) return; if (e.start.row > this.row) return; var n = t(e, { row: this.row, column: this.column }, this.$insertRight); this.setPosition(n.row, n.column, !0) }, this.setPosition = function (e, t, n) { var r; n ? r = { row: e, column: t } : r = this.$clipPositionToDocument(e, t); if (this.row == r.row && this.column == r.column) return; var i = { row: this.row, column: this.column }; this.row = r.row, this.column = r.column, this._signal("change", { old: i, value: r }) }, this.detach = function () { this.document.off("change", this.$onChange) }, this.attach = function (e) { this.document = e || this.document, this.document.on("change", this.$onChange) }, this.$clipPositionToDocument = function (e, t) { var n = {}; return e >= this.document.getLength() ? (n.row = Math.max(0, this.document.getLength() - 1), n.column = this.document.getLine(n.row).length) : e < 0 ? (n.row = 0, n.column = 0) : (n.row = e, n.column = Math.min(this.document.getLine(n.row).length, Math.max(0, t))), t < 0 && (n.column = 0), n } }).call(s.prototype) }), define("ace/document", ["require", "exports", "module", "ace/lib/oop", "ace/apply_delta", "ace/lib/event_emitter", "ace/range", "ace/anchor"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./apply_delta").applyDelta, s = e("./lib/event_emitter").EventEmitter, o = e("./range").Range, u = e("./anchor").Anchor, a = function (e) { this.$lines = [""], e.length === 0 ? this.$lines = [""] : Array.isArray(e) ? this.insertMergedLines({ row: 0, column: 0 }, e) : this.insert({ row: 0, column: 0 }, e) }; (function () { r.implement(this, s), this.setValue = function (e) { var t = this.getLength() - 1; this.remove(new o(0, 0, t, this.getLine(t).length)), this.insert({ row: 0, column: 0 }, e) }, this.getValue = function () { return this.getAllLines().join(this.getNewLineCharacter()) }, this.createAnchor = function (e, t) { return new u(this, e, t) }, "aaa".split(/a/).length === 0 ? this.$split = function (e) { return e.replace(/\r\n|\r/g, "\n").split("\n") } : this.$split = function (e) { return e.split(/\r\n|\r|\n/) }, this.$detectNewLine = function (e) { var t = e.match(/^.*?(\r\n|\r|\n)/m); this.$autoNewLine = t ? t[1] : "\n", this._signal("changeNewLineMode") }, this.getNewLineCharacter = function () { switch (this.$newLineMode) { case "windows": return "\r\n"; case "unix": return "\n"; default: return this.$autoNewLine || "\n" } }, this.$autoNewLine = "", this.$newLineMode = "auto", this.setNewLineMode = function (e) { if (this.$newLineMode === e) return; this.$newLineMode = e, this._signal("changeNewLineMode") }, this.getNewLineMode = function () { return this.$newLineMode }, this.isNewLine = function (e) { return e == "\r\n" || e == "\r" || e == "\n" }, this.getLine = function (e) { return this.$lines[e] || "" }, this.getLines = function (e, t) { return this.$lines.slice(e, t + 1) }, this.getAllLines = function () { return this.getLines(0, this.getLength()) }, this.getLength = function () { return this.$lines.length }, this.getTextRange = function (e) { return this.getLinesForRange(e).join(this.getNewLineCharacter()) }, this.getLinesForRange = function (e) { var t; if (e.start.row === e.end.row) t = [this.getLine(e.start.row).substring(e.start.column, e.end.column)]; else { t = this.getLines(e.start.row, e.end.row), t[0] = (t[0] || "").substring(e.start.column); var n = t.length - 1; e.end.row - e.start.row == n && (t[n] = t[n].substring(0, e.end.column)) } return t }, this.insertLines = function (e, t) { return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."), this.insertFullLines(e, t) }, this.removeLines = function (e, t) { return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."), this.removeFullLines(e, t) }, this.insertNewLine = function (e) { return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."), this.insertMergedLines(e, ["", ""]) }, this.insert = function (e, t) { return this.getLength() <= 1 && this.$detectNewLine(t), this.insertMergedLines(e, this.$split(t)) }, this.insertInLine = function (e, t) { var n = this.clippedPos(e.row, e.column), r = this.pos(e.row, e.column + t.length); return this.applyDelta({ start: n, end: r, action: "insert", lines: [t] }, !0), this.clonePos(r) }, this.clippedPos = function (e, t) { var n = this.getLength(); e === undefined ? e = n : e < 0 ? e = 0 : e >= n && (e = n - 1, t = undefined); var r = this.getLine(e); return t == undefined && (t = r.length), t = Math.min(Math.max(t, 0), r.length), { row: e, column: t } }, this.clonePos = function (e) { return { row: e.row, column: e.column } }, this.pos = function (e, t) { return { row: e, column: t } }, this.$clipPosition = function (e) { var t = this.getLength(); return e.row >= t ? (e.row = Math.max(0, t - 1), e.column = this.getLine(t - 1).length) : (e.row = Math.max(0, e.row), e.column = Math.min(Math.max(e.column, 0), this.getLine(e.row).length)), e }, this.insertFullLines = function (e, t) { e = Math.min(Math.max(e, 0), this.getLength()); var n = 0; e < this.getLength() ? (t = t.concat([""]), n = 0) : (t = [""].concat(t), e--, n = this.$lines[e].length), this.insertMergedLines({ row: e, column: n }, t) }, this.insertMergedLines = function (e, t) { var n = this.clippedPos(e.row, e.column), r = { row: n.row + t.length - 1, column: (t.length == 1 ? n.column : 0) + t[t.length - 1].length }; return this.applyDelta({ start: n, end: r, action: "insert", lines: t }), this.clonePos(r) }, this.remove = function (e) { var t = this.clippedPos(e.start.row, e.start.column), n = this.clippedPos(e.end.row, e.end.column); return this.applyDelta({ start: t, end: n, action: "remove", lines: this.getLinesForRange({ start: t, end: n }) }), this.clonePos(t) }, this.removeInLine = function (e, t, n) { var r = this.clippedPos(e, t), i = this.clippedPos(e, n); return this.applyDelta({ start: r, end: i, action: "remove", lines: this.getLinesForRange({ start: r, end: i }) }, !0), this.clonePos(r) }, this.removeFullLines = function (e, t) { e = Math.min(Math.max(0, e), this.getLength() - 1), t = Math.min(Math.max(0, t), this.getLength() - 1); var n = t == this.getLength() - 1 && e > 0, r = t < this.getLength() - 1, i = n ? e - 1 : e, s = n ? this.getLine(i).length : 0, u = r ? t + 1 : t, a = r ? 0 : this.getLine(u).length, f = new o(i, s, u, a), l = this.$lines.slice(e, t + 1); return this.applyDelta({ start: f.start, end: f.end, action: "remove", lines: this.getLinesForRange(f) }), l }, this.removeNewLine = function (e) { e < this.getLength() - 1 && e >= 0 && this.applyDelta({ start: this.pos(e, this.getLine(e).length), end: this.pos(e + 1, 0), action: "remove", lines: ["", ""] }) }, this.replace = function (e, t) { e instanceof o || (e = o.fromPoints(e.start, e.end)); if (t.length === 0 && e.isEmpty()) return e.start; if (t == this.getTextRange(e)) return e.end; this.remove(e); var n; return t ? n = this.insert(e.start, t) : n = e.start, n }, this.applyDeltas = function (e) { for (var t = 0; t < e.length; t++)this.applyDelta(e[t]) }, this.revertDeltas = function (e) { for (var t = e.length - 1; t >= 0; t--)this.revertDelta(e[t]) }, this.applyDelta = function (e, t) { var n = e.action == "insert"; if (n ? e.lines.length <= 1 && !e.lines[0] : !o.comparePoints(e.start, e.end)) return; n && e.lines.length > 2e4 ? this.$splitAndapplyLargeDelta(e, 2e4) : (i(this.$lines, e, t), this._signal("change", e)) }, this.$safeApplyDelta = function (e) { var t = this.$lines.length; (e.action == "remove" && e.start.row < t && e.end.row < t || e.action == "insert" && e.start.row <= t) && this.applyDelta(e) }, this.$splitAndapplyLargeDelta = function (e, t) { var n = e.lines, r = n.length - t + 1, i = e.start.row, s = e.start.column; for (var o = 0, u = 0; o < r; o = u) { u += t - 1; var a = n.slice(o, u); a.push(""), this.applyDelta({ start: this.pos(i + o, s), end: this.pos(i + u, s = 0), action: e.action, lines: a }, !0) } e.lines = n.slice(o), e.start.row = i + o, e.start.column = s, this.applyDelta(e, !0) }, this.revertDelta = function (e) { this.$safeApplyDelta({ start: this.clonePos(e.start), end: this.clonePos(e.end), action: e.action == "insert" ? "remove" : "insert", lines: e.lines.slice() }) }, this.indexToPosition = function (e, t) { var n = this.$lines || this.getAllLines(), r = this.getNewLineCharacter().length; for (var i = t || 0, s = n.length; i < s; i++) { e -= n[i].length + r; if (e < 0) return { row: i, column: e + n[i].length + r } } return { row: s - 1, column: e + n[s - 1].length + r } }, this.positionToIndex = function (e, t) { var n = this.$lines || this.getAllLines(), r = this.getNewLineCharacter().length, i = 0, s = Math.min(e.row, n.length); for (var o = t || 0; o < s; ++o)i += n[o].length + r; return i + e.column } }).call(a.prototype), t.Document = a }), define("ace/background_tokenizer", ["require", "exports", "module", "ace/lib/oop", "ace/lib/event_emitter"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./lib/event_emitter").EventEmitter, s = function (e, t) { this.running = !1, this.lines = [], this.states = [], this.currentLine = 0, this.tokenizer = e; var n = this; this.$worker = function () { if (!n.running) return; var e = new Date, t = n.currentLine, r = -1, i = n.doc, s = t; while (n.lines[t]) t++; var o = i.getLength(), u = 0; n.running = !1; while (t < o) { n.$tokenizeRow(t), r = t; do t++; while (n.lines[t]); u++; if (u % 5 === 0 && new Date - e > 20) { n.running = setTimeout(n.$worker, 20); break } } n.currentLine = t, r == -1 && (r = t), s <= r && n.fireUpdateEvent(s, r) } }; (function () { r.implement(this, i), this.setTokenizer = function (e) { this.tokenizer = e, this.lines = [], this.states = [], this.start(0) }, this.setDocument = function (e) { this.doc = e, this.lines = [], this.states = [], this.stop() }, this.fireUpdateEvent = function (e, t) { var n = { first: e, last: t }; this._signal("update", { data: n }) }, this.start = function (e) { this.currentLine = Math.min(e || 0, this.currentLine, this.doc.getLength()), this.lines.splice(this.currentLine, this.lines.length), this.states.splice(this.currentLine, this.states.length), this.stop(), this.running = setTimeout(this.$worker, 700) }, this.scheduleStart = function () { this.running || (this.running = setTimeout(this.$worker, 700)) }, this.$updateOnChange = function (e) { var t = e.start.row, n = e.end.row - t; if (n === 0) this.lines[t] = null; else if (e.action == "remove") this.lines.splice(t, n + 1, null), this.states.splice(t, n + 1, null); else { var r = Array(n + 1); r.unshift(t, 1), this.lines.splice.apply(this.lines, r), this.states.splice.apply(this.states, r) } this.currentLine = Math.min(t, this.currentLine, this.doc.getLength()), this.stop() }, this.stop = function () { this.running && clearTimeout(this.running), this.running = !1 }, this.getTokens = function (e) { return this.lines[e] || this.$tokenizeRow(e) }, this.getState = function (e) { return this.currentLine == e && this.$tokenizeRow(e), this.states[e] || "start" }, this.$tokenizeRow = function (e) { var t = this.doc.getLine(e), n = this.states[e - 1], r = this.tokenizer.getLineTokens(t, n, e); return this.states[e] + "" != r.state + "" ? (this.states[e] = r.state, this.lines[e + 1] = null, this.currentLine > e + 1 && (this.currentLine = e + 1)) : this.currentLine == e && (this.currentLine = e + 1), this.lines[e] = r.tokens } }).call(s.prototype), t.BackgroundTokenizer = s }), define("ace/search_highlight", ["require", "exports", "module", "ace/lib/lang", "ace/lib/oop", "ace/range"], function (e, t, n) { "use strict"; var r = e("./lib/lang"), i = e("./lib/oop"), s = e("./range").Range, o = function (e, t, n) { this.setRegexp(e), this.clazz = t, this.type = n || "text" }; (function () { this.MAX_RANGES = 500, this.setRegexp = function (e) { if (this.regExp + "" == e + "") return; this.regExp = e, this.cache = [] }, this.update = function (e, t, n, i) { if (!this.regExp) return; var o = i.firstRow, u = i.lastRow; for (var a = o; a <= u; a++) { var f = this.cache[a]; f == null && (f = r.getMatchOffsets(n.getLine(a), this.regExp), f.length > this.MAX_RANGES && (f = f.slice(0, this.MAX_RANGES)), f = f.map(function (e) { return new s(a, e.offset, a, e.offset + e.length) }), this.cache[a] = f.length ? f : ""); for (var l = f.length; l--;)t.drawSingleLineMarker(e, f[l].toScreenRange(n), this.clazz, i) } } }).call(o.prototype), t.SearchHighlight = o }), define("ace/edit_session/fold_line", ["require", "exports", "module", "ace/range"], function (e, t, n) { "use strict"; function i(e, t) { this.foldData = e, Array.isArray(t) ? this.folds = t : t = this.folds = [t]; var n = t[t.length - 1]; this.range = new r(t[0].start.row, t[0].start.column, n.end.row, n.end.column), this.start = this.range.start, this.end = this.range.end, this.folds.forEach(function (e) { e.setFoldLine(this) }, this) } var r = e("../range").Range; (function () { this.shiftRow = function (e) { this.start.row += e, this.end.row += e, this.folds.forEach(function (t) { t.start.row += e, t.end.row += e }) }, this.addFold = function (e) { if (e.sameRow) { if (e.start.row < this.startRow || e.endRow > this.endRow) throw new Error("Can't add a fold to this FoldLine as it has no connection"); this.folds.push(e), this.folds.sort(function (e, t) { return -e.range.compareEnd(t.start.row, t.start.column) }), this.range.compareEnd(e.start.row, e.start.column) > 0 ? (this.end.row = e.end.row, this.end.column = e.end.column) : this.range.compareStart(e.end.row, e.end.column) < 0 && (this.start.row = e.start.row, this.start.column = e.start.column) } else if (e.start.row == this.end.row) this.folds.push(e), this.end.row = e.end.row, this.end.column = e.end.column; else { if (e.end.row != this.start.row) throw new Error("Trying to add fold to FoldRow that doesn't have a matching row"); this.folds.unshift(e), this.start.row = e.start.row, this.start.column = e.start.column } e.foldLine = this }, this.containsRow = function (e) { return e >= this.start.row && e <= this.end.row }, this.walk = function (e, t, n) { var r = 0, i = this.folds, s, o, u, a = !0; t == null && (t = this.end.row, n = this.end.column); for (var f = 0; f < i.length; f++) { s = i[f], o = s.range.compareStart(t, n); if (o == -1) { e(null, t, n, r, a); return } u = e(null, s.start.row, s.start.column, r, a), u = !u && e(s.placeholder, s.start.row, s.start.column, r); if (u || o === 0) return; a = !s.sameRow, r = s.end.column } e(null, t, n, r, a) }, this.getNextFoldTo = function (e, t) { var n, r; for (var i = 0; i < this.folds.length; i++) { n = this.folds[i], r = n.range.compareEnd(e, t); if (r == -1) return { fold: n, kind: "after" }; if (r === 0) return { fold: n, kind: "inside" } } return null }, this.addRemoveChars = function (e, t, n) { var r = this.getNextFoldTo(e, t), i, s; if (r) { i = r.fold; if (r.kind == "inside" && i.start.column != t && i.start.row != e) window.console && window.console.log(e, t, i); else if (i.start.row == e) { s = this.folds; var o = s.indexOf(i); o === 0 && (this.start.column += n); for (o; o < s.length; o++) { i = s[o], i.start.column += n; if (!i.sameRow) return; i.end.column += n } this.end.column += n } } }, this.split = function (e, t) { var n = this.getNextFoldTo(e, t); if (!n || n.kind == "inside") return null; var r = n.fold, s = this.folds, o = this.foldData, u = s.indexOf(r), a = s[u - 1]; this.end.row = a.end.row, this.end.column = a.end.column, s = s.splice(u, s.length - u); var f = new i(o, s); return o.splice(o.indexOf(this) + 1, 0, f), f }, this.merge = function (e) { var t = e.folds; for (var n = 0; n < t.length; n++)this.addFold(t[n]); var r = this.foldData; r.splice(r.indexOf(e), 1) }, this.toString = function () { var e = [this.range.toString() + ": ["]; return this.folds.forEach(function (t) { e.push(" " + t.toString()) }), e.push("]"), e.join("\n") }, this.idxToPosition = function (e) { var t = 0; for (var n = 0; n < this.folds.length; n++) { var r = this.folds[n]; e -= r.start.column - t; if (e < 0) return { row: r.start.row, column: r.start.column + e }; e -= r.placeholder.length; if (e < 0) return r.start; t = r.end.column } return { row: this.end.row, column: this.end.column + e } } }).call(i.prototype), t.FoldLine = i }), define("ace/range_list", ["require", "exports", "module", "ace/range"], function (e, t, n) { "use strict"; var r = e("./range").Range, i = r.comparePoints, s = function () { this.ranges = [], this.$bias = 1 }; (function () { this.comparePoints = i, this.pointIndex = function (e, t, n) { var r = this.ranges; for (var s = n || 0; s < r.length; s++) { var o = r[s], u = i(e, o.end); if (u > 0) continue; var a = i(e, o.start); return u === 0 ? t && a !== 0 ? -s - 2 : s : a > 0 || a === 0 && !t ? s : -s - 1 } return -s - 1 }, this.add = function (e) { var t = !e.isEmpty(), n = this.pointIndex(e.start, t); n < 0 && (n = -n - 1); var r = this.pointIndex(e.end, t, n); return r < 0 ? r = -r - 1 : r++, this.ranges.splice(n, r - n, e) }, this.addList = function (e) { var t = []; for (var n = e.length; n--;)t.push.apply(t, this.add(e[n])); return t }, this.substractPoint = function (e) { var t = this.pointIndex(e); if (t >= 0) return this.ranges.splice(t, 1) }, this.merge = function () { var e = [], t = this.ranges; t = t.sort(function (e, t) { return i(e.start, t.start) }); var n = t[0], r; for (var s = 1; s < t.length; s++) { r = n, n = t[s]; var o = i(r.end, n.start); if (o < 0) continue; if (o == 0 && !r.isEmpty() && !n.isEmpty()) continue; i(r.end, n.end) < 0 && (r.end.row = n.end.row, r.end.column = n.end.column), t.splice(s, 1), e.push(n), n = r, s-- } return this.ranges = t, e }, this.contains = function (e, t) { return this.pointIndex({ row: e, column: t }) >= 0 }, this.containsPoint = function (e) { return this.pointIndex(e) >= 0 }, this.rangeAtPoint = function (e) { var t = this.pointIndex(e); if (t >= 0) return this.ranges[t] }, this.clipRows = function (e, t) { var n = this.ranges; if (n[0].start.row > t || n[n.length - 1].start.row < e) return []; var r = this.pointIndex({ row: e, column: 0 }); r < 0 && (r = -r - 1); var i = this.pointIndex({ row: t, column: 0 }, r); i < 0 && (i = -i - 1); var s = []; for (var o = r; o < i; o++)s.push(n[o]); return s }, this.removeAll = function () { return this.ranges.splice(0, this.ranges.length) }, this.attach = function (e) { this.session && this.detach(), this.session = e, this.onChange = this.$onChange.bind(this), this.session.on("change", this.onChange) }, this.detach = function () { if (!this.session) return; this.session.removeListener("change", this.onChange), this.session = null }, this.$onChange = function (e) { var t = e.start, n = e.end, r = t.row, i = n.row, s = this.ranges; for (var o = 0, u = s.length; o < u; o++) { var a = s[o]; if (a.end.row >= r) break } if (e.action == "insert") { var f = i - r, l = -t.column + n.column; for (; o < u; o++) { var a = s[o]; if (a.start.row > r) break; a.start.row == r && a.start.column >= t.column && (a.start.column == t.column && this.$bias <= 0 || (a.start.column += l, a.start.row += f)); if (a.end.row == r && a.end.column >= t.column) { if (a.end.column == t.column && this.$bias < 0) continue; a.end.column == t.column && l > 0 && o < u - 1 && a.end.column > a.start.column && a.end.column == s[o + 1].start.column && (a.end.column -= l), a.end.column += l, a.end.row += f } } } else { var f = r - i, l = t.column - n.column; for (; o < u; o++) { var a = s[o]; if (a.start.row > i) break; if (a.end.row < i && (r < a.end.row || r == a.end.row && t.column < a.end.column)) a.end.row = r, a.end.column = t.column; else if (a.end.row == i) if (a.end.column <= n.column) { if (f || a.end.column > t.column) a.end.column = t.column, a.end.row = t.row } else a.end.column += l, a.end.row += f; else a.end.row > i && (a.end.row += f); if (a.start.row < i && (r < a.start.row || r == a.start.row && t.column < a.start.column)) a.start.row = r, a.start.column = t.column; else if (a.start.row == i) if (a.start.column <= n.column) { if (f || a.start.column > t.column) a.start.column = t.column, a.start.row = t.row } else a.start.column += l, a.start.row += f; else a.start.row > i && (a.start.row += f) } } if (f != 0 && o < u) for (; o < u; o++) { var a = s[o]; a.start.row += f, a.end.row += f } } }).call(s.prototype), t.RangeList = s }), define("ace/edit_session/fold", ["require", "exports", "module", "ace/range_list", "ace/lib/oop"], function (e, t, n) { "use strict"; function o(e, t) { e.row -= t.row, e.row == 0 && (e.column -= t.column) } function u(e, t) { o(e.start, t), o(e.end, t) } function a(e, t) { e.row == 0 && (e.column += t.column), e.row += t.row } function f(e, t) { a(e.start, t), a(e.end, t) } var r = e("../range_list").RangeList, i = e("../lib/oop"), s = t.Fold = function (e, t) { this.foldLine = null, this.placeholder = t, this.range = e, this.start = e.start, this.end = e.end, this.sameRow = e.start.row == e.end.row, this.subFolds = this.ranges = [] }; i.inherits(s, r), function () { this.toString = function () { return '"' + this.placeholder + '" ' + this.range.toString() }, this.setFoldLine = function (e) { this.foldLine = e, this.subFolds.forEach(function (t) { t.setFoldLine(e) }) }, this.clone = function () { var e = this.range.clone(), t = new s(e, this.placeholder); return this.subFolds.forEach(function (e) { t.subFolds.push(e.clone()) }), t.collapseChildren = this.collapseChildren, t }, this.addSubFold = function (e) { if (this.range.isEqual(e)) return; u(e, this.start); var t = e.start.row, n = e.start.column; for (var r = 0, i = -1; r < this.subFolds.length; r++) { i = this.subFolds[r].range.compare(t, n); if (i != 1) break } var s = this.subFolds[r], o = 0; if (i == 0) { if (s.range.containsRange(e)) return s.addSubFold(e); o = 1 } var t = e.range.end.row, n = e.range.end.column; for (var a = r, i = -1; a < this.subFolds.length; a++) { i = this.subFolds[a].range.compare(t, n); if (i != 1) break } i == 0 && a++; var f = this.subFolds.splice(r, a - r, e), l = i == 0 ? f.length - 1 : f.length; for (var c = o; c < l; c++)e.addSubFold(f[c]); return e.setFoldLine(this.foldLine), e }, this.restoreRange = function (e) { return f(e, this.start) } }.call(s.prototype) }), define("ace/edit_session/folding", ["require", "exports", "module", "ace/range", "ace/edit_session/fold_line", "ace/edit_session/fold", "ace/token_iterator"], function (e, t, n) { "use strict"; function u() { this.getFoldAt = function (e, t, n) { var r = this.getFoldLine(e); if (!r) return null; var i = r.folds; for (var s = 0; s < i.length; s++) { var o = i[s].range; if (o.contains(e, t)) { if (n == 1 && o.isEnd(e, t) && !o.isEmpty()) continue; if (n == -1 && o.isStart(e, t) && !o.isEmpty()) continue; return i[s] } } }, this.getFoldsInRange = function (e) { var t = e.start, n = e.end, r = this.$foldData, i = []; t.column += 1, n.column -= 1; for (var s = 0; s < r.length; s++) { var o = r[s].range.compareRange(e); if (o == 2) continue; if (o == -2) break; var u = r[s].folds; for (var a = 0; a < u.length; a++) { var f = u[a]; o = f.range.compareRange(e); if (o == -2) break; if (o == 2) continue; if (o == 42) break; i.push(f) } } return t.column -= 1, n.column += 1, i }, this.getFoldsInRangeList = function (e) { if (Array.isArray(e)) { var t = []; e.forEach(function (e) { t = t.concat(this.getFoldsInRange(e)) }, this) } else var t = this.getFoldsInRange(e); return t }, this.getAllFolds = function () { var e = [], t = this.$foldData; for (var n = 0; n < t.length; n++)for (var r = 0; r < t[n].folds.length; r++)e.push(t[n].folds[r]); return e }, this.getFoldStringAt = function (e, t, n, r) { r = r || this.getFoldLine(e); if (!r) return null; var i = { end: { column: 0 } }, s, o; for (var u = 0; u < r.folds.length; u++) { o = r.folds[u]; var a = o.range.compareEnd(e, t); if (a == -1) { s = this.getLine(o.start.row).substring(i.end.column, o.start.column); break } if (a === 0) return null; i = o } return s || (s = this.getLine(o.start.row).substring(i.end.column)), n == -1 ? s.substring(0, t - i.end.column) : n == 1 ? s.substring(t - i.end.column) : s }, this.getFoldLine = function (e, t) { var n = this.$foldData, r = 0; t && (r = n.indexOf(t)), r == -1 && (r = 0); for (r; r < n.length; r++) { var i = n[r]; if (i.start.row <= e && i.end.row >= e) return i; if (i.end.row > e) return null } return null }, this.getNextFoldLine = function (e, t) { var n = this.$foldData, r = 0; t && (r = n.indexOf(t)), r == -1 && (r = 0); for (r; r < n.length; r++) { var i = n[r]; if (i.end.row >= e) return i } return null }, this.getFoldedRowCount = function (e, t) { var n = this.$foldData, r = t - e + 1; for (var i = 0; i < n.length; i++) { var s = n[i], o = s.end.row, u = s.start.row; if (o >= t) { u < t && (u >= e ? r -= t - u : r = 0); break } o >= e && (u >= e ? r -= o - u : r -= o - e + 1) } return r }, this.$addFoldLine = function (e) { return this.$foldData.push(e), this.$foldData.sort(function (e, t) { return e.start.row - t.start.row }), e }, this.addFold = function (e, t) { var n = this.$foldData, r = !1, o; e instanceof s ? o = e : (o = new s(t, e), o.collapseChildren = t.collapseChildren), this.$clipRangeToDocument(o.range); var u = o.start.row, a = o.start.column, f = o.end.row, l = o.end.column, c = this.getFoldAt(u, a, 1), h = this.getFoldAt(f, l, -1); if (c && h == c) return c.addSubFold(o); c && !c.range.isStart(u, a) && this.removeFold(c), h && !h.range.isEnd(f, l) && this.removeFold(h); var p = this.getFoldsInRange(o.range); p.length > 0 && (this.removeFolds(p), p.forEach(function (e) { o.addSubFold(e) })); for (var d = 0; d < n.length; d++) { var v = n[d]; if (f == v.start.row) { v.addFold(o), r = !0; break } if (u == v.end.row) { v.addFold(o), r = !0; if (!o.sameRow) { var m = n[d + 1]; if (m && m.start.row == f) { v.merge(m); break } } break } if (f <= v.start.row) break } return r || (v = this.$addFoldLine(new i(this.$foldData, o))), this.$useWrapMode ? this.$updateWrapData(v.start.row, v.start.row) : this.$updateRowLengthCache(v.start.row, v.start.row), this.$modified = !0, this._signal("changeFold", { data: o, action: "add" }), o }, this.addFolds = function (e) { e.forEach(function (e) { this.addFold(e) }, this) }, this.removeFold = function (e) { var t = e.foldLine, n = t.start.row, r = t.end.row, i = this.$foldData, s = t.folds; if (s.length == 1) i.splice(i.indexOf(t), 1); else if (t.range.isEnd(e.end.row, e.end.column)) s.pop(), t.end.row = s[s.length - 1].end.row, t.end.column = s[s.length - 1].end.column; else if (t.range.isStart(e.start.row, e.start.column)) s.shift(), t.start.row = s[0].start.row, t.start.column = s[0].start.column; else if (e.sameRow) s.splice(s.indexOf(e), 1); else { var o = t.split(e.start.row, e.start.column); s = o.folds, s.shift(), o.start.row = s[0].start.row, o.start.column = s[0].start.column } this.$updating || (this.$useWrapMode ? this.$updateWrapData(n, r) : this.$updateRowLengthCache(n, r)), this.$modified = !0, this._signal("changeFold", { data: e, action: "remove" }) }, this.removeFolds = function (e) { var t = []; for (var n = 0; n < e.length; n++)t.push(e[n]); t.forEach(function (e) { this.removeFold(e) }, this), this.$modified = !0 }, this.expandFold = function (e) { this.removeFold(e), e.subFolds.forEach(function (t) { e.restoreRange(t), this.addFold(t) }, this), e.collapseChildren > 0 && this.foldAll(e.start.row + 1, e.end.row, e.collapseChildren - 1), e.subFolds = [] }, this.expandFolds = function (e) { e.forEach(function (e) { this.expandFold(e) }, this) }, this.unfold = function (e, t) { var n, i; e == null ? (n = new r(0, 0, this.getLength(), 0), t = !0) : typeof e == "number" ? n = new r(e, 0, e, this.getLine(e).length) : "row" in e ? n = r.fromPoints(e, e) : n = e, i = this.getFoldsInRangeList(n); if (t) this.removeFolds(i); else { var s = i; while (s.length) this.expandFolds(s), s = this.getFoldsInRangeList(n) } if (i.length) return i }, this.isRowFolded = function (e, t) { return !!this.getFoldLine(e, t) }, this.getRowFoldEnd = function (e, t) { var n = this.getFoldLine(e, t); return n ? n.end.row : e }, this.getRowFoldStart = function (e, t) { var n = this.getFoldLine(e, t); return n ? n.start.row : e }, this.getFoldDisplayLine = function (e, t, n, r, i) { r == null && (r = e.start.row), i == null && (i = 0), t == null && (t = e.end.row), n == null && (n = this.getLine(t).length); var s = this.doc, o = ""; return e.walk(function (e, t, n, u) { if (t < r) return; if (t == r) { if (n < i) return; u = Math.max(i, u) } e != null ? o += e : o += s.getLine(t).substring(u, n) }, t, n), o }, this.getDisplayLine = function (e, t, n, r) { var i = this.getFoldLine(e); if (!i) { var s; return s = this.doc.getLine(e), s.substring(r || 0, t || s.length) } return this.getFoldDisplayLine(i, e, t, n, r) }, this.$cloneFoldData = function () { var e = []; return e = this.$foldData.map(function (t) { var n = t.folds.map(function (e) { return e.clone() }); return new i(e, n) }), e }, this.toggleFold = function (e) { var t = this.selection, n = t.getRange(), r, i; if (n.isEmpty()) { var s = n.start; r = this.getFoldAt(s.row, s.column); if (r) { this.expandFold(r); return } (i = this.findMatchingBracket(s)) ? n.comparePoint(i) == 1 ? n.end = i : (n.start = i, n.start.column++, n.end.column--) : (i = this.findMatchingBracket({ row: s.row, column: s.column + 1 })) ? (n.comparePoint(i) == 1 ? n.end = i : n.start = i, n.start.column++) : n = this.getCommentFoldRange(s.row, s.column) || n } else { var o = this.getFoldsInRange(n); if (e && o.length) { this.expandFolds(o); return } o.length == 1 && (r = o[0]) } r || (r = this.getFoldAt(n.start.row, n.start.column)); if (r && r.range.toString() == n.toString()) { this.expandFold(r); return } var u = "..."; if (!n.isMultiLine()) { u = this.getTextRange(n); if (u.length < 4) return; u = u.trim().substring(0, 2) + ".." } this.addFold(u, n) }, this.getCommentFoldRange = function (e, t, n) { var i = new o(this, e, t), s = i.getCurrentToken(), u = s.type; if (s && /^comment|string/.test(u)) { u = u.match(/comment|string/)[0], u == "comment" && (u += "|doc-start"); var a = new RegExp(u), f = new r; if (n != 1) { do s = i.stepBackward(); while (s && a.test(s.type)); i.stepForward() } f.start.row = i.getCurrentTokenRow(), f.start.column = i.getCurrentTokenColumn() + 2, i = new o(this, e, t); if (n != -1) { var l = -1; do { s = i.stepForward(); if (l == -1) { var c = this.getState(i.$row); a.test(c) || (l = i.$row) } else if (i.$row > l) break } while (s && a.test(s.type)); s = i.stepBackward() } else s = i.getCurrentToken(); return f.end.row = i.getCurrentTokenRow(), f.end.column = i.getCurrentTokenColumn() + s.value.length - 2, f } }, this.foldAll = function (e, t, n) { n == undefined && (n = 1e5); var r = this.foldWidgets; if (!r) return; t = t || this.getLength(), e = e || 0; for (var i = e; i < t; i++) { r[i] == null && (r[i] = this.getFoldWidget(i)); if (r[i] != "start") continue; var s = this.getFoldWidgetRange(i); if (s && s.isMultiLine() && s.end.row <= t && s.start.row >= e) { i = s.end.row; try { var o = this.addFold("...", s); o && (o.collapseChildren = n) } catch (u) { } } } }, this.$foldStyles = { manual: 1, markbegin: 1, markbeginend: 1 }, this.$foldStyle = "markbegin", this.setFoldStyle = function (e) { if (!this.$foldStyles[e]) throw new Error("invalid fold style: " + e + "[" + Object.keys(this.$foldStyles).join(", ") + "]"); if (this.$foldStyle == e) return; this.$foldStyle = e, e == "manual" && this.unfold(); var t = this.$foldMode; this.$setFolding(null), this.$setFolding(t) }, this.$setFolding = function (e) { if (this.$foldMode == e) return; this.$foldMode = e, this.off("change", this.$updateFoldWidgets), this.off("tokenizerUpdate", this.$tokenizerUpdateFoldWidgets), this._signal("changeAnnotation"); if (!e || this.$foldStyle == "manual") { this.foldWidgets = null; return } this.foldWidgets = [], this.getFoldWidget = e.getFoldWidget.bind(e, this, this.$foldStyle), this.getFoldWidgetRange = e.getFoldWidgetRange.bind(e, this, this.$foldStyle), this.$updateFoldWidgets = this.updateFoldWidgets.bind(this), this.$tokenizerUpdateFoldWidgets = this.tokenizerUpdateFoldWidgets.bind(this), this.on("change", this.$updateFoldWidgets), this.on("tokenizerUpdate", this.$tokenizerUpdateFoldWidgets) }, this.getParentFoldRangeData = function (e, t) { var n = this.foldWidgets; if (!n || t && n[e]) return {}; var r = e - 1, i; while (r >= 0) { var s = n[r]; s == null && (s = n[r] = this.getFoldWidget(r)); if (s == "start") { var o = this.getFoldWidgetRange(r); i || (i = o); if (o && o.end.row >= e) break } r-- } return { range: r !== -1 && o, firstRange: i } }, this.onFoldWidgetClick = function (e, t) { t = t.domEvent; var n = { children: t.shiftKey, all: t.ctrlKey || t.metaKey, siblings: t.altKey }, r = this.$toggleFoldWidget(e, n); if (!r) { var i = t.target || t.srcElement; i && /ace_fold-widget/.test(i.className) && (i.className += " ace_invalid") } }, this.$toggleFoldWidget = function (e, t) { if (!this.getFoldWidget) return; var n = this.getFoldWidget(e), r = this.getLine(e), i = n === "end" ? -1 : 1, s = this.getFoldAt(e, i === -1 ? 0 : r.length, i); if (s) return t.children || t.all ? this.removeFold(s) : this.expandFold(s), s; var o = this.getFoldWidgetRange(e, !0); if (o && !o.isMultiLine()) { s = this.getFoldAt(o.start.row, o.start.column, 1); if (s && o.isEqual(s.range)) return this.removeFold(s), s } if (t.siblings) { var u = this.getParentFoldRangeData(e); if (u.range) var a = u.range.start.row + 1, f = u.range.end.row; this.foldAll(a, f, t.all ? 1e4 : 0) } else t.children ? (f = o ? o.end.row : this.getLength(), this.foldAll(e + 1, f, t.all ? 1e4 : 0)) : o && (t.all && (o.collapseChildren = 1e4), this.addFold("...", o)); return o }, this.toggleFoldWidget = function (e) { var t = this.selection.getCursor().row; t = this.getRowFoldStart(t); var n = this.$toggleFoldWidget(t, {}); if (n) return; var r = this.getParentFoldRangeData(t, !0); n = r.range || r.firstRange; if (n) { t = n.start.row; var i = this.getFoldAt(t, this.getLine(t).length, 1); i ? this.removeFold(i) : this.addFold("...", n) } }, this.updateFoldWidgets = function (e) { var t = e.start.row, n = e.end.row - t; if (n === 0) this.foldWidgets[t] = null; else if (e.action == "remove") this.foldWidgets.splice(t, n + 1, null); else { var r = Array(n + 1); r.unshift(t, 1), this.foldWidgets.splice.apply(this.foldWidgets, r) } }, this.tokenizerUpdateFoldWidgets = function (e) { var t = e.data; t.first != t.last && this.foldWidgets.length > t.first && this.foldWidgets.splice(t.first, this.foldWidgets.length) } } var r = e("../range").Range, i = e("./fold_line").FoldLine, s = e("./fold").Fold, o = e("../token_iterator").TokenIterator; t.Folding = u }), define("ace/edit_session/bracket_match", ["require", "exports", "module", "ace/token_iterator", "ace/range"], function (e, t, n) { "use strict"; function s() { this.findMatchingBracket = function (e, t) { if (e.column == 0) return null; var n = t || this.getLine(e.row).charAt(e.column - 1); if (n == "") return null; var r = n.match(/([\(\[\{])|([\)\]\}])/); return r ? r[1] ? this.$findClosingBracket(r[1], e) : this.$findOpeningBracket(r[2], e) : null }, this.getBracketRange = function (e) { var t = this.getLine(e.row), n = !0, r, s = t.charAt(e.column - 1), o = s && s.match(/([\(\[\{])|([\)\]\}])/); o || (s = t.charAt(e.column), e = { row: e.row, column: e.column + 1 }, o = s && s.match(/([\(\[\{])|([\)\]\}])/), n = !1); if (!o) return null; if (o[1]) { var u = this.$findClosingBracket(o[1], e); if (!u) return null; r = i.fromPoints(e, u), n || (r.end.column++, r.start.column--), r.cursor = r.end } else { var u = this.$findOpeningBracket(o[2], e); if (!u) return null; r = i.fromPoints(u, e), n || (r.start.column++, r.end.column--), r.cursor = r.start } return r }, this.getMatchingBracketRanges = function (e) { var t = this.getLine(e.row), n = t.charAt(e.column - 1), r = n && n.match(/([\(\[\{])|([\)\]\}])/); r || (n = t.charAt(e.column), e = { row: e.row, column: e.column + 1 }, r = n && n.match(/([\(\[\{])|([\)\]\}])/)); if (!r) return null; var s = new i(e.row, e.column - 1, e.row, e.column), o = r[1] ? this.$findClosingBracket(r[1], e) : this.$findOpeningBracket(r[2], e); if (!o) return [s]; var u = new i(o.row, o.column, o.row, o.column + 1); return [s, u] }, this.$brackets = { ")": "(", "(": ")", "]": "[", "[": "]", "{": "}", "}": "{", "<": ">", ">": "<" }, this.$findOpeningBracket = function (e, t, n) { var i = this.$brackets[e], s = 1, o = new r(this, t.row, t.column), u = o.getCurrentToken(); u || (u = o.stepForward()); if (!u) return; n || (n = new RegExp("(\\.?" + u.type.replace(".", "\\.").replace("rparen", ".paren").replace(/\b(?:end)\b/, "(?:start|begin|end)") + ")+")); var a = t.column - o.getCurrentTokenColumn() - 2, f = u.value; for (; ;) { while (a >= 0) { var l = f.charAt(a); if (l == i) { s -= 1; if (s == 0) return { row: o.getCurrentTokenRow(), column: a + o.getCurrentTokenColumn() } } else l == e && (s += 1); a -= 1 } do u = o.stepBackward(); while (u && !n.test(u.type)); if (u == null) break; f = u.value, a = f.length - 1 } return null }, this.$findClosingBracket = function (e, t, n) { var i = this.$brackets[e], s = 1, o = new r(this, t.row, t.column), u = o.getCurrentToken(); u || (u = o.stepForward()); if (!u) return; n || (n = new RegExp("(\\.?" + u.type.replace(".", "\\.").replace("lparen", ".paren").replace(/\b(?:start|begin)\b/, "(?:start|begin|end)") + ")+")); var a = t.column - o.getCurrentTokenColumn(); for (; ;) { var f = u.value, l = f.length; while (a < l) { var c = f.charAt(a); if (c == i) { s -= 1; if (s == 0) return { row: o.getCurrentTokenRow(), column: a + o.getCurrentTokenColumn() } } else c == e && (s += 1); a += 1 } do u = o.stepForward(); while (u && !n.test(u.type)); if (u == null) break; a = 0 } return null } } var r = e("../token_iterator").TokenIterator, i = e("../range").Range; t.BracketMatch = s }), define("ace/edit_session", ["require", "exports", "module", "ace/lib/oop", "ace/lib/lang", "ace/bidihandler", "ace/config", "ace/lib/event_emitter", "ace/selection", "ace/mode/text", "ace/range", "ace/document", "ace/background_tokenizer", "ace/search_highlight", "ace/edit_session/folding", "ace/edit_session/bracket_match"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./lib/lang"), s = e("./bidihandler").BidiHandler, o = e("./config"), u = e("./lib/event_emitter").EventEmitter, a = e("./selection").Selection, f = e("./mode/text").Mode, l = e("./range").Range, c = e("./document").Document, h = e("./background_tokenizer").BackgroundTokenizer, p = e("./search_highlight").SearchHighlight, d = function (e, t) { this.$breakpoints = [], this.$decorations = [], this.$frontMarkers = {}, this.$backMarkers = {}, this.$markerId = 1, this.$undoSelect = !0, this.$foldData = [], this.id = "session" + ++d.$uid, this.$foldData.toString = function () { return this.join("\n") }, this.on("changeFold", this.onChangeFold.bind(this)), this.$onChange = this.onChange.bind(this); if (typeof e != "object" || !e.getLine) e = new c(e); this.setDocument(e), this.selection = new a(this), this.$bidiHandler = new s(this), o.resetOptions(this), this.setMode(t), o._signal("session", this) }; d.$uid = 0, function () { function m(e) { return e < 4352 ? !1 : e >= 4352 && e <= 4447 || e >= 4515 && e <= 4519 || e >= 4602 && e <= 4607 || e >= 9001 && e <= 9002 || e >= 11904 && e <= 11929 || e >= 11931 && e <= 12019 || e >= 12032 && e <= 12245 || e >= 12272 && e <= 12283 || e >= 12288 && e <= 12350 || e >= 12353 && e <= 12438 || e >= 12441 && e <= 12543 || e >= 12549 && e <= 12589 || e >= 12593 && e <= 12686 || e >= 12688 && e <= 12730 || e >= 12736 && e <= 12771 || e >= 12784 && e <= 12830 || e >= 12832 && e <= 12871 || e >= 12880 && e <= 13054 || e >= 13056 && e <= 19903 || e >= 19968 && e <= 42124 || e >= 42128 && e <= 42182 || e >= 43360 && e <= 43388 || e >= 44032 && e <= 55203 || e >= 55216 && e <= 55238 || e >= 55243 && e <= 55291 || e >= 63744 && e <= 64255 || e >= 65040 && e <= 65049 || e >= 65072 && e <= 65106 || e >= 65108 && e <= 65126 || e >= 65128 && e <= 65131 || e >= 65281 && e <= 65376 || e >= 65504 && e <= 65510 } r.implement(this, u), this.setDocument = function (e) { this.doc && this.doc.removeListener("change", this.$onChange), this.doc = e, e.on("change", this.$onChange), this.bgTokenizer && this.bgTokenizer.setDocument(this.getDocument()), this.resetCaches() }, this.getDocument = function () { return this.doc }, this.$resetRowCache = function (e) { if (!e) { this.$docRowCache = [], this.$screenRowCache = []; return } var t = this.$docRowCache.length, n = this.$getRowCacheIndex(this.$docRowCache, e) + 1; t > n && (this.$docRowCache.splice(n, t), this.$screenRowCache.splice(n, t)) }, this.$getRowCacheIndex = function (e, t) { var n = 0, r = e.length - 1; while (n <= r) { var i = n + r >> 1, s = e[i]; if (t > s) n = i + 1; else { if (!(t < s)) return i; r = i - 1 } } return n - 1 }, this.resetCaches = function () { this.$modified = !0, this.$wrapData = [], this.$rowLengthCache = [], this.$resetRowCache(0), this.bgTokenizer && this.bgTokenizer.start(0) }, this.onChangeFold = function (e) { var t = e.data; this.$resetRowCache(t.start.row) }, this.onChange = function (e) { this.$modified = !0, this.$bidiHandler.onChange(e), this.$resetRowCache(e.start.row); var t = this.$updateInternalDataOnChange(e); !this.$fromUndo && this.$undoManager && (t && t.length && (this.$undoManager.add({ action: "removeFolds", folds: t }, this.mergeUndoDeltas), this.mergeUndoDeltas = !0), this.$undoManager.add(e, this.mergeUndoDeltas), this.mergeUndoDeltas = !0, this.$informUndoManager.schedule()), this.bgTokenizer && this.bgTokenizer.$updateOnChange(e), this._signal("change", e) }, this.setValue = function (e) { this.doc.setValue(e), this.selection.moveTo(0, 0), this.$resetRowCache(0), this.setUndoManager(this.$undoManager), this.getUndoManager().reset() }, this.getValue = this.toString = function () { return this.doc.getValue() }, this.getSelection = function () { return this.selection }, this.getState = function (e) { return this.bgTokenizer.getState(e) }, this.getTokens = function (e) { return this.bgTokenizer.getTokens(e) }, this.getTokenAt = function (e, t) { var n = this.bgTokenizer.getTokens(e), r, i = 0; if (t == null) { var s = n.length - 1; i = this.getLine(e).length } else for (var s = 0; s < n.length; s++) { i += n[s].value.length; if (i >= t) break } return r = n[s], r ? (r.index = s, r.start = i - r.value.length, r) : null }, this.setUndoManager = function (e) { this.$undoManager = e, this.$informUndoManager && this.$informUndoManager.cancel(); if (e) { var t = this; e.addSession(this), this.$syncInformUndoManager = function () { t.$informUndoManager.cancel(), t.mergeUndoDeltas = !1 }, this.$informUndoManager = i.delayedCall(this.$syncInformUndoManager) } else this.$syncInformUndoManager = function () { } }, this.markUndoGroup = function () { this.$syncInformUndoManager && this.$syncInformUndoManager() }, this.$defaultUndoManager = { undo: function () { }, redo: function () { }, hasUndo: function () { }, hasRedo: function () { }, reset: function () { }, add: function () { }, addSelection: function () { }, startNewGroup: function () { }, addSession: function () { } }, this.getUndoManager = function () { return this.$undoManager || this.$defaultUndoManager }, this.getTabString = function () { return this.getUseSoftTabs() ? i.stringRepeat(" ", this.getTabSize()) : " " }, this.setUseSoftTabs = function (e) { this.setOption("useSoftTabs", e) }, this.getUseSoftTabs = function () { return this.$useSoftTabs && !this.$mode.$indentWithTabs }, this.setTabSize = function (e) { this.setOption("tabSize", e) }, this.getTabSize = function () { return this.$tabSize }, this.isTabStop = function (e) { return this.$useSoftTabs && e.column % this.$tabSize === 0 }, this.setNavigateWithinSoftTabs = function (e) { this.setOption("navigateWithinSoftTabs", e) }, this.getNavigateWithinSoftTabs = function () { return this.$navigateWithinSoftTabs }, this.$overwrite = !1, this.setOverwrite = function (e) { this.setOption("overwrite", e) }, this.getOverwrite = function () { return this.$overwrite }, this.toggleOverwrite = function () { this.setOverwrite(!this.$overwrite) }, this.addGutterDecoration = function (e, t) { this.$decorations[e] || (this.$decorations[e] = ""), this.$decorations[e] += " " + t, this._signal("changeBreakpoint", {}) }, this.removeGutterDecoration = function (e, t) { this.$decorations[e] = (this.$decorations[e] || "").replace(" " + t, ""), this._signal("changeBreakpoint", {}) }, this.getBreakpoints = function () { return this.$breakpoints }, this.setBreakpoints = function (e) { this.$breakpoints = []; for (var t = 0; t < e.length; t++)this.$breakpoints[e[t]] = "ace_breakpoint"; this._signal("changeBreakpoint", {}) }, this.clearBreakpoints = function () { this.$breakpoints = [], this._signal("changeBreakpoint", {}) }, this.setBreakpoint = function (e, t) { t === undefined && (t = "ace_breakpoint"), t ? this.$breakpoints[e] = t : delete this.$breakpoints[e], this._signal("changeBreakpoint", {}) }, this.clearBreakpoint = function (e) { delete this.$breakpoints[e], this._signal("changeBreakpoint", {}) }, this.addMarker = function (e, t, n, r) { var i = this.$markerId++, s = { range: e, type: n || "line", renderer: typeof n == "function" ? n : null, clazz: t, inFront: !!r, id: i }; return r ? (this.$frontMarkers[i] = s, this._signal("changeFrontMarker")) : (this.$backMarkers[i] = s, this._signal("changeBackMarker")), i }, this.addDynamicMarker = function (e, t) { if (!e.update) return; var n = this.$markerId++; return e.id = n, e.inFront = !!t, t ? (this.$frontMarkers[n] = e, this._signal("changeFrontMarker")) : (this.$backMarkers[n] = e, this._signal("changeBackMarker")), e }, this.removeMarker = function (e) { var t = this.$frontMarkers[e] || this.$backMarkers[e]; if (!t) return; var n = t.inFront ? this.$frontMarkers : this.$backMarkers; delete n[e], this._signal(t.inFront ? "changeFrontMarker" : "changeBackMarker") }, this.getMarkers = function (e) { return e ? this.$frontMarkers : this.$backMarkers }, this.highlight = function (e) { if (!this.$searchHighlight) { var t = new p(null, "ace_selected-word", "text"); this.$searchHighlight = this.addDynamicMarker(t) } this.$searchHighlight.setRegexp(e) }, this.highlightLines = function (e, t, n, r) { typeof t != "number" && (n = t, t = e), n || (n = "ace_step"); var i = new l(e, 0, t, Infinity); return i.id = this.addMarker(i, n, "fullLine", r), i }, this.setAnnotations = function (e) { this.$annotations = e, this._signal("changeAnnotation", {}) }, this.getAnnotations = function () { return this.$annotations || [] }, this.clearAnnotations = function () { this.setAnnotations([]) }, this.$detectNewLine = function (e) { var t = e.match(/^.*?(\r?\n)/m); t ? this.$autoNewLine = t[1] : this.$autoNewLine = "\n" }, this.getWordRange = function (e, t) { var n = this.getLine(e), r = !1; t > 0 && (r = !!n.charAt(t - 1).match(this.tokenRe)), r || (r = !!n.charAt(t).match(this.tokenRe)); if (r) var i = this.tokenRe; else if (/^\s+$/.test(n.slice(t - 1, t + 1))) var i = /\s/; else var i = this.nonTokenRe; var s = t; if (s > 0) { do s--; while (s >= 0 && n.charAt(s).match(i)); s++ } var o = t; while (o < n.length && n.charAt(o).match(i)) o++; return new l(e, s, e, o) }, this.getAWordRange = function (e, t) { var n = this.getWordRange(e, t), r = this.getLine(n.end.row); while (r.charAt(n.end.column).match(/[ \t]/)) n.end.column += 1; return n }, this.setNewLineMode = function (e) { this.doc.setNewLineMode(e) }, this.getNewLineMode = function () { return this.doc.getNewLineMode() }, this.setUseWorker = function (e) { this.setOption("useWorker", e) }, this.getUseWorker = function () { return this.$useWorker }, this.onReloadTokenizer = function (e) { var t = e.data; this.bgTokenizer.start(t.first), this._signal("tokenizerUpdate", e) }, this.$modes = o.$modes, this.$mode = null, this.$modeId = null, this.setMode = function (e, t) { if (e && typeof e == "object") { if (e.getTokenizer) return this.$onChangeMode(e); var n = e, r = n.path } else r = e || "ace/mode/text"; this.$modes["ace/mode/text"] || (this.$modes["ace/mode/text"] = new f); if (this.$modes[r] && !n) { this.$onChangeMode(this.$modes[r]), t && t(); return } this.$modeId = r, o.loadModule(["mode", r], function (e) { if (this.$modeId !== r) return t && t(); this.$modes[r] && !n ? this.$onChangeMode(this.$modes[r]) : e && e.Mode && (e = new e.Mode(n), n || (this.$modes[r] = e, e.$id = r), this.$onChangeMode(e)), t && t() }.bind(this)), this.$mode || this.$onChangeMode(this.$modes["ace/mode/text"], !0) }, this.$onChangeMode = function (e, t) { t || (this.$modeId = e.$id); if (this.$mode === e) return; var n = this.$mode; this.$mode = e, this.$stopWorker(), this.$useWorker && this.$startWorker(); var r = e.getTokenizer(); if (r.on !== undefined) { var i = this.onReloadTokenizer.bind(this); r.on("update", i) } if (!this.bgTokenizer) { this.bgTokenizer = new h(r); var s = this; this.bgTokenizer.on("update", function (e) { s._signal("tokenizerUpdate", e) }) } else this.bgTokenizer.setTokenizer(r); this.bgTokenizer.setDocument(this.getDocument()), this.tokenRe = e.tokenRe, this.nonTokenRe = e.nonTokenRe, t || (e.attachToSession && e.attachToSession(this), this.$options.wrapMethod.set.call(this, this.$wrapMethod), this.$setFolding(e.foldingRules), this.bgTokenizer.start(0), this._emit("changeMode", { oldMode: n, mode: e })) }, this.$stopWorker = function () { this.$worker && (this.$worker.terminate(), this.$worker = null) }, this.$startWorker = function () { try { this.$worker = this.$mode.createWorker(this) } catch (e) { o.warn("Could not load worker", e), this.$worker = null } }, this.getMode = function () { return this.$mode }, this.$scrollTop = 0, this.setScrollTop = function (e) { if (this.$scrollTop === e || isNaN(e)) return; this.$scrollTop = e, this._signal("changeScrollTop", e) }, this.getScrollTop = function () { return this.$scrollTop }, this.$scrollLeft = 0, this.setScrollLeft = function (e) { if (this.$scrollLeft === e || isNaN(e)) return; this.$scrollLeft = e, this._signal("changeScrollLeft", e) }, this.getScrollLeft = function () { return this.$scrollLeft }, this.getScreenWidth = function () { return this.$computeWidth(), this.lineWidgets ? Math.max(this.getLineWidgetMaxWidth(), this.screenWidth) : this.screenWidth }, this.getLineWidgetMaxWidth = function () { if (this.lineWidgetsWidth != null) return this.lineWidgetsWidth; var e = 0; return this.lineWidgets.forEach(function (t) { t && t.screenWidth > e && (e = t.screenWidth) }), this.lineWidgetWidth = e }, this.$computeWidth = function (e) { if (this.$modified || e) { this.$modified = !1; if (this.$useWrapMode) return this.screenWidth = this.$wrapLimit; var t = this.doc.getAllLines(), n = this.$rowLengthCache, r = 0, i = 0, s = this.$foldData[i], o = s ? s.start.row : Infinity, u = t.length; for (var a = 0; a < u; a++) { if (a > o) { a = s.end.row + 1; if (a >= u) break; s = this.$foldData[i++], o = s ? s.start.row : Infinity } n[a] == null && (n[a] = this.$getStringScreenWidth(t[a])[0]), n[a] > r && (r = n[a]) } this.screenWidth = r } }, this.getLine = function (e) { return this.doc.getLine(e) }, this.getLines = function (e, t) { return this.doc.getLines(e, t) }, this.getLength = function () { return this.doc.getLength() }, this.getTextRange = function (e) { return this.doc.getTextRange(e || this.selection.getRange()) }, this.insert = function (e, t) { return this.doc.insert(e, t) }, this.remove = function (e) { return this.doc.remove(e) }, this.removeFullLines = function (e, t) { return this.doc.removeFullLines(e, t) }, this.undoChanges = function (e, t) { if (!e.length) return; this.$fromUndo = !0; for (var n = e.length - 1; n != -1; n--) { var r = e[n]; r.action == "insert" || r.action == "remove" ? this.doc.revertDelta(r) : r.folds && this.addFolds(r.folds) } !t && this.$undoSelect && (e.selectionBefore ? this.selection.fromJSON(e.selectionBefore) : this.selection.setRange(this.$getUndoSelection(e, !0))), this.$fromUndo = !1 }, this.redoChanges = function (e, t) { if (!e.length) return; this.$fromUndo = !0; for (var n = 0; n < e.length; n++) { var r = e[n]; (r.action == "insert" || r.action == "remove") && this.doc.$safeApplyDelta(r) } !t && this.$undoSelect && (e.selectionAfter ? this.selection.fromJSON(e.selectionAfter) : this.selection.setRange(this.$getUndoSelection(e, !1))), this.$fromUndo = !1 }, this.setUndoSelect = function (e) { this.$undoSelect = e }, this.$getUndoSelection = function (e, t) { function n(e) { return t ? e.action !== "insert" : e.action === "insert" } var r, i; for (var s = 0; s < e.length; s++) { var o = e[s]; if (!o.start) continue; if (!r) { n(o) ? r = l.fromPoints(o.start, o.end) : r = l.fromPoints(o.start, o.start); continue } n(o) ? (i = o.start, r.compare(i.row, i.column) == -1 && r.setStart(i), i = o.end, r.compare(i.row, i.column) == 1 && r.setEnd(i)) : (i = o.start, r.compare(i.row, i.column) == -1 && (r = l.fromPoints(o.start, o.start))) } return r }, this.replace = function (e, t) { return this.doc.replace(e, t) }, this.moveText = function (e, t, n) { var r = this.getTextRange(e), i = this.getFoldsInRange(e), s = l.fromPoints(t, t); if (!n) { this.remove(e); var o = e.start.row - e.end.row, u = o ? -e.end.column : e.start.column - e.end.column; u && (s.start.row == e.end.row && s.start.column > e.end.column && (s.start.column += u), s.end.row == e.end.row && s.end.column > e.end.column && (s.end.column += u)), o && s.start.row >= e.end.row && (s.start.row += o, s.end.row += o) } s.end = this.insert(s.start, r); if (i.length) { var a = e.start, f = s.start, o = f.row - a.row, u = f.column - a.column; this.addFolds(i.map(function (e) { return e = e.clone(), e.start.row == a.row && (e.start.column += u), e.end.row == a.row && (e.end.column += u), e.start.row += o, e.end.row += o, e })) } return s }, this.indentRows = function (e, t, n) { n = n.replace(/\t/g, this.getTabString()); for (var r = e; r <= t; r++)this.doc.insertInLine({ row: r, column: 0 }, n) }, this.outdentRows = function (e) { var t = e.collapseRows(), n = new l(0, 0, 0, 0), r = this.getTabSize(); for (var i = t.start.row; i <= t.end.row; ++i) { var s = this.getLine(i); n.start.row = i, n.end.row = i; for (var o = 0; o < r; ++o)if (s.charAt(o) != " ") break; o < r && s.charAt(o) == " " ? (n.start.column = o, n.end.column = o + 1) : (n.start.column = 0, n.end.column = o), this.remove(n) } }, this.$moveLines = function (e, t, n) { e = this.getRowFoldStart(e), t = this.getRowFoldEnd(t); if (n < 0) { var r = this.getRowFoldStart(e + n); if (r < 0) return 0; var i = r - e } else if (n > 0) { var r = this.getRowFoldEnd(t + n); if (r > this.doc.getLength() - 1) return 0; var i = r - t } else { e = this.$clipRowToDocument(e), t = this.$clipRowToDocument(t); var i = t - e + 1 } var s = new l(e, 0, t, Number.MAX_VALUE), o = this.getFoldsInRange(s).map(function (e) { return e = e.clone(), e.start.row += i, e.end.row += i, e }), u = n == 0 ? this.doc.getLines(e, t) : this.doc.removeFullLines(e, t); return this.doc.insertFullLines(e + i, u), o.length && this.addFolds(o), i }, this.moveLinesUp = function (e, t) { return this.$moveLines(e, t, -1) }, this.moveLinesDown = function (e, t) { return this.$moveLines(e, t, 1) }, this.duplicateLines = function (e, t) { return this.$moveLines(e, t, 0) }, this.$clipRowToDocument = function (e) { return Math.max(0, Math.min(e, this.doc.getLength() - 1)) }, this.$clipColumnToRow = function (e, t) { return t < 0 ? 0 : Math.min(this.doc.getLine(e).length, t) }, this.$clipPositionToDocument = function (e, t) { t = Math.max(0, t); if (e < 0) e = 0, t = 0; else { var n = this.doc.getLength(); e >= n ? (e = n - 1, t = this.doc.getLine(n - 1).length) : t = Math.min(this.doc.getLine(e).length, t) } return { row: e, column: t } }, this.$clipRangeToDocument = function (e) { e.start.row < 0 ? (e.start.row = 0, e.start.column = 0) : e.start.column = this.$clipColumnToRow(e.start.row, e.start.column); var t = this.doc.getLength() - 1; return e.end.row > t ? (e.end.row = t, e.end.column = this.doc.getLine(t).length) : e.end.column = this.$clipColumnToRow(e.end.row, e.end.column), e }, this.$wrapLimit = 80, this.$useWrapMode = !1, this.$wrapLimitRange = { min: null, max: null }, this.setUseWrapMode = function (e) { if (e != this.$useWrapMode) { this.$useWrapMode = e, this.$modified = !0, this.$resetRowCache(0); if (e) { var t = this.getLength(); this.$wrapData = Array(t), this.$updateWrapData(0, t - 1) } this._signal("changeWrapMode") } }, this.getUseWrapMode = function () { return this.$useWrapMode }, this.setWrapLimitRange = function (e, t) { if (this.$wrapLimitRange.min !== e || this.$wrapLimitRange.max !== t) this.$wrapLimitRange = { min: e, max: t }, this.$modified = !0, this.$bidiHandler.markAsDirty(), this.$useWrapMode && this._signal("changeWrapMode") }, this.adjustWrapLimit = function (e, t) { var n = this.$wrapLimitRange; n.max < 0 && (n = { min: t, max: t }); var r = this.$constrainWrapLimit(e, n.min, n.max); return r != this.$wrapLimit && r > 1 ? (this.$wrapLimit = r, this.$modified = !0, this.$useWrapMode && (this.$updateWrapData(0, this.getLength() - 1), this.$resetRowCache(0), this._signal("changeWrapLimit")), !0) : !1 }, this.$constrainWrapLimit = function (e, t, n) { return t && (e = Math.max(t, e)), n && (e = Math.min(n, e)), e }, this.getWrapLimit = function () { return this.$wrapLimit }, this.setWrapLimit = function (e) { this.setWrapLimitRange(e, e) }, this.getWrapLimitRange = function () { return { min: this.$wrapLimitRange.min, max: this.$wrapLimitRange.max } }, this.$updateInternalDataOnChange = function (e) { var t = this.$useWrapMode, n = e.action, r = e.start, i = e.end, s = r.row, o = i.row, u = o - s, a = null; this.$updating = !0; if (u != 0) if (n === "remove") { this[t ? "$wrapData" : "$rowLengthCache"].splice(s, u); var f = this.$foldData; a = this.getFoldsInRange(e), this.removeFolds(a); var l = this.getFoldLine(i.row), c = 0; if (l) { l.addRemoveChars(i.row, i.column, r.column - i.column), l.shiftRow(-u); var h = this.getFoldLine(s); h && h !== l && (h.merge(l), l = h), c = f.indexOf(l) + 1 } for (c; c < f.length; c++) { var l = f[c]; l.start.row >= i.row && l.shiftRow(-u) } o = s } else { var p = Array(u); p.unshift(s, 0); var d = t ? this.$wrapData : this.$rowLengthCache; d.splice.apply(d, p); var f = this.$foldData, l = this.getFoldLine(s), c = 0; if (l) { var v = l.range.compareInside(r.row, r.column); v == 0 ? (l = l.split(r.row, r.column), l && (l.shiftRow(u), l.addRemoveChars(o, 0, i.column - r.column))) : v == -1 && (l.addRemoveChars(s, 0, i.column - r.column), l.shiftRow(u)), c = f.indexOf(l) + 1 } for (c; c < f.length; c++) { var l = f[c]; l.start.row >= s && l.shiftRow(u) } } else { u = Math.abs(e.start.column - e.end.column), n === "remove" && (a = this.getFoldsInRange(e), this.removeFolds(a), u = -u); var l = this.getFoldLine(s); l && l.addRemoveChars(s, r.column, u) } return t && this.$wrapData.length != this.doc.getLength() && console.error("doc.getLength() and $wrapData.length have to be the same!"), this.$updating = !1, t ? this.$updateWrapData(s, o) : this.$updateRowLengthCache(s, o), a }, this.$updateRowLengthCache = function (e, t, n) { this.$rowLengthCache[e] = null, this.$rowLengthCache[t] = null }, this.$updateWrapData = function (e, t) { var r = this.doc.getAllLines(), i = this.getTabSize(), o = this.$wrapData, u = this.$wrapLimit, a, f, l = e; t = Math.min(t, r.length - 1); while (l <= t) f = this.getFoldLine(l, f), f ? (a = [], f.walk(function (e, t, i, o) { var u; if (e != null) { u = this.$getDisplayTokens(e, a.length), u[0] = n; for (var f = 1; f < u.length; f++)u[f] = s } else u = this.$getDisplayTokens(r[t].substring(o, i), a.length); a = a.concat(u) }.bind(this), f.end.row, r[f.end.row].length + 1), o[f.start.row] = this.$computeWrapSplits(a, u, i), l = f.end.row + 1) : (a = this.$getDisplayTokens(r[l]), o[l] = this.$computeWrapSplits(a, u, i), l++) }; var e = 1, t = 2, n = 3, s = 4, a = 9, c = 10, d = 11, v = 12; this.$computeWrapSplits = function (e, r, i) { function g() { var t = 0; if (m === 0) return t; if (p) for (var n = 0; n < e.length; n++) { var r = e[n]; if (r == c) t += 1; else { if (r != d) { if (r == v) continue; break } t += i } } return h && p !== !1 && (t += i), Math.min(t, m) } function y(t) { var n = t - f; for (var r = f; r < t; r++) { var i = e[r]; if (i === 12 || i === 2) n -= 1 } o.length || (b = g(), o.indent = b), l += n, o.push(l), f = t } if (e.length == 0) return []; var o = [], u = e.length, f = 0, l = 0, h = this.$wrapAsCode, p = this.$indentedSoftWrap, m = r <= Math.max(2 * i, 8) || p === !1 ? 0 : Math.floor(r / 2), b = 0; while (u - f > r - b) { var w = f + r - b; if (e[w - 1] >= c && e[w] >= c) { y(w); continue } if (e[w] == n || e[w] == s) { for (w; w != f - 1; w--)if (e[w] == n) break; if (w > f) { y(w); continue } w = f + r; for (w; w < e.length; w++)if (e[w] != s) break; if (w == e.length) break; y(w); continue } var E = Math.max(w - (r - (r >> 2)), f - 1); while (w > E && e[w] < n) w--; if (h) { while (w > E && e[w] < n) w--; while (w > E && e[w] == a) w-- } else while (w > E && e[w] < c) w--; if (w > E) { y(++w); continue } w = f + r, e[w] == t && w--, y(w - b) } return o }, this.$getDisplayTokens = function (n, r) { var i = [], s; r = r || 0; for (var o = 0; o < n.length; o++) { var u = n.charCodeAt(o); if (u == 9) { s = this.getScreenTabSize(i.length + r), i.push(d); for (var f = 1; f < s; f++)i.push(v) } else u == 32 ? i.push(c) : u > 39 && u < 48 || u > 57 && u < 64 ? i.push(a) : u >= 4352 && m(u) ? i.push(e, t) : i.push(e) } return i }, this.$getStringScreenWidth = function (e, t, n) { if (t == 0) return [0, 0]; t == null && (t = Infinity), n = n || 0; var r, i; for (i = 0; i < e.length; i++) { r = e.charCodeAt(i), r == 9 ? n += this.getScreenTabSize(n) : r >= 4352 && m(r) ? n += 2 : n += 1; if (n > t) break } return [n, i] }, this.lineWidgets = null, this.getRowLength = function (e) { var t = 1; return this.lineWidgets && (t += this.lineWidgets[e] && this.lineWidgets[e].rowCount || 0), !this.$useWrapMode || !this.$wrapData[e] ? t : this.$wrapData[e].length + t }, this.getRowLineCount = function (e) { return !this.$useWrapMode || !this.$wrapData[e] ? 1 : this.$wrapData[e].length + 1 }, this.getRowWrapIndent = function (e) { if (this.$useWrapMode) { var t = this.screenToDocumentPosition(e, Number.MAX_VALUE), n = this.$wrapData[t.row]; return n.length && n[0] < t.column ? n.indent : 0 } return 0 }, this.getScreenLastRowColumn = function (e) { var t = this.screenToDocumentPosition(e, Number.MAX_VALUE); return this.documentToScreenColumn(t.row, t.column) }, this.getDocumentLastRowColumn = function (e, t) { var n = this.documentToScreenRow(e, t); return this.getScreenLastRowColumn(n) }, this.getDocumentLastRowColumnPosition = function (e, t) { var n = this.documentToScreenRow(e, t); return this.screenToDocumentPosition(n, Number.MAX_VALUE / 10) }, this.getRowSplitData = function (e) { return this.$useWrapMode ? this.$wrapData[e] : undefined }, this.getScreenTabSize = function (e) { return this.$tabSize - (e % this.$tabSize | 0) }, this.screenToDocumentRow = function (e, t) { return this.screenToDocumentPosition(e, t).row }, this.screenToDocumentColumn = function (e, t) { return this.screenToDocumentPosition(e, t).column }, this.screenToDocumentPosition = function (e, t, n) { if (e < 0) return { row: 0, column: 0 }; var r, i = 0, s = 0, o, u = 0, a = 0, f = this.$screenRowCache, l = this.$getRowCacheIndex(f, e), c = f.length; if (c && l >= 0) var u = f[l], i = this.$docRowCache[l], h = e > f[c - 1]; else var h = !c; var p = this.getLength() - 1, d = this.getNextFoldLine(i), v = d ? d.start.row : Infinity; while (u <= e) { a = this.getRowLength(i); if (u + a > e || i >= p) break; u += a, i++, i > v && (i = d.end.row + 1, d = this.getNextFoldLine(i, d), v = d ? d.start.row : Infinity), h && (this.$docRowCache.push(i), this.$screenRowCache.push(u)) } if (d && d.start.row <= i) r = this.getFoldDisplayLine(d), i = d.start.row; else { if (u + a <= e || i > p) return { row: p, column: this.getLine(p).length }; r = this.getLine(i), d = null } var m = 0, g = Math.floor(e - u); if (this.$useWrapMode) { var y = this.$wrapData[i]; y && (o = y[g], g > 0 && y.length && (m = y.indent, s = y[g - 1] || y[y.length - 1], r = r.substring(s))) } return n !== undefined && this.$bidiHandler.isBidiRow(u + g, i, g) && (t = this.$bidiHandler.offsetToCol(n)), s += this.$getStringScreenWidth(r, t - m)[1], this.$useWrapMode && s >= o && (s = o - 1), d ? d.idxToPosition(s) : { row: i, column: s } }, this.documentToScreenPosition = function (e, t) { if (typeof t == "undefined") var n = this.$clipPositionToDocument(e.row, e.column); else n = this.$clipPositionToDocument(e, t); e = n.row, t = n.column; var r = 0, i = null, s = null; s = this.getFoldAt(e, t, 1), s && (e = s.start.row, t = s.start.column); var o, u = 0, a = this.$docRowCache, f = this.$getRowCacheIndex(a, e), l = a.length; if (l && f >= 0) var u = a[f], r = this.$screenRowCache[f], c = e > a[l - 1]; else var c = !l; var h = this.getNextFoldLine(u), p = h ? h.start.row : Infinity; while (u < e) { if (u >= p) { o = h.end.row + 1; if (o > e) break; h = this.getNextFoldLine(o, h), p = h ? h.start.row : Infinity } else o = u + 1; r += this.getRowLength(u), u = o, c && (this.$docRowCache.push(u), this.$screenRowCache.push(r)) } var d = ""; h && u >= p ? (d = this.getFoldDisplayLine(h, e, t), i = h.start.row) : (d = this.getLine(e).substring(0, t), i = e); var v = 0; if (this.$useWrapMode) { var m = this.$wrapData[i]; if (m) { var g = 0; while (d.length >= m[g]) r++, g++; d = d.substring(m[g - 1] || 0, d.length), v = g > 0 ? m.indent : 0 } } return this.lineWidgets && this.lineWidgets[u] && this.lineWidgets[u].rowsAbove && (r += this.lineWidgets[u].rowsAbove), { row: r, column: v + this.$getStringScreenWidth(d)[0] } }, this.documentToScreenColumn = function (e, t) { return this.documentToScreenPosition(e, t).column }, this.documentToScreenRow = function (e, t) { return this.documentToScreenPosition(e, t).row }, this.getScreenLength = function () { var e = 0, t = null; if (!this.$useWrapMode) { e = this.getLength(); var n = this.$foldData; for (var r = 0; r < n.length; r++)t = n[r], e -= t.end.row - t.start.row } else { var i = this.$wrapData.length, s = 0, r = 0, t = this.$foldData[r++], o = t ? t.start.row : Infinity; while (s < i) { var u = this.$wrapData[s]; e += u ? u.length + 1 : 1, s++, s > o && (s = t.end.row + 1, t = this.$foldData[r++], o = t ? t.start.row : Infinity) } } return this.lineWidgets && (e += this.$getWidgetScreenLength()), e }, this.$setFontMetrics = function (e) { if (!this.$enableVarChar) return; this.$getStringScreenWidth = function (t, n, r) { if (n === 0) return [0, 0]; n || (n = Infinity), r = r || 0; var i, s; for (s = 0; s < t.length; s++) { i = t.charAt(s), i === " " ? r += this.getScreenTabSize(r) : r += e.getCharacterWidth(i); if (r > n) break } return [r, s] } }, this.destroy = function () { this.bgTokenizer && (this.bgTokenizer.setDocument(null), this.bgTokenizer = null), this.$stopWorker(), this.removeAllListeners(), this.selection.detach() }, this.isFullWidth = m }.call(d.prototype), e("./edit_session/folding").Folding.call(d.prototype), e("./edit_session/bracket_match").BracketMatch.call(d.prototype), o.defineOptions(d.prototype, "session", { wrap: { set: function (e) { !e || e == "off" ? e = !1 : e == "free" ? e = !0 : e == "printMargin" ? e = -1 : typeof e == "string" && (e = parseInt(e, 10) || !1); if (this.$wrap == e) return; this.$wrap = e; if (!e) this.setUseWrapMode(!1); else { var t = typeof e == "number" ? e : null; this.setWrapLimitRange(t, t), this.setUseWrapMode(!0) } }, get: function () { return this.getUseWrapMode() ? this.$wrap == -1 ? "printMargin" : this.getWrapLimitRange().min ? this.$wrap : "free" : "off" }, handlesSet: !0 }, wrapMethod: { set: function (e) { e = e == "auto" ? this.$mode.type != "text" : e != "text", e != this.$wrapAsCode && (this.$wrapAsCode = e, this.$useWrapMode && (this.$useWrapMode = !1, this.setUseWrapMode(!0))) }, initialValue: "auto" }, indentedSoftWrap: { set: function () { this.$useWrapMode && (this.$useWrapMode = !1, this.setUseWrapMode(!0)) }, initialValue: !0 }, firstLineNumber: { set: function () { this._signal("changeBreakpoint") }, initialValue: 1 }, useWorker: { set: function (e) { this.$useWorker = e, this.$stopWorker(), e && this.$startWorker() }, initialValue: !0 }, useSoftTabs: { initialValue: !0 }, tabSize: { set: function (e) { e = parseInt(e), e > 0 && this.$tabSize !== e && (this.$modified = !0, this.$rowLengthCache = [], this.$tabSize = e, this._signal("changeTabSize")) }, initialValue: 4, handlesSet: !0 }, navigateWithinSoftTabs: { initialValue: !1 }, foldStyle: { set: function (e) { this.setFoldStyle(e) }, handlesSet: !0 }, overwrite: { set: function (e) { this._signal("changeOverwrite") }, initialValue: !1 }, newLineMode: { set: function (e) { this.doc.setNewLineMode(e) }, get: function () { return this.doc.getNewLineMode() }, handlesSet: !0 }, mode: { set: function (e) { this.setMode(e) }, get: function () { return this.$modeId }, handlesSet: !0 } }), t.EditSession = d }), define("ace/search", ["require", "exports", "module", "ace/lib/lang", "ace/lib/oop", "ace/range"], function (e, t, n) { "use strict"; function u(e, t) { function n(e) { return /\w/.test(e) || t.regExp ? "\\b" : "" } return n(e[0]) + e + n(e[e.length - 1]) } var r = e("./lib/lang"), i = e("./lib/oop"), s = e("./range").Range, o = function () { this.$options = {} }; (function () { this.set = function (e) { return i.mixin(this.$options, e), this }, this.getOptions = function () { return r.copyObject(this.$options) }, this.setOptions = function (e) { this.$options = e }, this.find = function (e) { var t = this.$options, n = this.$matchIterator(e, t); if (!n) return !1; var r = null; return n.forEach(function (e, n, i, o) { return r = new s(e, n, i, o), n == o && t.start && t.start.start && t.skipCurrent != 0 && r.isEqual(t.start) ? (r = null, !1) : !0 }), r }, this.findAll = function (e) { var t = this.$options; if (!t.needle) return []; this.$assembleRegExp(t); var n = t.range, i = n ? e.getLines(n.start.row, n.end.row) : e.doc.getAllLines(), o = [], u = t.re; if (t.$isMultiLine) { var a = u.length, f = i.length - a, l; e: for (var c = u.offset || 0; c <= f; c++) { for (var h = 0; h < a; h++)if (i[c + h].search(u[h]) == -1) continue e; var p = i[c], d = i[c + a - 1], v = p.length - p.match(u[0])[0].length, m = d.match(u[a - 1])[0].length; if (l && l.end.row === c && l.end.column > v) continue; o.push(l = new s(c, v, c + a - 1, m)), a > 2 && (c = c + a - 2) } } else for (var g = 0; g < i.length; g++) { var y = r.getMatchOffsets(i[g], u); for (var h = 0; h < y.length; h++) { var b = y[h]; o.push(new s(g, b.offset, g, b.offset + b.length)) } } if (n) { var w = n.start.column, E = n.start.column, g = 0, h = o.length - 1; while (g < h && o[g].start.column < w && o[g].start.row == n.start.row) g++; while (g < h && o[h].end.column > E && o[h].end.row == n.end.row) h--; o = o.slice(g, h + 1); for (g = 0, h = o.length; g < h; g++)o[g].start.row += n.start.row, o[g].end.row += n.start.row } return o }, this.replace = function (e, t) { var n = this.$options, r = this.$assembleRegExp(n); if (n.$isMultiLine) return t; if (!r) return; var i = r.exec(e); if (!i || i[0].length != e.length) return null; t = e.replace(r, t); if (n.preserveCase) { t = t.split(""); for (var s = Math.min(e.length, e.length); s--;) { var o = e[s]; o && o.toLowerCase() != o ? t[s] = t[s].toUpperCase() : t[s] = t[s].toLowerCase() } t = t.join("") } return t }, this.$assembleRegExp = function (e, t) { if (e.needle instanceof RegExp) return e.re = e.needle; var n = e.needle; if (!e.needle) return e.re = !1; e.regExp || (n = r.escapeRegExp(n)), e.wholeWord && (n = u(n, e)); var i = e.caseSensitive ? "gm" : "gmi"; e.$isMultiLine = !t && /[\n\r]/.test(n); if (e.$isMultiLine) return e.re = this.$assembleMultilineRegExp(n, i); try { var s = new RegExp(n, i) } catch (o) { s = !1 } return e.re = s }, this.$assembleMultilineRegExp = function (e, t) { var n = e.replace(/\r\n|\r|\n/g, "$\n^").split("\n"), r = []; for (var i = 0; i < n.length; i++)try { r.push(new RegExp(n[i], t)) } catch (s) { return !1 } return r }, this.$matchIterator = function (e, t) { var n = this.$assembleRegExp(t); if (!n) return !1; var r = t.backwards == 1, i = t.skipCurrent != 0, s = t.range, o = t.start; o || (o = s ? s[r ? "end" : "start"] : e.selection.getRange()), o.start && (o = o[i != r ? "end" : "start"]); var u = s ? s.start.row : 0, a = s ? s.end.row : e.getLength() - 1; if (r) var f = function (e) { var n = o.row; if (c(n, o.column, e)) return; for (n--; n >= u; n--)if (c(n, Number.MAX_VALUE, e)) return; if (t.wrap == 0) return; for (n = a, u = o.row; n >= u; n--)if (c(n, Number.MAX_VALUE, e)) return }; else var f = function (e) { var n = o.row; if (c(n, o.column, e)) return; for (n += 1; n <= a; n++)if (c(n, 0, e)) return; if (t.wrap == 0) return; for (n = u, a = o.row; n <= a; n++)if (c(n, 0, e)) return }; if (t.$isMultiLine) var l = n.length, c = function (t, i, s) { var o = r ? t - l + 1 : t; if (o < 0) return; var u = e.getLine(o), a = u.search(n[0]); if (!r && a < i || a === -1) return; for (var f = 1; f < l; f++) { u = e.getLine(o + f); if (u.search(n[f]) == -1) return } var c = u.match(n[l - 1])[0].length; if (r && c > i) return; if (s(o, a, o + l - 1, c)) return !0 }; else if (r) var c = function (t, r, i) { var s = e.getLine(t), o = [], u, a = 0; n.lastIndex = 0; while (u = n.exec(s)) { var f = u[0].length; a = u.index; if (!f) { if (a >= s.length) break; n.lastIndex = a += 1 } if (u.index + f > r) break; o.push(u.index, f) } for (var l = o.length - 1; l >= 0; l -= 2) { var c = o[l - 1], f = o[l]; if (i(t, c, t, c + f)) return !0 } }; else var c = function (t, r, i) { var s = e.getLine(t), o, u; n.lastIndex = r; while (u = n.exec(s)) { var a = u[0].length; o = u.index; if (i(t, o, t, o + a)) return !0; if (!a) { n.lastIndex = o += 1; if (o >= s.length) return !1 } } }; return { forEach: f } } }).call(o.prototype), t.Search = o }), define("ace/keyboard/hash_handler", ["require", "exports", "module", "ace/lib/keys", "ace/lib/useragent"], function (e, t, n) { "use strict"; function o(e, t) { this.platform = t || (i.isMac ? "mac" : "win"), this.commands = {}, this.commandKeyBinding = {}, this.addCommands(e), this.$singleCommand = !0 } function u(e, t) { o.call(this, e, t), this.$singleCommand = !1 } var r = e("../lib/keys"), i = e("../lib/useragent"), s = r.KEY_MODS; u.prototype = o.prototype, function () { function e(e) { return typeof e == "object" && e.bindKey && e.bindKey.position || (e.isDefault ? -100 : 0) } this.addCommand = function (e) { this.commands[e.name] && this.removeCommand(e), this.commands[e.name] = e, e.bindKey && this._buildKeyHash(e) }, this.removeCommand = function (e, t) { var n = e && (typeof e == "string" ? e : e.name); e = this.commands[n], t || delete this.commands[n]; var r = this.commandKeyBinding; for (var i in r) { var s = r[i]; if (s == e) delete r[i]; else if (Array.isArray(s)) { var o = s.indexOf(e); o != -1 && (s.splice(o, 1), s.length == 1 && (r[i] = s[0])) } } }, this.bindKey = function (e, t, n) { typeof e == "object" && e && (n == undefined && (n = e.position), e = e[this.platform]); if (!e) return; if (typeof t == "function") return this.addCommand({ exec: t, bindKey: e, name: t.name || e }); e.split("|").forEach(function (e) { var r = ""; if (e.indexOf(" ") != -1) { var i = e.split(/\s+/); e = i.pop(), i.forEach(function (e) { var t = this.parseKeys(e), n = s[t.hashId] + t.key; r += (r ? " " : "") + n, this._addCommandToBinding(r, "chainKeys") }, this), r += " " } var o = this.parseKeys(e), u = s[o.hashId] + o.key; this._addCommandToBinding(r + u, t, n) }, this) }, this._addCommandToBinding = function (t, n, r) { var i = this.commandKeyBinding, s; if (!n) delete i[t]; else if (!i[t] || this.$singleCommand) i[t] = n; else { Array.isArray(i[t]) ? (s = i[t].indexOf(n)) != -1 && i[t].splice(s, 1) : i[t] = [i[t]], typeof r != "number" && (r = e(n)); var o = i[t]; for (s = 0; s < o.length; s++) { var u = o[s], a = e(u); if (a > r) break } o.splice(s, 0, n) } }, this.addCommands = function (e) { e && Object.keys(e).forEach(function (t) { var n = e[t]; if (!n) return; if (typeof n == "string") return this.bindKey(n, t); typeof n == "function" && (n = { exec: n }); if (typeof n != "object") return; n.name || (n.name = t), this.addCommand(n) }, this) }, this.removeCommands = function (e) { Object.keys(e).forEach(function (t) { this.removeCommand(e[t]) }, this) }, this.bindKeys = function (e) { Object.keys(e).forEach(function (t) { this.bindKey(t, e[t]) }, this) }, this._buildKeyHash = function (e) { this.bindKey(e.bindKey, e) }, this.parseKeys = function (e) { var t = e.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function (e) { return e }), n = t.pop(), i = r[n]; if (r.FUNCTION_KEYS[i]) n = r.FUNCTION_KEYS[i].toLowerCase(); else { if (!t.length) return { key: n, hashId: -1 }; if (t.length == 1 && t[0] == "shift") return { key: n.toUpperCase(), hashId: -1 } } var s = 0; for (var o = t.length; o--;) { var u = r.KEY_MODS[t[o]]; if (u == null) return typeof console != "undefined" && console.error("invalid modifier " + t[o] + " in " + e), !1; s |= u } return { key: n, hashId: s } }, this.findKeyCommand = function (t, n) { var r = s[t] + n; return this.commandKeyBinding[r] }, this.handleKeyboard = function (e, t, n, r) { if (r < 0) return; var i = s[t] + n, o = this.commandKeyBinding[i]; e.$keyChain && (e.$keyChain += " " + i, o = this.commandKeyBinding[e.$keyChain] || o); if (o) if (o == "chainKeys" || o[o.length - 1] == "chainKeys") return e.$keyChain = e.$keyChain || i, { command: "null" }; if (e.$keyChain) if (!!t && t != 4 || n.length != 1) { if (t == -1 || r > 0) e.$keyChain = "" } else e.$keyChain = e.$keyChain.slice(0, -i.length - 1); return { command: o } }, this.getStatusText = function (e, t) { return t.$keyChain || "" } }.call(o.prototype), t.HashHandler = o, t.MultiHashHandler = u }), define("ace/commands/command_manager", ["require", "exports", "module", "ace/lib/oop", "ace/keyboard/hash_handler", "ace/lib/event_emitter"], function (e, t, n) { "use strict"; var r = e("../lib/oop"), i = e("../keyboard/hash_handler").MultiHashHandler, s = e("../lib/event_emitter").EventEmitter, o = function (e, t) { i.call(this, t, e), this.byName = this.commands, this.setDefaultHandler("exec", function (e) { return e.command.exec(e.editor, e.args || {}) }) }; r.inherits(o, i), function () { r.implement(this, s), this.exec = function (e, t, n) { if (Array.isArray(e)) { for (var r = e.length; r--;)if (this.exec(e[r], t, n)) return !0; return !1 } typeof e == "string" && (e = this.commands[e]); if (!e) return !1; if (t && t.$readOnly && !e.readOnly) return !1; if (this.$checkCommandState != 0 && e.isAvailable && !e.isAvailable(t)) return !1; var i = { editor: t, command: e, args: n }; return i.returnValue = this._emit("exec", i), this._signal("afterExec", i), i.returnValue === !1 ? !1 : !0 }, this.toggleRecording = function (e) { if (this.$inReplay) return; return e && e._emit("changeStatus"), this.recording ? (this.macro.pop(), this.off("exec", this.$addCommandToMacro), this.macro.length || (this.macro = this.oldMacro), this.recording = !1) : (this.$addCommandToMacro || (this.$addCommandToMacro = function (e) { this.macro.push([e.command, e.args]) }.bind(this)), this.oldMacro = this.macro, this.macro = [], this.on("exec", this.$addCommandToMacro), this.recording = !0) }, this.replay = function (e) { if (this.$inReplay || !this.macro) return; if (this.recording) return this.toggleRecording(e); try { this.$inReplay = !0, this.macro.forEach(function (t) { typeof t == "string" ? this.exec(t, e) : this.exec(t[0], e, t[1]) }, this) } finally { this.$inReplay = !1 } }, this.trimMacro = function (e) { return e.map(function (e) { return typeof e[0] != "string" && (e[0] = e[0].name), e[1] || (e = e[0]), e }) } }.call(o.prototype), t.CommandManager = o }), define("ace/commands/default_commands", ["require", "exports", "module", "ace/lib/lang", "ace/config", "ace/range"], function (e, t, n) { "use strict"; function o(e, t) { return { win: e, mac: t } } var r = e("../lib/lang"), i = e("../config"), s = e("../range").Range; t.commands = [{ name: "showSettingsMenu", bindKey: o("Ctrl-,", "Command-,"), exec: function (e) { i.loadModule("ace/ext/settings_menu", function (t) { t.init(e), e.showSettingsMenu() }) }, readOnly: !0 }, { name: "goToNextError", bindKey: o("Alt-E", "F4"), exec: function (e) { i.loadModule("./ext/error_marker", function (t) { t.showErrorMarker(e, 1) }) }, scrollIntoView: "animate", readOnly: !0 }, { name: "goToPreviousError", bindKey: o("Alt-Shift-E", "Shift-F4"), exec: function (e) { i.loadModule("./ext/error_marker", function (t) { t.showErrorMarker(e, -1) }) }, scrollIntoView: "animate", readOnly: !0 }, { name: "selectall", description: "Select all", bindKey: o("Ctrl-A", "Command-A"), exec: function (e) { e.selectAll() }, readOnly: !0 }, { name: "centerselection", description: "Center selection", bindKey: o(null, "Ctrl-L"), exec: function (e) { e.centerSelection() }, readOnly: !0 }, { name: "gotoline", description: "Go to line...", bindKey: o("Ctrl-L", "Command-L"), exec: function (e, t) { typeof t == "number" && !isNaN(t) && e.gotoLine(t), e.prompt({ $type: "gotoLine" }) }, readOnly: !0 }, { name: "fold", bindKey: o("Alt-L|Ctrl-F1", "Command-Alt-L|Command-F1"), exec: function (e) { e.session.toggleFold(!1) }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: !0 }, { name: "unfold", bindKey: o("Alt-Shift-L|Ctrl-Shift-F1", "Command-Alt-Shift-L|Command-Shift-F1"), exec: function (e) { e.session.toggleFold(!0) }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: !0 }, { name: "toggleFoldWidget", bindKey: o("F2", "F2"), exec: function (e) { e.session.toggleFoldWidget() }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: !0 }, { name: "toggleParentFoldWidget", bindKey: o("Alt-F2", "Alt-F2"), exec: function (e) { e.session.toggleFoldWidget(!0) }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: !0 }, { name: "foldall", description: "Fold all", bindKey: o(null, "Ctrl-Command-Option-0"), exec: function (e) { e.session.foldAll() }, scrollIntoView: "center", readOnly: !0 }, { name: "foldOther", description: "Fold other", bindKey: o("Alt-0", "Command-Option-0"), exec: function (e) { e.session.foldAll(), e.session.unfold(e.selection.getAllRanges()) }, scrollIntoView: "center", readOnly: !0 }, { name: "unfoldall", description: "Unfold all", bindKey: o("Alt-Shift-0", "Command-Option-Shift-0"), exec: function (e) { e.session.unfold() }, scrollIntoView: "center", readOnly: !0 }, { name: "findnext", description: "Find next", bindKey: o("Ctrl-K", "Command-G"), exec: function (e) { e.findNext() }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: !0 }, { name: "findprevious", description: "Find previous", bindKey: o("Ctrl-Shift-K", "Command-Shift-G"), exec: function (e) { e.findPrevious() }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: !0 }, { name: "selectOrFindNext", description: "Select or find next", bindKey: o("Alt-K", "Ctrl-G"), exec: function (e) { e.selection.isEmpty() ? e.selection.selectWord() : e.findNext() }, readOnly: !0 }, { name: "selectOrFindPrevious", description: "Select or find previous", bindKey: o("Alt-Shift-K", "Ctrl-Shift-G"), exec: function (e) { e.selection.isEmpty() ? e.selection.selectWord() : e.findPrevious() }, readOnly: !0 }, { name: "find", description: "Find", bindKey: o("Ctrl-F", "Command-F"), exec: function (e) { i.loadModule("ace/ext/searchbox", function (t) { t.Search(e) }) }, readOnly: !0 }, { name: "overwrite", description: "Overwrite", bindKey: "Insert", exec: function (e) { e.toggleOverwrite() }, readOnly: !0 }, { name: "selecttostart", description: "Select to start", bindKey: o("Ctrl-Shift-Home", "Command-Shift-Home|Command-Shift-Up"), exec: function (e) { e.getSelection().selectFileStart() }, multiSelectAction: "forEach", readOnly: !0, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "gotostart", description: "Go to start", bindKey: o("Ctrl-Home", "Command-Home|Command-Up"), exec: function (e) { e.navigateFileStart() }, multiSelectAction: "forEach", readOnly: !0, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "selectup", description: "Select up", bindKey: o("Shift-Up", "Shift-Up|Ctrl-Shift-P"), exec: function (e) { e.getSelection().selectUp() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "golineup", description: "Go line up", bindKey: o("Up", "Up|Ctrl-P"), exec: function (e, t) { e.navigateUp(t.times) }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selecttoend", description: "Select to end", bindKey: o("Ctrl-Shift-End", "Command-Shift-End|Command-Shift-Down"), exec: function (e) { e.getSelection().selectFileEnd() }, multiSelectAction: "forEach", readOnly: !0, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "gotoend", description: "Go to end", bindKey: o("Ctrl-End", "Command-End|Command-Down"), exec: function (e) { e.navigateFileEnd() }, multiSelectAction: "forEach", readOnly: !0, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "selectdown", description: "Select down", bindKey: o("Shift-Down", "Shift-Down|Ctrl-Shift-N"), exec: function (e) { e.getSelection().selectDown() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "golinedown", description: "Go line down", bindKey: o("Down", "Down|Ctrl-N"), exec: function (e, t) { e.navigateDown(t.times) }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selectwordleft", description: "Select word left", bindKey: o("Ctrl-Shift-Left", "Option-Shift-Left"), exec: function (e) { e.getSelection().selectWordLeft() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "gotowordleft", description: "Go to word left", bindKey: o("Ctrl-Left", "Option-Left"), exec: function (e) { e.navigateWordLeft() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selecttolinestart", description: "Select to line start", bindKey: o("Alt-Shift-Left", "Command-Shift-Left|Ctrl-Shift-A"), exec: function (e) { e.getSelection().selectLineStart() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "gotolinestart", description: "Go to line start", bindKey: o("Alt-Left|Home", "Command-Left|Home|Ctrl-A"), exec: function (e) { e.navigateLineStart() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selectleft", description: "Select left", bindKey: o("Shift-Left", "Shift-Left|Ctrl-Shift-B"), exec: function (e) { e.getSelection().selectLeft() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "gotoleft", description: "Go to left", bindKey: o("Left", "Left|Ctrl-B"), exec: function (e, t) { e.navigateLeft(t.times) }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selectwordright", description: "Select word right", bindKey: o("Ctrl-Shift-Right", "Option-Shift-Right"), exec: function (e) { e.getSelection().selectWordRight() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "gotowordright", description: "Go to word right", bindKey: o("Ctrl-Right", "Option-Right"), exec: function (e) { e.navigateWordRight() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selecttolineend", description: "Select to line end", bindKey: o("Alt-Shift-Right", "Command-Shift-Right|Shift-End|Ctrl-Shift-E"), exec: function (e) { e.getSelection().selectLineEnd() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "gotolineend", description: "Go to line end", bindKey: o("Alt-Right|End", "Command-Right|End|Ctrl-E"), exec: function (e) { e.navigateLineEnd() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selectright", description: "Select right", bindKey: o("Shift-Right", "Shift-Right"), exec: function (e) { e.getSelection().selectRight() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "gotoright", description: "Go to right", bindKey: o("Right", "Right|Ctrl-F"), exec: function (e, t) { e.navigateRight(t.times) }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selectpagedown", description: "Select page down", bindKey: "Shift-PageDown", exec: function (e) { e.selectPageDown() }, readOnly: !0 }, { name: "pagedown", description: "Page down", bindKey: o(null, "Option-PageDown"), exec: function (e) { e.scrollPageDown() }, readOnly: !0 }, { name: "gotopagedown", description: "Go to page down", bindKey: o("PageDown", "PageDown|Ctrl-V"), exec: function (e) { e.gotoPageDown() }, readOnly: !0 }, { name: "selectpageup", description: "Select page up", bindKey: "Shift-PageUp", exec: function (e) { e.selectPageUp() }, readOnly: !0 }, { name: "pageup", description: "Page up", bindKey: o(null, "Option-PageUp"), exec: function (e) { e.scrollPageUp() }, readOnly: !0 }, { name: "gotopageup", description: "Go to page up", bindKey: "PageUp", exec: function (e) { e.gotoPageUp() }, readOnly: !0 }, { name: "scrollup", description: "Scroll up", bindKey: o("Ctrl-Up", null), exec: function (e) { e.renderer.scrollBy(0, -2 * e.renderer.layerConfig.lineHeight) }, readOnly: !0 }, { name: "scrolldown", description: "Scroll down", bindKey: o("Ctrl-Down", null), exec: function (e) { e.renderer.scrollBy(0, 2 * e.renderer.layerConfig.lineHeight) }, readOnly: !0 }, { name: "selectlinestart", description: "Select line start", bindKey: "Shift-Home", exec: function (e) { e.getSelection().selectLineStart() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "selectlineend", description: "Select line end", bindKey: "Shift-End", exec: function (e) { e.getSelection().selectLineEnd() }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "togglerecording", description: "Toggle recording", bindKey: o("Ctrl-Alt-E", "Command-Option-E"), exec: function (e) { e.commands.toggleRecording(e) }, readOnly: !0 }, { name: "replaymacro", description: "Replay macro", bindKey: o("Ctrl-Shift-E", "Command-Shift-E"), exec: function (e) { e.commands.replay(e) }, readOnly: !0 }, { name: "jumptomatching", description: "Jump to matching", bindKey: o("Ctrl-\\|Ctrl-P", "Command-\\"), exec: function (e) { e.jumpToMatching() }, multiSelectAction: "forEach", scrollIntoView: "animate", readOnly: !0 }, { name: "selecttomatching", description: "Select to matching", bindKey: o("Ctrl-Shift-\\|Ctrl-Shift-P", "Command-Shift-\\"), exec: function (e) { e.jumpToMatching(!0) }, multiSelectAction: "forEach", scrollIntoView: "animate", readOnly: !0 }, { name: "expandToMatching", description: "Expand to matching", bindKey: o("Ctrl-Shift-M", "Ctrl-Shift-M"), exec: function (e) { e.jumpToMatching(!0, !0) }, multiSelectAction: "forEach", scrollIntoView: "animate", readOnly: !0 }, { name: "passKeysToBrowser", description: "Pass keys to browser", bindKey: o(null, null), exec: function () { }, passEvent: !0, readOnly: !0 }, { name: "copy", description: "Copy", exec: function (e) { }, readOnly: !0 }, { name: "cut", description: "Cut", exec: function (e) { var t = e.$copyWithEmptySelection && e.selection.isEmpty(), n = t ? e.selection.getLineRange() : e.selection.getRange(); e._emit("cut", n), n.isEmpty() || e.session.remove(n), e.clearSelection() }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "paste", description: "Paste", exec: function (e, t) { e.$handlePaste(t) }, scrollIntoView: "cursor" }, { name: "removeline", description: "Remove line", bindKey: o("Ctrl-D", "Command-D"), exec: function (e) { e.removeLines() }, scrollIntoView: "cursor", multiSelectAction: "forEachLine" }, { name: "duplicateSelection", description: "Duplicate selection", bindKey: o("Ctrl-Shift-D", "Command-Shift-D"), exec: function (e) { e.duplicateSelection() }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "sortlines", description: "Sort lines", bindKey: o("Ctrl-Alt-S", "Command-Alt-S"), exec: function (e) { e.sortLines() }, scrollIntoView: "selection", multiSelectAction: "forEachLine" }, { name: "togglecomment", description: "Toggle comment", bindKey: o("Ctrl-/", "Command-/"), exec: function (e) { e.toggleCommentLines() }, multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" }, { name: "toggleBlockComment", description: "Toggle block comment", bindKey: o("Ctrl-Shift-/", "Command-Shift-/"), exec: function (e) { e.toggleBlockComment() }, multiSelectAction: "forEach", scrollIntoView: "selectionPart" }, { name: "modifyNumberUp", description: "Modify number up", bindKey: o("Ctrl-Shift-Up", "Alt-Shift-Up"), exec: function (e) { e.modifyNumber(1) }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "modifyNumberDown", description: "Modify number down", bindKey: o("Ctrl-Shift-Down", "Alt-Shift-Down"), exec: function (e) { e.modifyNumber(-1) }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "replace", description: "Replace", bindKey: o("Ctrl-H", "Command-Option-F"), exec: function (e) { i.loadModule("ace/ext/searchbox", function (t) { t.Search(e, !0) }) } }, { name: "undo", description: "Undo", bindKey: o("Ctrl-Z", "Command-Z"), exec: function (e) { e.undo() } }, { name: "redo", description: "Redo", bindKey: o("Ctrl-Shift-Z|Ctrl-Y", "Command-Shift-Z|Command-Y"), exec: function (e) { e.redo() } }, { name: "copylinesup", description: "Copy lines up", bindKey: o("Alt-Shift-Up", "Command-Option-Up"), exec: function (e) { e.copyLinesUp() }, scrollIntoView: "cursor" }, { name: "movelinesup", description: "Move lines up", bindKey: o("Alt-Up", "Option-Up"), exec: function (e) { e.moveLinesUp() }, scrollIntoView: "cursor" }, { name: "copylinesdown", description: "Copy lines down", bindKey: o("Alt-Shift-Down", "Command-Option-Down"), exec: function (e) { e.copyLinesDown() }, scrollIntoView: "cursor" }, { name: "movelinesdown", description: "Move lines down", bindKey: o("Alt-Down", "Option-Down"), exec: function (e) { e.moveLinesDown() }, scrollIntoView: "cursor" }, { name: "del", description: "Delete", bindKey: o("Delete", "Delete|Ctrl-D|Shift-Delete"), exec: function (e) { e.remove("right") }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "backspace", description: "Backspace", bindKey: o("Shift-Backspace|Backspace", "Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H"), exec: function (e) { e.remove("left") }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "cut_or_delete", description: "Cut or delete", bindKey: o("Shift-Delete", null), exec: function (e) { if (!e.selection.isEmpty()) return !1; e.remove("left") }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removetolinestart", description: "Remove to line start", bindKey: o("Alt-Backspace", "Command-Backspace"), exec: function (e) { e.removeToLineStart() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removetolineend", description: "Remove to line end", bindKey: o("Alt-Delete", "Ctrl-K|Command-Delete"), exec: function (e) { e.removeToLineEnd() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removetolinestarthard", description: "Remove to line start hard", bindKey: o("Ctrl-Shift-Backspace", null), exec: function (e) { var t = e.selection.getRange(); t.start.column = 0, e.session.remove(t) }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removetolineendhard", description: "Remove to line end hard", bindKey: o("Ctrl-Shift-Delete", null), exec: function (e) { var t = e.selection.getRange(); t.end.column = Number.MAX_VALUE, e.session.remove(t) }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removewordleft", description: "Remove word left", bindKey: o("Ctrl-Backspace", "Alt-Backspace|Ctrl-Alt-Backspace"), exec: function (e) { e.removeWordLeft() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removewordright", description: "Remove word right", bindKey: o("Ctrl-Delete", "Alt-Delete"), exec: function (e) { e.removeWordRight() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "outdent", description: "Outdent", bindKey: o("Shift-Tab", "Shift-Tab"), exec: function (e) { e.blockOutdent() }, multiSelectAction: "forEach", scrollIntoView: "selectionPart" }, { name: "indent", description: "Indent", bindKey: o("Tab", "Tab"), exec: function (e) { e.indent() }, multiSelectAction: "forEach", scrollIntoView: "selectionPart" }, { name: "blockoutdent", description: "Block outdent", bindKey: o("Ctrl-[", "Ctrl-["), exec: function (e) { e.blockOutdent() }, multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" }, { name: "blockindent", description: "Block indent", bindKey: o("Ctrl-]", "Ctrl-]"), exec: function (e) { e.blockIndent() }, multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" }, { name: "insertstring", description: "Insert string", exec: function (e, t) { e.insert(t) }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "inserttext", description: "Insert text", exec: function (e, t) { e.insert(r.stringRepeat(t.text || "", t.times || 1)) }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "splitline", description: "Split line", bindKey: o(null, "Ctrl-O"), exec: function (e) { e.splitLine() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "transposeletters", description: "Transpose letters", bindKey: o("Alt-Shift-X", "Ctrl-T"), exec: function (e) { e.transposeLetters() }, multiSelectAction: function (e) { e.transposeSelections(1) }, scrollIntoView: "cursor" }, { name: "touppercase", description: "To uppercase", bindKey: o("Ctrl-U", "Ctrl-U"), exec: function (e) { e.toUpperCase() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "tolowercase", description: "To lowercase", bindKey: o("Ctrl-Shift-U", "Ctrl-Shift-U"), exec: function (e) { e.toLowerCase() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "autoindent", description: "Auto Indent", bindKey: o(null, null), exec: function (e) { e.autoIndent() }, multiSelectAction: "forEachLine", scrollIntoView: "animate" }, { name: "expandtoline", description: "Expand to line", bindKey: o("Ctrl-Shift-L", "Command-Shift-L"), exec: function (e) { var t = e.selection.getRange(); t.start.column = t.end.column = 0, t.end.row++, e.selection.setRange(t, !1) }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: !0 }, { name: "joinlines", description: "Join lines", bindKey: o(null, null), exec: function (e) { var t = e.selection.isBackwards(), n = t ? e.selection.getSelectionLead() : e.selection.getSelectionAnchor(), i = t ? e.selection.getSelectionAnchor() : e.selection.getSelectionLead(), o = e.session.doc.getLine(n.row).length, u = e.session.doc.getTextRange(e.selection.getRange()), a = u.replace(/\n\s*/, " ").length, f = e.session.doc.getLine(n.row); for (var l = n.row + 1; l <= i.row + 1; l++) { var c = r.stringTrimLeft(r.stringTrimRight(e.session.doc.getLine(l))); c.length !== 0 && (c = " " + c), f += c } i.row + 1 < e.session.doc.getLength() - 1 && (f += e.session.doc.getNewLineCharacter()), e.clearSelection(), e.session.doc.replace(new s(n.row, 0, i.row + 2, 0), f), a > 0 ? (e.selection.moveCursorTo(n.row, n.column), e.selection.selectTo(n.row, n.column + a)) : (o = e.session.doc.getLine(n.row).length > o ? o + 1 : o, e.selection.moveCursorTo(n.row, o)) }, multiSelectAction: "forEach", readOnly: !0 }, { name: "invertSelection", description: "Invert selection", bindKey: o(null, null), exec: function (e) { var t = e.session.doc.getLength() - 1, n = e.session.doc.getLine(t).length, r = e.selection.rangeList.ranges, i = []; r.length < 1 && (r = [e.selection.getRange()]); for (var o = 0; o < r.length; o++)o == r.length - 1 && (r[o].end.row !== t || r[o].end.column !== n) && i.push(new s(r[o].end.row, r[o].end.column, t, n)), o === 0 ? (r[o].start.row !== 0 || r[o].start.column !== 0) && i.push(new s(0, 0, r[o].start.row, r[o].start.column)) : i.push(new s(r[o - 1].end.row, r[o - 1].end.column, r[o].start.row, r[o].start.column)); e.exitMultiSelectMode(), e.clearSelection(); for (var o = 0; o < i.length; o++)e.selection.addRange(i[o], !1) }, readOnly: !0, scrollIntoView: "none" }, { name: "addLineAfter", exec: function (e) { e.selection.clearSelection(), e.navigateLineEnd(), e.insert("\n") }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "addLineBefore", exec: function (e) { e.selection.clearSelection(); var t = e.getCursorPosition(); e.selection.moveTo(t.row - 1, Number.MAX_VALUE), e.insert("\n"), t.row === 0 && e.navigateUp() }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "openCommandPallete", description: "Open command pallete", bindKey: o("F1", "F1"), exec: function (e) { e.prompt({ $type: "commands" }) }, readOnly: !0 }, { name: "modeSelect", description: "Change language mode...", bindKey: o(null, null), exec: function (e) { e.prompt({ $type: "modes" }) }, readOnly: !0 }] }), define("ace/editor", ["require", "exports", "module", "ace/lib/fixoldbrowsers", "ace/lib/oop", "ace/lib/dom", "ace/lib/lang", "ace/lib/useragent", "ace/keyboard/textinput", "ace/mouse/mouse_handler", "ace/mouse/fold_handler", "ace/keyboard/keybinding", "ace/edit_session", "ace/search", "ace/range", "ace/lib/event_emitter", "ace/commands/command_manager", "ace/commands/default_commands", "ace/config", "ace/token_iterator", "ace/clipboard"], function (e, t, n) { "use strict"; e("./lib/fixoldbrowsers"); var r = e("./lib/oop"), i = e("./lib/dom"), s = e("./lib/lang"), o = e("./lib/useragent"), u = e("./keyboard/textinput").TextInput, a = e("./mouse/mouse_handler").MouseHandler, f = e("./mouse/fold_handler").FoldHandler, l = e("./keyboard/keybinding").KeyBinding, c = e("./edit_session").EditSession, h = e("./search").Search, p = e("./range").Range, d = e("./lib/event_emitter").EventEmitter, v = e("./commands/command_manager").CommandManager, m = e("./commands/default_commands").commands, g = e("./config"), y = e("./token_iterator").TokenIterator, b = e("./clipboard"), w = function (e, t, n) { this.$toDestroy = []; var r = e.getContainerElement(); this.container = r, this.renderer = e, this.id = "editor" + ++w.$uid, this.commands = new v(o.isMac ? "mac" : "win", m), typeof document == "object" && (this.textInput = new u(e.getTextAreaContainer(), this), this.renderer.textarea = this.textInput.getElement(), this.$mouseHandler = new a(this), new f(this)), this.keyBinding = new l(this), this.$search = (new h).set({ wrap: !0 }), this.$historyTracker = this.$historyTracker.bind(this), this.commands.on("exec", this.$historyTracker), this.$initOperationListeners(), this._$emitInputEvent = s.delayedCall(function () { this._signal("input", {}), this.session && this.session.bgTokenizer && this.session.bgTokenizer.scheduleStart() }.bind(this)), this.on("change", function (e, t) { t._$emitInputEvent.schedule(31) }), this.setSession(t || n && n.session || new c("")), g.resetOptions(this), n && this.setOptions(n), g._signal("editor", this) }; w.$uid = 0, function () { r.implement(this, d), this.$initOperationListeners = function () { this.commands.on("exec", this.startOperation.bind(this), !0), this.commands.on("afterExec", this.endOperation.bind(this), !0), this.$opResetTimer = s.delayedCall(this.endOperation.bind(this, !0)), this.on("change", function () { this.curOp || (this.startOperation(), this.curOp.selectionBefore = this.$lastSel), this.curOp.docChanged = !0 }.bind(this), !0), this.on("changeSelection", function () { this.curOp || (this.startOperation(), this.curOp.selectionBefore = this.$lastSel), this.curOp.selectionChanged = !0 }.bind(this), !0) }, this.curOp = null, this.prevOp = {}, this.startOperation = function (e) { if (this.curOp) { if (!e || this.curOp.command) return; this.prevOp = this.curOp } e || (this.previousCommand = null, e = {}), this.$opResetTimer.schedule(), this.curOp = this.session.curOp = { command: e.command || {}, args: e.args, scrollTop: this.renderer.scrollTop }, this.curOp.selectionBefore = this.selection.toJSON() }, this.endOperation = function (e) { if (this.curOp && this.session) { if (e && e.returnValue === !1 || !this.session) return this.curOp = null; if (e == 1 && this.curOp.command && this.curOp.command.name == "mouse") return; this._signal("beforeEndOperation"); if (!this.curOp) return; var t = this.curOp.command, n = t && t.scrollIntoView; if (n) { switch (n) { case "center-animate": n = "animate"; case "center": this.renderer.scrollCursorIntoView(null, .5); break; case "animate": case "cursor": this.renderer.scrollCursorIntoView(); break; case "selectionPart": var r = this.selection.getRange(), i = this.renderer.layerConfig; (r.start.row >= i.lastRow || r.end.row <= i.firstRow) && this.renderer.scrollSelectionIntoView(this.selection.anchor, this.selection.lead); break; default: }n == "animate" && this.renderer.animateScrolling(this.curOp.scrollTop) } var s = this.selection.toJSON(); this.curOp.selectionAfter = s, this.$lastSel = this.selection.toJSON(), this.session.getUndoManager().addSelection(s), this.prevOp = this.curOp, this.curOp = null } }, this.$mergeableCommands = ["backspace", "del", "insertstring"], this.$historyTracker = function (e) { if (!this.$mergeUndoDeltas) return; var t = this.prevOp, n = this.$mergeableCommands, r = t.command && e.command.name == t.command.name; if (e.command.name == "insertstring") { var i = e.args; this.mergeNextCommand === undefined && (this.mergeNextCommand = !0), r = r && this.mergeNextCommand && (!/\s/.test(i) || /\s/.test(t.args)), this.mergeNextCommand = !0 } else r = r && n.indexOf(e.command.name) !== -1; this.$mergeUndoDeltas != "always" && Date.now() - this.sequenceStartTime > 2e3 && (r = !1), r ? this.session.mergeUndoDeltas = !0 : n.indexOf(e.command.name) !== -1 && (this.sequenceStartTime = Date.now()) }, this.setKeyboardHandler = function (e, t) { if (e && typeof e == "string" && e != "ace") { this.$keybindingId = e; var n = this; g.loadModule(["keybinding", e], function (r) { n.$keybindingId == e && n.keyBinding.setKeyboardHandler(r && r.handler), t && t() }) } else this.$keybindingId = null, this.keyBinding.setKeyboardHandler(e), t && t() }, this.getKeyboardHandler = function () { return this.keyBinding.getKeyboardHandler() }, this.setSession = function (e) { if (this.session == e) return; this.curOp && this.endOperation(), this.curOp = {}; var t = this.session; if (t) { this.session.off("change", this.$onDocumentChange), this.session.off("changeMode", this.$onChangeMode), this.session.off("tokenizerUpdate", this.$onTokenizerUpdate), this.session.off("changeTabSize", this.$onChangeTabSize), this.session.off("changeWrapLimit", this.$onChangeWrapLimit), this.session.off("changeWrapMode", this.$onChangeWrapMode), this.session.off("changeFold", this.$onChangeFold), this.session.off("changeFrontMarker", this.$onChangeFrontMarker), this.session.off("changeBackMarker", this.$onChangeBackMarker), this.session.off("changeBreakpoint", this.$onChangeBreakpoint), this.session.off("changeAnnotation", this.$onChangeAnnotation), this.session.off("changeOverwrite", this.$onCursorChange), this.session.off("changeScrollTop", this.$onScrollTopChange), this.session.off("changeScrollLeft", this.$onScrollLeftChange); var n = this.session.getSelection(); n.off("changeCursor", this.$onCursorChange), n.off("changeSelection", this.$onSelectionChange) } this.session = e, e ? (this.$onDocumentChange = this.onDocumentChange.bind(this), e.on("change", this.$onDocumentChange), this.renderer.setSession(e), this.$onChangeMode = this.onChangeMode.bind(this), e.on("changeMode", this.$onChangeMode), this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this), e.on("tokenizerUpdate", this.$onTokenizerUpdate), this.$onChangeTabSize = this.renderer.onChangeTabSize.bind(this.renderer), e.on("changeTabSize", this.$onChangeTabSize), this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this), e.on("changeWrapLimit", this.$onChangeWrapLimit), this.$onChangeWrapMode = this.onChangeWrapMode.bind(this), e.on("changeWrapMode", this.$onChangeWrapMode), this.$onChangeFold = this.onChangeFold.bind(this), e.on("changeFold", this.$onChangeFold), this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this), this.session.on("changeFrontMarker", this.$onChangeFrontMarker), this.$onChangeBackMarker = this.onChangeBackMarker.bind(this), this.session.on("changeBackMarker", this.$onChangeBackMarker), this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this), this.session.on("changeBreakpoint", this.$onChangeBreakpoint), this.$onChangeAnnotation = this.onChangeAnnotation.bind(this), this.session.on("changeAnnotation", this.$onChangeAnnotation), this.$onCursorChange = this.onCursorChange.bind(this), this.session.on("changeOverwrite", this.$onCursorChange), this.$onScrollTopChange = this.onScrollTopChange.bind(this), this.session.on("changeScrollTop", this.$onScrollTopChange), this.$onScrollLeftChange = this.onScrollLeftChange.bind(this), this.session.on("changeScrollLeft", this.$onScrollLeftChange), this.selection = e.getSelection(), this.selection.on("changeCursor", this.$onCursorChange), this.$onSelectionChange = this.onSelectionChange.bind(this), this.selection.on("changeSelection", this.$onSelectionChange), this.onChangeMode(), this.onCursorChange(), this.onScrollTopChange(), this.onScrollLeftChange(), this.onSelectionChange(), this.onChangeFrontMarker(), this.onChangeBackMarker(), this.onChangeBreakpoint(), this.onChangeAnnotation(), this.session.getUseWrapMode() && this.renderer.adjustWrapLimit(), this.renderer.updateFull()) : (this.selection = null, this.renderer.setSession(e)), this._signal("changeSession", { session: e, oldSession: t }), this.curOp = null, t && t._signal("changeEditor", { oldEditor: this }), e && e._signal("changeEditor", { editor: this }), e && e.bgTokenizer && e.bgTokenizer.scheduleStart() }, this.getSession = function () { return this.session }, this.setValue = function (e, t) { return this.session.doc.setValue(e), t ? t == 1 ? this.navigateFileEnd() : t == -1 && this.navigateFileStart() : this.selectAll(), e }, this.getValue = function () { return this.session.getValue() }, this.getSelection = function () { return this.selection }, this.resize = function (e) { this.renderer.onResize(e) }, this.setTheme = function (e, t) { this.renderer.setTheme(e, t) }, this.getTheme = function () { return this.renderer.getTheme() }, this.setStyle = function (e) { this.renderer.setStyle(e) }, this.unsetStyle = function (e) { this.renderer.unsetStyle(e) }, this.getFontSize = function () { return this.getOption("fontSize") || i.computedStyle(this.container).fontSize }, this.setFontSize = function (e) { this.setOption("fontSize", e) }, this.$highlightBrackets = function () { if (this.$highlightPending) return; var e = this; this.$highlightPending = !0, setTimeout(function () { e.$highlightPending = !1; var t = e.session; if (!t || !t.bgTokenizer) return; t.$bracketHighlight && (t.$bracketHighlight.markerIds.forEach(function (e) { t.removeMarker(e) }), t.$bracketHighlight = null); var n = t.getMatchingBracketRanges(e.getCursorPosition()); !n && t.$mode.getMatching && (n = t.$mode.getMatching(e.session)); if (!n) return; var r = "ace_bracket"; Array.isArray(n) ? n.length == 1 && (r = "ace_error_bracket") : n = [n], n.length == 2 && (p.comparePoints(n[0].end, n[1].start) == 0 ? n = [p.fromPoints(n[0].start, n[1].end)] : p.comparePoints(n[0].start, n[1].end) == 0 && (n = [p.fromPoints(n[1].start, n[0].end)])), t.$bracketHighlight = { ranges: n, markerIds: n.map(function (e) { return t.addMarker(e, r, "text") }) } }, 50) }, this.$highlightTags = function () { if (this.$highlightTagPending) return; var e = this; this.$highlightTagPending = !0, setTimeout(function () { e.$highlightTagPending = !1; var t = e.session; if (!t || !t.bgTokenizer) return; var n = e.getCursorPosition(), r = new y(e.session, n.row, n.column), i = r.getCurrentToken(); if (!i || !/\b(?:tag-open|tag-name)/.test(i.type)) { t.removeMarker(t.$tagHighlight), t.$tagHighlight = null; return } if (i.type.indexOf("tag-open") != -1) { i = r.stepForward(); if (!i) return } var s = i.value, o = 0, u = r.stepBackward(); if (u.value == "<") { do u = i, i = r.stepForward(), i && i.value === s && i.type.indexOf("tag-name") !== -1 && (u.value === "<" ? o++ : u.value === "= 0) } else { do i = u, u = r.stepBackward(), i && i.value === s && i.type.indexOf("tag-name") !== -1 && (u.value === "<" ? o++ : u.value === " 1) && (t = !1) } if (e.$highlightLineMarker && !t) e.removeMarker(e.$highlightLineMarker.id), e.$highlightLineMarker = null; else if (!e.$highlightLineMarker && t) { var n = new p(t.row, t.column, t.row, Infinity); n.id = e.addMarker(n, "ace_active-line", "screenLine"), e.$highlightLineMarker = n } else t && (e.$highlightLineMarker.start.row = t.row, e.$highlightLineMarker.end.row = t.row, e.$highlightLineMarker.start.column = t.column, e._signal("changeBackMarker")) }, this.onSelectionChange = function (e) { var t = this.session; t.$selectionMarker && t.removeMarker(t.$selectionMarker), t.$selectionMarker = null; if (!this.selection.isEmpty()) { var n = this.selection.getRange(), r = this.getSelectionStyle(); t.$selectionMarker = t.addMarker(n, "ace_selection", r) } else this.$updateHighlightActiveLine(); var i = this.$highlightSelectedWord && this.$getSelectionHighLightRegexp(); this.session.highlight(i), this._signal("changeSelection") }, this.$getSelectionHighLightRegexp = function () { var e = this.session, t = this.getSelectionRange(); if (t.isEmpty() || t.isMultiLine()) return; var n = t.start.column, r = t.end.column, i = e.getLine(t.start.row), s = i.substring(n, r); if (s.length > 5e3 || !/[\w\d]/.test(s)) return; var o = this.$search.$assembleRegExp({ wholeWord: !0, caseSensitive: !0, needle: s }), u = i.substring(n - 1, r + 1); if (!o.test(u)) return; return o }, this.onChangeFrontMarker = function () { this.renderer.updateFrontMarkers() }, this.onChangeBackMarker = function () { this.renderer.updateBackMarkers() }, this.onChangeBreakpoint = function () { this.renderer.updateBreakpoints() }, this.onChangeAnnotation = function () { this.renderer.setAnnotations(this.session.getAnnotations()) }, this.onChangeMode = function (e) { this.renderer.updateText(), this._emit("changeMode", e) }, this.onChangeWrapLimit = function () { this.renderer.updateFull() }, this.onChangeWrapMode = function () { this.renderer.onResize(!0) }, this.onChangeFold = function () { this.$updateHighlightActiveLine(), this.renderer.updateFull() }, this.getSelectedText = function () { return this.session.getTextRange(this.getSelectionRange()) }, this.getCopyText = function () { var e = this.getSelectedText(), t = this.session.doc.getNewLineCharacter(), n = !1; if (!e && this.$copyWithEmptySelection) { n = !0; var r = this.selection.getAllRanges(); for (var i = 0; i < r.length; i++) { var s = r[i]; if (i && r[i - 1].start.row == s.start.row) continue; e += this.session.getLine(s.start.row) + t } } var o = { text: e }; return this._signal("copy", o), b.lineMode = n ? o.text : "", o.text }, this.onCopy = function () { this.commands.exec("copy", this) }, this.onCut = function () { this.commands.exec("cut", this) }, this.onPaste = function (e, t) { var n = { text: e, event: t }; this.commands.exec("paste", this, n) }, this.$handlePaste = function (e) { typeof e == "string" && (e = { text: e }), this._signal("paste", e); var t = e.text, n = t == b.lineMode, r = this.session; if (!this.inMultiSelectMode || this.inVirtualSelectionMode) n ? r.insert({ row: this.selection.lead.row, column: 0 }, t) : this.insert(t); else if (n) this.selection.rangeList.ranges.forEach(function (e) { r.insert({ row: e.start.row, column: 0 }, t) }); else { var i = t.split(/\r\n|\r|\n/), s = this.selection.rangeList.ranges, o = i.length == 2 && (!i[0] || !i[1]); if (i.length != s.length || o) return this.commands.exec("insertstring", this, t); for (var u = s.length; u--;) { var a = s[u]; a.isEmpty() || r.remove(a), r.insert(a.start, i[u]) } } }, this.execCommand = function (e, t) { return this.commands.exec(e, this, t) }, this.insert = function (e, t) { var n = this.session, r = n.getMode(), i = this.getCursorPosition(); if (this.getBehavioursEnabled() && !t) { var s = r.transformAction(n.getState(i.row), "insertion", this, n, e); s && (e !== s.text && (this.inVirtualSelectionMode || (this.session.mergeUndoDeltas = !1, this.mergeNextCommand = !1)), e = s.text) } e == " " && (e = this.session.getTabString()); if (!this.selection.isEmpty()) { var o = this.getSelectionRange(); i = this.session.remove(o), this.clearSelection() } else if (this.session.getOverwrite() && e.indexOf("\n") == -1) { var o = new p.fromPoints(i, i); o.end.column += e.length, this.session.remove(o) } if (e == "\n" || e == "\r\n") { var u = n.getLine(i.row); if (i.column > u.search(/\S|$/)) { var a = u.substr(i.column).search(/\S|$/); n.doc.removeInLine(i.row, i.column, i.column + a) } } this.clearSelection(); var f = i.column, l = n.getState(i.row), u = n.getLine(i.row), c = r.checkOutdent(l, u, e); n.insert(i, e), s && s.selection && (s.selection.length == 2 ? this.selection.setSelectionRange(new p(i.row, f + s.selection[0], i.row, f + s.selection[1])) : this.selection.setSelectionRange(new p(i.row + s.selection[0], s.selection[1], i.row + s.selection[2], s.selection[3]))); if (this.$enableAutoIndent) { if (n.getDocument().isNewLine(e)) { var h = r.getNextLineIndent(l, u.slice(0, i.column), n.getTabString()); n.insert({ row: i.row + 1, column: 0 }, h) } c && r.autoOutdent(l, n, i.row) } }, this.autoIndent = function () { var e = this.session, t = e.getMode(), n, r; if (this.selection.isEmpty()) n = 0, r = e.doc.getLength() - 1; else { var i = this.getSelectionRange(); n = i.start.row, r = i.end.row } var s = "", o = "", u = "", a, f, l, c = e.getTabString(); for (var h = n; h <= r; h++)h > 0 && (s = e.getState(h - 1), o = e.getLine(h - 1), u = t.getNextLineIndent(s, o, c)), a = e.getLine(h), f = t.$getIndent(a), u !== f && (f.length > 0 && (l = new p(h, 0, h, f.length), e.remove(l)), u.length > 0 && e.insert({ row: h, column: 0 }, u)), t.autoOutdent(s, e, h) }, this.onTextInput = function (e, t) { if (!t) return this.keyBinding.onTextInput(e); this.startOperation({ command: { name: "insertstring" } }); var n = this.applyComposition.bind(this, e, t); this.selection.rangeCount ? this.forEachSelection(n) : n(), this.endOperation() }, this.applyComposition = function (e, t) { if (t.extendLeft || t.extendRight) { var n = this.selection.getRange(); n.start.column -= t.extendLeft, n.end.column += t.extendRight, n.start.column < 0 && (n.start.row--, n.start.column += this.session.getLine(n.start.row).length + 1), this.selection.setRange(n), !e && !n.isEmpty() && this.remove() } (e || !this.selection.isEmpty()) && this.insert(e, !0); if (t.restoreStart || t.restoreEnd) { var n = this.selection.getRange(); n.start.column -= t.restoreStart, n.end.column -= t.restoreEnd, this.selection.setRange(n) } }, this.onCommandKey = function (e, t, n) { return this.keyBinding.onCommandKey(e, t, n) }, this.setOverwrite = function (e) { this.session.setOverwrite(e) }, this.getOverwrite = function () { return this.session.getOverwrite() }, this.toggleOverwrite = function () { this.session.toggleOverwrite() }, this.setScrollSpeed = function (e) { this.setOption("scrollSpeed", e) }, this.getScrollSpeed = function () { return this.getOption("scrollSpeed") }, this.setDragDelay = function (e) { this.setOption("dragDelay", e) }, this.getDragDelay = function () { return this.getOption("dragDelay") }, this.setSelectionStyle = function (e) { this.setOption("selectionStyle", e) }, this.getSelectionStyle = function () { return this.getOption("selectionStyle") }, this.setHighlightActiveLine = function (e) { this.setOption("highlightActiveLine", e) }, this.getHighlightActiveLine = function () { return this.getOption("highlightActiveLine") }, this.setHighlightGutterLine = function (e) { this.setOption("highlightGutterLine", e) }, this.getHighlightGutterLine = function () { return this.getOption("highlightGutterLine") }, this.setHighlightSelectedWord = function (e) { this.setOption("highlightSelectedWord", e) }, this.getHighlightSelectedWord = function () { return this.$highlightSelectedWord }, this.setAnimatedScroll = function (e) { this.renderer.setAnimatedScroll(e) }, this.getAnimatedScroll = function () { return this.renderer.getAnimatedScroll() }, this.setShowInvisibles = function (e) { this.renderer.setShowInvisibles(e) }, this.getShowInvisibles = function () { return this.renderer.getShowInvisibles() }, this.setDisplayIndentGuides = function (e) { this.renderer.setDisplayIndentGuides(e) }, this.getDisplayIndentGuides = function () { return this.renderer.getDisplayIndentGuides() }, this.setShowPrintMargin = function (e) { this.renderer.setShowPrintMargin(e) }, this.getShowPrintMargin = function () { return this.renderer.getShowPrintMargin() }, this.setPrintMarginColumn = function (e) { this.renderer.setPrintMarginColumn(e) }, this.getPrintMarginColumn = function () { return this.renderer.getPrintMarginColumn() }, this.setReadOnly = function (e) { this.setOption("readOnly", e) }, this.getReadOnly = function () { return this.getOption("readOnly") }, this.setBehavioursEnabled = function (e) { this.setOption("behavioursEnabled", e) }, this.getBehavioursEnabled = function () { return this.getOption("behavioursEnabled") }, this.setWrapBehavioursEnabled = function (e) { this.setOption("wrapBehavioursEnabled", e) }, this.getWrapBehavioursEnabled = function () { return this.getOption("wrapBehavioursEnabled") }, this.setShowFoldWidgets = function (e) { this.setOption("showFoldWidgets", e) }, this.getShowFoldWidgets = function () { return this.getOption("showFoldWidgets") }, this.setFadeFoldWidgets = function (e) { this.setOption("fadeFoldWidgets", e) }, this.getFadeFoldWidgets = function () { return this.getOption("fadeFoldWidgets") }, this.remove = function (e) { this.selection.isEmpty() && (e == "left" ? this.selection.selectLeft() : this.selection.selectRight()); var t = this.getSelectionRange(); if (this.getBehavioursEnabled()) { var n = this.session, r = n.getState(t.start.row), i = n.getMode().transformAction(r, "deletion", this, n, t); if (t.end.column === 0) { var s = n.getTextRange(t); if (s[s.length - 1] == "\n") { var o = n.getLine(t.end.row); /^\s+$/.test(o) && (t.end.column = o.length) } } i && (t = i) } this.session.remove(t), this.clearSelection() }, this.removeWordRight = function () { this.selection.isEmpty() && this.selection.selectWordRight(), this.session.remove(this.getSelectionRange()), this.clearSelection() }, this.removeWordLeft = function () { this.selection.isEmpty() && this.selection.selectWordLeft(), this.session.remove(this.getSelectionRange()), this.clearSelection() }, this.removeToLineStart = function () { this.selection.isEmpty() && this.selection.selectLineStart(), this.selection.isEmpty() && this.selection.selectLeft(), this.session.remove(this.getSelectionRange()), this.clearSelection() }, this.removeToLineEnd = function () { this.selection.isEmpty() && this.selection.selectLineEnd(); var e = this.getSelectionRange(); e.start.column == e.end.column && e.start.row == e.end.row && (e.end.column = 0, e.end.row++), this.session.remove(e), this.clearSelection() }, this.splitLine = function () { this.selection.isEmpty() || (this.session.remove(this.getSelectionRange()), this.clearSelection()); var e = this.getCursorPosition(); this.insert("\n"), this.moveCursorToPosition(e) }, this.transposeLetters = function () { if (!this.selection.isEmpty()) return; var e = this.getCursorPosition(), t = e.column; if (t === 0) return; var n = this.session.getLine(e.row), r, i; t < n.length ? (r = n.charAt(t) + n.charAt(t - 1), i = new p(e.row, t - 1, e.row, t + 1)) : (r = n.charAt(t - 1) + n.charAt(t - 2), i = new p(e.row, t - 2, e.row, t)), this.session.replace(i, r), this.session.selection.moveToPosition(i.end) }, this.toLowerCase = function () { var e = this.getSelectionRange(); this.selection.isEmpty() && this.selection.selectWord(); var t = this.getSelectionRange(), n = this.session.getTextRange(t); this.session.replace(t, n.toLowerCase()), this.selection.setSelectionRange(e) }, this.toUpperCase = function () { var e = this.getSelectionRange(); this.selection.isEmpty() && this.selection.selectWord(); var t = this.getSelectionRange(), n = this.session.getTextRange(t); this.session.replace(t, n.toUpperCase()), this.selection.setSelectionRange(e) }, this.indent = function () { var e = this.session, t = this.getSelectionRange(); if (t.start.row < t.end.row) { var n = this.$getSelectedRows(); e.indentRows(n.first, n.last, " "); return } if (t.start.column < t.end.column) { var r = e.getTextRange(t); if (!/^\s+$/.test(r)) { var n = this.$getSelectedRows(); e.indentRows(n.first, n.last, " "); return } } var i = e.getLine(t.start.row), o = t.start, u = e.getTabSize(), a = e.documentToScreenColumn(o.row, o.column); if (this.session.getUseSoftTabs()) var f = u - a % u, l = s.stringRepeat(" ", f); else { var f = a % u; while (i[t.start.column - 1] == " " && f) t.start.column--, f--; this.selection.setSelectionRange(t), l = " " } return this.insert(l) }, this.blockIndent = function () { var e = this.$getSelectedRows(); this.session.indentRows(e.first, e.last, " ") }, this.blockOutdent = function () { var e = this.session.getSelection(); this.session.outdentRows(e.getRange()) }, this.sortLines = function () { var e = this.$getSelectedRows(), t = this.session, n = []; for (var r = e.first; r <= e.last; r++)n.push(t.getLine(r)); n.sort(function (e, t) { return e.toLowerCase() < t.toLowerCase() ? -1 : e.toLowerCase() > t.toLowerCase() ? 1 : 0 }); var i = new p(0, 0, 0, 0); for (var r = e.first; r <= e.last; r++) { var s = t.getLine(r); i.start.row = r, i.end.row = r, i.end.column = s.length, t.replace(i, n[r - e.first]) } }, this.toggleCommentLines = function () { var e = this.session.getState(this.getCursorPosition().row), t = this.$getSelectedRows(); this.session.getMode().toggleCommentLines(e, this.session, t.first, t.last) }, this.toggleBlockComment = function () { var e = this.getCursorPosition(), t = this.session.getState(e.row), n = this.getSelectionRange(); this.session.getMode().toggleBlockComment(t, this.session, n, e) }, this.getNumberAt = function (e, t) { var n = /[\-]?[0-9]+(?:\.[0-9]+)?/g; n.lastIndex = 0; var r = this.session.getLine(e); while (n.lastIndex < t) { var i = n.exec(r); if (i.index <= t && i.index + i[0].length >= t) { var s = { value: i[0], start: i.index, end: i.index + i[0].length }; return s } } return null }, this.modifyNumber = function (e) { var t = this.selection.getCursor().row, n = this.selection.getCursor().column, r = new p(t, n - 1, t, n), i = this.session.getTextRange(r); if (!isNaN(parseFloat(i)) && isFinite(i)) { var s = this.getNumberAt(t, n); if (s) { var o = s.value.indexOf(".") >= 0 ? s.start + s.value.indexOf(".") + 1 : s.end, u = s.start + s.value.length - o, a = parseFloat(s.value); a *= Math.pow(10, u), o !== s.end && n < o ? e *= Math.pow(10, s.end - n - 1) : e *= Math.pow(10, s.end - n), a += e, a /= Math.pow(10, u); var f = a.toFixed(u), l = new p(t, s.start, t, s.end); this.session.replace(l, f), this.moveCursorTo(t, Math.max(s.start + 1, n + f.length - s.value.length)) } } else this.toggleWord() }, this.$toggleWordPairs = [["first", "last"], ["true", "false"], ["yes", "no"], ["width", "height"], ["top", "bottom"], ["right", "left"], ["on", "off"], ["x", "y"], ["get", "set"], ["max", "min"], ["horizontal", "vertical"], ["show", "hide"], ["add", "remove"], ["up", "down"], ["before", "after"], ["even", "odd"], ["in", "out"], ["inside", "outside"], ["next", "previous"], ["increase", "decrease"], ["attach", "detach"], ["&&", "||"], ["==", "!="]], this.toggleWord = function () { var e = this.selection.getCursor().row, t = this.selection.getCursor().column; this.selection.selectWord(); var n = this.getSelectedText(), r = this.selection.getWordRange().start.column, i = n.replace(/([a-z]+|[A-Z]+)(?=[A-Z_]|$)/g, "$1 ").split(/\s/), o = t - r - 1; o < 0 && (o = 0); var u = 0, a = 0, f = this; n.match(/[A-Za-z0-9_]+/) && i.forEach(function (t, i) { a = u + t.length, o >= u && o <= a && (n = t, f.selection.clearSelection(), f.moveCursorTo(e, u + r), f.selection.selectTo(e, a + r)), u = a }); var l = this.$toggleWordPairs, c; for (var h = 0; h < l.length; h++) { var p = l[h]; for (var d = 0; d <= 1; d++) { var v = +!d, m = n.match(new RegExp("^\\s?_?(" + s.escapeRegExp(p[d]) + ")\\s?$", "i")); if (m) { var g = n.match(new RegExp("([_]|^|\\s)(" + s.escapeRegExp(m[1]) + ")($|\\s)", "g")); g && (c = n.replace(new RegExp(s.escapeRegExp(p[d]), "i"), function (e) { var t = p[v]; return e.toUpperCase() == e ? t = t.toUpperCase() : e.charAt(0).toUpperCase() == e.charAt(0) && (t = t.substr(0, 0) + p[v].charAt(0).toUpperCase() + t.substr(1)), t }), this.insert(c), c = "") } } } }, this.removeLines = function () { var e = this.$getSelectedRows(); this.session.removeFullLines(e.first, e.last), this.clearSelection() }, this.duplicateSelection = function () { var e = this.selection, t = this.session, n = e.getRange(), r = e.isBackwards(); if (n.isEmpty()) { var i = n.start.row; t.duplicateLines(i, i) } else { var s = r ? n.start : n.end, o = t.insert(s, t.getTextRange(n), !1); n.start = s, n.end = o, e.setSelectionRange(n, r) } }, this.moveLinesDown = function () { this.$moveLines(1, !1) }, this.moveLinesUp = function () { this.$moveLines(-1, !1) }, this.moveText = function (e, t, n) { return this.session.moveText(e, t, n) }, this.copyLinesUp = function () { this.$moveLines(-1, !0) }, this.copyLinesDown = function () { this.$moveLines(1, !0) }, this.$moveLines = function (e, t) { var n, r, i = this.selection; if (!i.inMultiSelectMode || this.inVirtualSelectionMode) { var s = i.toOrientedRange(); n = this.$getSelectedRows(s), r = this.session.$moveLines(n.first, n.last, t ? 0 : e), t && e == -1 && (r = 0), s.moveBy(r, 0), i.fromOrientedRange(s) } else { var o = i.rangeList.ranges; i.rangeList.detach(this.session), this.inVirtualSelectionMode = !0; var u = 0, a = 0, f = o.length; for (var l = 0; l < f; l++) { var c = l; o[l].moveBy(u, 0), n = this.$getSelectedRows(o[l]); var h = n.first, p = n.last; while (++l < f) { a && o[l].moveBy(a, 0); var d = this.$getSelectedRows(o[l]); if (t && d.first != p) break; if (!t && d.first > p + 1) break; p = d.last } l--, u = this.session.$moveLines(h, p, t ? 0 : e), t && e == -1 && (c = l + 1); while (c <= l) o[c].moveBy(u, 0), c++; t || (u = 0), a += u } i.fromOrientedRange(i.ranges[0]), i.rangeList.attach(this.session), this.inVirtualSelectionMode = !1 } }, this.$getSelectedRows = function (e) { return e = (e || this.getSelectionRange()).collapseRows(), { first: this.session.getRowFoldStart(e.start.row), last: this.session.getRowFoldEnd(e.end.row) } }, this.onCompositionStart = function (e) { this.renderer.showComposition(e) }, this.onCompositionUpdate = function (e) { this.renderer.setCompositionText(e) }, this.onCompositionEnd = function () { this.renderer.hideComposition() }, this.getFirstVisibleRow = function () { return this.renderer.getFirstVisibleRow() }, this.getLastVisibleRow = function () { return this.renderer.getLastVisibleRow() }, this.isRowVisible = function (e) { return e >= this.getFirstVisibleRow() && e <= this.getLastVisibleRow() }, this.isRowFullyVisible = function (e) { return e >= this.renderer.getFirstFullyVisibleRow() && e <= this.renderer.getLastFullyVisibleRow() }, this.$getVisibleRowCount = function () { return this.renderer.getScrollBottomRow() - this.renderer.getScrollTopRow() + 1 }, this.$moveByPage = function (e, t) { var n = this.renderer, r = this.renderer.layerConfig, i = e * Math.floor(r.height / r.lineHeight); t === !0 ? this.selection.$moveSelection(function () { this.moveCursorBy(i, 0) }) : t === !1 && (this.selection.moveCursorBy(i, 0), this.selection.clearSelection()); var s = n.scrollTop; n.scrollBy(0, i * r.lineHeight), t != null && n.scrollCursorIntoView(null, .5), n.animateScrolling(s) }, this.selectPageDown = function () { this.$moveByPage(1, !0) }, this.selectPageUp = function () { this.$moveByPage(-1, !0) }, this.gotoPageDown = function () { this.$moveByPage(1, !1) }, this.gotoPageUp = function () { this.$moveByPage(-1, !1) }, this.scrollPageDown = function () { this.$moveByPage(1) }, this.scrollPageUp = function () { this.$moveByPage(-1) }, this.scrollToRow = function (e) { this.renderer.scrollToRow(e) }, this.scrollToLine = function (e, t, n, r) { this.renderer.scrollToLine(e, t, n, r) }, this.centerSelection = function () { var e = this.getSelectionRange(), t = { row: Math.floor(e.start.row + (e.end.row - e.start.row) / 2), column: Math.floor(e.start.column + (e.end.column - e.start.column) / 2) }; this.renderer.alignCursor(t, .5) }, this.getCursorPosition = function () { return this.selection.getCursor() }, this.getCursorPositionScreen = function () { return this.session.documentToScreenPosition(this.getCursorPosition()) }, this.getSelectionRange = function () { return this.selection.getRange() }, this.selectAll = function () { this.selection.selectAll() }, this.clearSelection = function () { this.selection.clearSelection() }, this.moveCursorTo = function (e, t) { this.selection.moveCursorTo(e, t) }, this.moveCursorToPosition = function (e) { this.selection.moveCursorToPosition(e) }, this.jumpToMatching = function (e, t) { var n = this.getCursorPosition(), r = new y(this.session, n.row, n.column), i = r.getCurrentToken(), s = i || r.stepForward(); if (!s) return; var o, u = !1, a = {}, f = n.column - s.start, l, c = { ")": "(", "(": "(", "]": "[", "[": "[", "{": "{", "}": "{" }; do { if (s.value.match(/[{}()\[\]]/g)) for (; f < s.value.length && !u; f++) { if (!c[s.value[f]]) continue; l = c[s.value[f]] + "." + s.type.replace("rparen", "lparen"), isNaN(a[l]) && (a[l] = 0); switch (s.value[f]) { case "(": case "[": case "{": a[l]++; break; case ")": case "]": case "}": a[l]--, a[l] === -1 && (o = "bracket", u = !0) } } else s.type.indexOf("tag-name") !== -1 && (isNaN(a[s.value]) && (a[s.value] = 0), i.value === "<" ? a[s.value]++ : i.value === "= 0; --s)this.$tryReplace(n[s], e) && r++; return this.selection.setSelectionRange(i), r }, this.$tryReplace = function (e, t) { var n = this.session.getTextRange(e); return t = this.$search.replace(n, t), t !== null ? (e.end = this.session.replace(e, t), e) : null }, this.getLastSearchOptions = function () { return this.$search.getOptions() }, this.find = function (e, t, n) { t || (t = {}), typeof e == "string" || e instanceof RegExp ? t.needle = e : typeof e == "object" && r.mixin(t, e); var i = this.selection.getRange(); t.needle == null && (e = this.session.getTextRange(i) || this.$search.$options.needle, e || (i = this.session.getWordRange(i.start.row, i.start.column), e = this.session.getTextRange(i)), this.$search.set({ needle: e })), this.$search.set(t), t.start || this.$search.set({ start: i }); var s = this.$search.find(this.session); if (t.preventScroll) return s; if (s) return this.revealRange(s, n), s; t.backwards ? i.start = i.end : i.end = i.start, this.selection.setRange(i) }, this.findNext = function (e, t) { this.find({ skipCurrent: !0, backwards: !1 }, e, t) }, this.findPrevious = function (e, t) { this.find(e, { skipCurrent: !0, backwards: !0 }, t) }, this.revealRange = function (e, t) { this.session.unfold(e), this.selection.setSelectionRange(e); var n = this.renderer.scrollTop; this.renderer.scrollSelectionIntoView(e.start, e.end, .5), t !== !1 && this.renderer.animateScrolling(n) }, this.undo = function () { this.session.getUndoManager().undo(this.session), this.renderer.scrollCursorIntoView(null, .5) }, this.redo = function () { this.session.getUndoManager().redo(this.session), this.renderer.scrollCursorIntoView(null, .5) }, this.destroy = function () { this.$toDestroy && (this.$toDestroy.forEach(function (e) { e.destroy() }), this.$toDestroy = null), this.renderer.destroy(), this._signal("destroy", this), this.session && this.session.destroy(), this._$emitInputEvent && this._$emitInputEvent.cancel(), this.removeAllListeners() }, this.setAutoScrollEditorIntoView = function (e) { if (!e) return; var t, n = this, r = !1; this.$scrollAnchor || (this.$scrollAnchor = document.createElement("div")); var i = this.$scrollAnchor; i.style.cssText = "position:absolute", this.container.insertBefore(i, this.container.firstChild); var s = this.on("changeSelection", function () { r = !0 }), o = this.renderer.on("beforeRender", function () { r && (t = n.renderer.container.getBoundingClientRect()) }), u = this.renderer.on("afterRender", function () { if (r && t && (n.isFocused() || n.searchBox && n.searchBox.isFocused())) { var e = n.renderer, s = e.$cursorLayer.$pixelPos, o = e.layerConfig, u = s.top - o.offset; s.top >= 0 && u + t.top < 0 ? r = !0 : s.top < o.height && s.top + t.top + o.lineHeight > window.innerHeight ? r = !1 : r = null, r != null && (i.style.top = u + "px", i.style.left = s.left + "px", i.style.height = o.lineHeight + "px", i.scrollIntoView(r)), r = t = null } }); this.setAutoScrollEditorIntoView = function (e) { if (e) return; delete this.setAutoScrollEditorIntoView, this.off("changeSelection", s), this.renderer.off("afterRender", u), this.renderer.off("beforeRender", o) } }, this.$resetCursorStyle = function () { var e = this.$cursorStyle || "ace", t = this.renderer.$cursorLayer; if (!t) return; t.setSmoothBlinking(/smooth/.test(e)), t.isBlinking = !this.$readOnly && e != "wide", i.setCssClass(t.element, "ace_slim-cursors", /slim/.test(e)) }, this.prompt = function (e, t, n) { var r = this; g.loadModule("./ext/prompt", function (i) { i.prompt(r, e, t, n) }) } }.call(w.prototype), g.defineOptions(w.prototype, "editor", { selectionStyle: { set: function (e) { this.onSelectionChange(), this._signal("changeSelectionStyle", { data: e }) }, initialValue: "line" }, highlightActiveLine: { set: function () { this.$updateHighlightActiveLine() }, initialValue: !0 }, highlightSelectedWord: { set: function (e) { this.$onSelectionChange() }, initialValue: !0 }, readOnly: { set: function (e) { this.textInput.setReadOnly(e), this.$resetCursorStyle() }, initialValue: !1 }, copyWithEmptySelection: { set: function (e) { this.textInput.setCopyWithEmptySelection(e) }, initialValue: !1 }, cursorStyle: { set: function (e) { this.$resetCursorStyle() }, values: ["ace", "slim", "smooth", "wide"], initialValue: "ace" }, mergeUndoDeltas: { values: [!1, !0, "always"], initialValue: !0 }, behavioursEnabled: { initialValue: !0 }, wrapBehavioursEnabled: { initialValue: !0 }, enableAutoIndent: { initialValue: !0 }, autoScrollEditorIntoView: { set: function (e) { this.setAutoScrollEditorIntoView(e) } }, keyboardHandler: { set: function (e) { this.setKeyboardHandler(e) }, get: function () { return this.$keybindingId }, handlesSet: !0 }, value: { set: function (e) { this.session.setValue(e) }, get: function () { return this.getValue() }, handlesSet: !0, hidden: !0 }, session: { set: function (e) { this.setSession(e) }, get: function () { return this.session }, handlesSet: !0, hidden: !0 }, showLineNumbers: { set: function (e) { this.renderer.$gutterLayer.setShowLineNumbers(e), this.renderer.$loop.schedule(this.renderer.CHANGE_GUTTER), e && this.$relativeLineNumbers ? E.attach(this) : E.detach(this) }, initialValue: !0 }, relativeLineNumbers: { set: function (e) { this.$showLineNumbers && e ? E.attach(this) : E.detach(this) } }, placeholder: { set: function (e) { this.$updatePlaceholder || (this.$updatePlaceholder = function () { var e = this.session && (this.renderer.$composition || this.getValue()); if (e && this.renderer.placeholderNode) this.renderer.off("afterRender", this.$updatePlaceholder), i.removeCssClass(this.container, "ace_hasPlaceholder"), this.renderer.placeholderNode.remove(), this.renderer.placeholderNode = null; else if (!e && !this.renderer.placeholderNode) { this.renderer.on("afterRender", this.$updatePlaceholder), i.addCssClass(this.container, "ace_hasPlaceholder"); var t = i.createElement("div"); t.className = "ace_placeholder", t.textContent = this.$placeholder || "", this.renderer.placeholderNode = t, this.renderer.content.appendChild(this.renderer.placeholderNode) } else !e && this.renderer.placeholderNode && (this.renderer.placeholderNode.textContent = this.$placeholder || "") }.bind(this), this.on("input", this.$updatePlaceholder)), this.$updatePlaceholder() } }, hScrollBarAlwaysVisible: "renderer", vScrollBarAlwaysVisible: "renderer", highlightGutterLine: "renderer", animatedScroll: "renderer", showInvisibles: "renderer", showPrintMargin: "renderer", printMarginColumn: "renderer", printMargin: "renderer", fadeFoldWidgets: "renderer", showFoldWidgets: "renderer", displayIndentGuides: "renderer", showGutter: "renderer", fontSize: "renderer", fontFamily: "renderer", maxLines: "renderer", minLines: "renderer", scrollPastEnd: "renderer", fixedWidthGutter: "renderer", theme: "renderer", hasCssTransforms: "renderer", maxPixelHeight: "renderer", useTextareaForIME: "renderer", scrollSpeed: "$mouseHandler", dragDelay: "$mouseHandler", dragEnabled: "$mouseHandler", focusTimeout: "$mouseHandler", tooltipFollowsMouse: "$mouseHandler", firstLineNumber: "session", overwrite: "session", newLineMode: "session", useWorker: "session", useSoftTabs: "session", navigateWithinSoftTabs: "session", tabSize: "session", wrap: "session", indentedSoftWrap: "session", foldStyle: "session", mode: "session" }); var E = { getText: function (e, t) { return (Math.abs(e.selection.lead.row - t) || t + 1 + (t < 9 ? "\u00b7" : "")) + "" }, getWidth: function (e, t, n) { return Math.max(t.toString().length, (n.lastRow + 1).toString().length, 2) * n.characterWidth }, update: function (e, t) { t.renderer.$loop.schedule(t.renderer.CHANGE_GUTTER) }, attach: function (e) { e.renderer.$gutterLayer.$renderer = this, e.on("changeSelection", this.update), this.update(null, e) }, detach: function (e) { e.renderer.$gutterLayer.$renderer == this && (e.renderer.$gutterLayer.$renderer = null), e.off("changeSelection", this.update), this.update(null, e) } }; t.Editor = w }), define("ace/undomanager", ["require", "exports", "module", "ace/range"], function (e, t, n) { "use strict"; function i(e, t) { for (var n = t; n--;) { var r = e[n]; if (r && !r[0].ignore) { while (n < t - 1) { var i = d(e[n], e[n + 1]); e[n] = i[0], e[n + 1] = i[1], n++ } return !0 } } } function a(e) { var t = e.action == "insert", n = e.start, r = e.end, i = (r.row - n.row) * (t ? 1 : -1), s = (r.column - n.column) * (t ? 1 : -1); t && (r = n); for (var o in this.marks) { var a = this.marks[o], f = u(a, n); if (f < 0) continue; if (f === 0 && t) { if (a.bias != 1) { a.bias == -1; continue } f = 1 } var l = t ? f : u(a, r); if (l > 0) { a.row += i, a.column += a.row == r.row ? s : 0; continue } !t && l <= 0 && (a.row = n.row, a.column = n.column, l === 0 && (a.bias = 1)) } } function f(e) { return { row: e.row, column: e.column } } function l(e) { return { start: f(e.start), end: f(e.end), action: e.action, lines: e.lines.slice() } } function c(e) { e = e || this; if (Array.isArray(e)) return e.map(c).join("\n"); var t = ""; e.action ? (t = e.action == "insert" ? "+" : "-", t += "[" + e.lines + "]") : e.value && (Array.isArray(e.value) ? t = e.value.map(h).join("\n") : t = h(e.value)), e.start && (t += h(e)); if (e.id || e.rev) t += " (" + (e.id || e.rev) + ")"; return t } function h(e) { return e.start.row + ":" + e.start.column + "=>" + e.end.row + ":" + e.end.column } function p(e, t) { var n = e.action == "insert", r = t.action == "insert"; if (n && r) if (o(t.start, e.end) >= 0) m(t, e, -1); else { if (!(o(t.start, e.start) <= 0)) return null; m(e, t, 1) } else if (n && !r) if (o(t.start, e.end) >= 0) m(t, e, -1); else { if (!(o(t.end, e.start) <= 0)) return null; m(e, t, -1) } else if (!n && r) if (o(t.start, e.start) >= 0) m(t, e, 1); else { if (!(o(t.start, e.start) <= 0)) return null; m(e, t, 1) } else if (!n && !r) if (o(t.start, e.start) >= 0) m(t, e, 1); else { if (!(o(t.end, e.start) <= 0)) return null; m(e, t, -1) } return [t, e] } function d(e, t) { for (var n = e.length; n--;)for (var r = 0; r < t.length; r++)if (!p(e[n], t[r])) { while (n < e.length) { while (r--) p(t[r], e[n]); r = t.length, n++ } return [e, t] } return e.selectionBefore = t.selectionBefore = e.selectionAfter = t.selectionAfter = null, [t, e] } function v(e, t) { var n = e.action == "insert", r = t.action == "insert"; if (n && r) o(e.start, t.start) < 0 ? m(t, e, 1) : m(e, t, 1); else if (n && !r) o(e.start, t.end) >= 0 ? m(e, t, -1) : o(e.start, t.start) <= 0 ? m(t, e, 1) : (m(e, s.fromPoints(t.start, e.start), -1), m(t, e, 1)); else if (!n && r) o(t.start, e.end) >= 0 ? m(t, e, -1) : o(t.start, e.start) <= 0 ? m(e, t, 1) : (m(t, s.fromPoints(e.start, t.start), -1), m(e, t, 1)); else if (!n && !r) if (o(t.start, e.end) >= 0) m(t, e, -1); else { if (!(o(t.end, e.start) <= 0)) { var i, u; return o(e.start, t.start) < 0 && (i = e, e = y(e, t.start)), o(e.end, t.end) > 0 && (u = y(e, t.end)), g(t.end, e.start, e.end, -1), u && !i && (e.lines = u.lines, e.start = u.start, e.end = u.end, u = e), [t, i, u].filter(Boolean) } m(e, t, -1) } return [t, e] } function m(e, t, n) { g(e.start, t.start, t.end, n), g(e.end, t.start, t.end, n) } function g(e, t, n, r) { e.row == (r == 1 ? t : n).row && (e.column += r * (n.column - t.column)), e.row += r * (n.row - t.row) } function y(e, t) { var n = e.lines, r = e.end; e.end = f(t); var i = e.end.row - e.start.row, s = n.splice(i, n.length), o = i ? t.column : t.column - e.start.column; n.push(s[0].substring(0, o)), s[0] = s[0].substr(o); var u = { start: f(t), end: r, lines: s, action: e.action }; return u } function b(e, t) { t = l(t); for (var n = e.length; n--;) { var r = e[n]; for (var i = 0; i < r.length; i++) { var s = r[i], o = v(s, t); t = o[0], o.length != 2 && (o[2] ? (r.splice(i + 1, 1, o[1], o[2]), i++) : o[1] || (r.splice(i, 1), i--)) } r.length || e.splice(n, 1) } return e } function w(e, t) { for (var n = 0; n < t.length; n++) { var r = t[n]; for (var i = 0; i < r.length; i++)b(e, r[i]) } } var r = function () { this.$maxRev = 0, this.$fromUndo = !1, this.reset() }; (function () { this.addSession = function (e) { this.$session = e }, this.add = function (e, t, n) { if (this.$fromUndo) return; if (e == this.$lastDelta) return; this.$keepRedoStack || (this.$redoStack.length = 0); if (t === !1 || !this.lastDeltas) this.lastDeltas = [], this.$undoStack.push(this.lastDeltas), e.id = this.$rev = ++this.$maxRev; if (e.action == "remove" || e.action == "insert") this.$lastDelta = e; this.lastDeltas.push(e) }, this.addSelection = function (e, t) { this.selections.push({ value: e, rev: t || this.$rev }) }, this.startNewGroup = function () { return this.lastDeltas = null, this.$rev }, this.markIgnored = function (e, t) { t == null && (t = this.$rev + 1); var n = this.$undoStack; for (var r = n.length; r--;) { var i = n[r][0]; if (i.id <= e) break; i.id < t && (i.ignore = !0) } this.lastDeltas = null }, this.getSelection = function (e, t) { var n = this.selections; for (var r = n.length; r--;) { var i = n[r]; if (i.rev < e) return t && (i = n[r + 1]), i } }, this.getRevision = function () { return this.$rev }, this.getDeltas = function (e, t) { t == null && (t = this.$rev + 1); var n = this.$undoStack, r = null, i = 0; for (var s = n.length; s--;) { var o = n[s][0]; o.id < t && !r && (r = s + 1); if (o.id <= e) { i = s + 1; break } } return n.slice(i, r) }, this.getChangedRanges = function (e, t) { t == null && (t = this.$rev + 1) }, this.getChangedLines = function (e, t) { t == null && (t = this.$rev + 1) }, this.undo = function (e, t) { this.lastDeltas = null; var n = this.$undoStack; if (!i(n, n.length)) return; e || (e = this.$session), this.$redoStackBaseRev !== this.$rev && this.$redoStack.length && (this.$redoStack = []), this.$fromUndo = !0; var r = n.pop(), s = null; return r && (s = e.undoChanges(r, t), this.$redoStack.push(r), this.$syncRev()), this.$fromUndo = !1, s }, this.redo = function (e, t) { this.lastDeltas = null, e || (e = this.$session), this.$fromUndo = !0; if (this.$redoStackBaseRev != this.$rev) { var n = this.getDeltas(this.$redoStackBaseRev, this.$rev + 1); w(this.$redoStack, n), this.$redoStackBaseRev = this.$rev, this.$redoStack.forEach(function (e) { e[0].id = ++this.$maxRev }, this) } var r = this.$redoStack.pop(), i = null; return r && (i = e.redoChanges(r, t), this.$undoStack.push(r), this.$syncRev()), this.$fromUndo = !1, i }, this.$syncRev = function () { var e = this.$undoStack, t = e[e.length - 1], n = t && t[0].id || 0; this.$redoStackBaseRev = n, this.$rev = n }, this.reset = function () { this.lastDeltas = null, this.$lastDelta = null, this.$undoStack = [], this.$redoStack = [], this.$rev = 0, this.mark = 0, this.$redoStackBaseRev = this.$rev, this.selections = [] }, this.canUndo = function () { return this.$undoStack.length > 0 }, this.canRedo = function () { return this.$redoStack.length > 0 }, this.bookmark = function (e) { e == undefined && (e = this.$rev), this.mark = e }, this.isAtBookmark = function () { return this.$rev === this.mark }, this.toJSON = function () { }, this.fromJSON = function () { }, this.hasUndo = this.canUndo, this.hasRedo = this.canRedo, this.isClean = this.isAtBookmark, this.markClean = this.bookmark, this.$prettyPrint = function (e) { return e ? c(e) : c(this.$undoStack) + "\n---\n" + c(this.$redoStack) } }).call(r.prototype); var s = e("./range").Range, o = s.comparePoints, u = s.comparePoints; t.UndoManager = r }), define("ace/layer/lines", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { "use strict"; var r = e("../lib/dom"), i = function (e, t) { this.element = e, this.canvasHeight = t || 5e5, this.element.style.height = this.canvasHeight * 2 + "px", this.cells = [], this.cellCache = [], this.$offsetCoefficient = 0 }; (function () { this.moveContainer = function (e) { r.translate(this.element, 0, -(e.firstRowScreen * e.lineHeight % this.canvasHeight) - e.offset * this.$offsetCoefficient) }, this.pageChanged = function (e, t) { return Math.floor(e.firstRowScreen * e.lineHeight / this.canvasHeight) !== Math.floor(t.firstRowScreen * t.lineHeight / this.canvasHeight) }, this.computeLineTop = function (e, t, n) { var r = t.firstRowScreen * t.lineHeight, i = Math.floor(r / this.canvasHeight), s = n.documentToScreenRow(e, 0) * t.lineHeight; return s - i * this.canvasHeight }, this.computeLineHeight = function (e, t, n) { return t.lineHeight * n.getRowLineCount(e) }, this.getLength = function () { return this.cells.length }, this.get = function (e) { return this.cells[e] }, this.shift = function () { this.$cacheCell(this.cells.shift()) }, this.pop = function () { this.$cacheCell(this.cells.pop()) }, this.push = function (e) { if (Array.isArray(e)) { this.cells.push.apply(this.cells, e); var t = r.createFragment(this.element); for (var n = 0; n < e.length; n++)t.appendChild(e[n].element); this.element.appendChild(t) } else this.cells.push(e), this.element.appendChild(e.element) }, this.unshift = function (e) { if (Array.isArray(e)) { this.cells.unshift.apply(this.cells, e); var t = r.createFragment(this.element); for (var n = 0; n < e.length; n++)t.appendChild(e[n].element); this.element.firstChild ? this.element.insertBefore(t, this.element.firstChild) : this.element.appendChild(t) } else this.cells.unshift(e), this.element.insertAdjacentElement("afterbegin", e.element) }, this.last = function () { return this.cells.length ? this.cells[this.cells.length - 1] : null }, this.$cacheCell = function (e) { if (!e) return; e.element.remove(), this.cellCache.push(e) }, this.createCell = function (e, t, n, i) { var s = this.cellCache.pop(); if (!s) { var o = r.createElement("div"); i && i(o), this.element.appendChild(o), s = { element: o, text: "", row: e } } return s.row = e, s } }).call(i.prototype), t.Lines = i }), define("ace/layer/gutter", ["require", "exports", "module", "ace/lib/dom", "ace/lib/oop", "ace/lib/lang", "ace/lib/event_emitter", "ace/layer/lines"], function (e, t, n) { "use strict"; function f(e) { var t = document.createTextNode(""); e.appendChild(t); var n = r.createElement("span"); return e.appendChild(n), e } var r = e("../lib/dom"), i = e("../lib/oop"), s = e("../lib/lang"), o = e("../lib/event_emitter").EventEmitter, u = e("./lines").Lines, a = function (e) { this.element = r.createElement("div"), this.element.className = "ace_layer ace_gutter-layer", e.appendChild(this.element), this.setShowFoldWidgets(this.$showFoldWidgets), this.gutterWidth = 0, this.$annotations = [], this.$updateAnnotations = this.$updateAnnotations.bind(this), this.$lines = new u(this.element), this.$lines.$offsetCoefficient = 1 }; (function () { i.implement(this, o), this.setSession = function (e) { this.session && this.session.off("change", this.$updateAnnotations), this.session = e, e && e.on("change", this.$updateAnnotations) }, this.addGutterDecoration = function (e, t) { window.console && console.warn && console.warn("deprecated use session.addGutterDecoration"), this.session.addGutterDecoration(e, t) }, this.removeGutterDecoration = function (e, t) { window.console && console.warn && console.warn("deprecated use session.removeGutterDecoration"), this.session.removeGutterDecoration(e, t) }, this.setAnnotations = function (e) { this.$annotations = []; for (var t = 0; t < e.length; t++) { var n = e[t], r = n.row, i = this.$annotations[r]; i || (i = this.$annotations[r] = { text: [] }); var o = n.text; o = o ? s.escapeHTML(o) : n.html || "", i.text.indexOf(o) === -1 && i.text.push(o); var u = n.type; u == "error" ? i.className = " ace_error" : u == "warning" && i.className != " ace_error" ? i.className = " ace_warning" : u == "info" && !i.className && (i.className = " ace_info") } }, this.$updateAnnotations = function (e) { if (!this.$annotations.length) return; var t = e.start.row, n = e.end.row - t; if (n !== 0) if (e.action == "remove") this.$annotations.splice(t, n + 1, null); else { var r = new Array(n + 1); r.unshift(t, 1), this.$annotations.splice.apply(this.$annotations, r) } }, this.update = function (e) { this.config = e; var t = this.session, n = e.firstRow, r = Math.min(e.lastRow + e.gutterOffset, t.getLength() - 1); this.oldLastRow = r, this.config = e, this.$lines.moveContainer(e), this.$updateCursorRow(); var i = t.getNextFoldLine(n), s = i ? i.start.row : Infinity, o = null, u = -1, a = n; for (; ;) { a > s && (a = i.end.row + 1, i = t.getNextFoldLine(a, i), s = i ? i.start.row : Infinity); if (a > r) { while (this.$lines.getLength() > u + 1) this.$lines.pop(); break } o = this.$lines.get(++u), o ? o.row = a : (o = this.$lines.createCell(a, e, this.session, f), this.$lines.push(o)), this.$renderCell(o, e, i, a), a++ } this._signal("afterRender"), this.$updateGutterWidth(e) }, this.$updateGutterWidth = function (e) { var t = this.session, n = t.gutterRenderer || this.$renderer, r = t.$firstLineNumber, i = this.$lines.last() ? this.$lines.last().text : ""; if (this.$fixedWidth || t.$useWrapMode) i = t.getLength() + r - 1; var s = n ? n.getWidth(t, i, e) : i.toString().length * e.characterWidth, o = this.$padding || this.$computePadding(); s += o.left + o.right, s !== this.gutterWidth && !isNaN(s) && (this.gutterWidth = s, this.element.parentNode.style.width = this.element.style.width = Math.ceil(this.gutterWidth) + "px", this._signal("changeGutterWidth", s)) }, this.$updateCursorRow = function () { if (!this.$highlightGutterLine) return; var e = this.session.selection.getCursor(); if (this.$cursorRow === e.row) return; this.$cursorRow = e.row }, this.updateLineHighlight = function () { if (!this.$highlightGutterLine) return; var e = this.session.selection.cursor.row; this.$cursorRow = e; if (this.$cursorCell && this.$cursorCell.row == e) return; this.$cursorCell && (this.$cursorCell.element.className = this.$cursorCell.element.className.replace("ace_gutter-active-line ", "")); var t = this.$lines.cells; this.$cursorCell = null; for (var n = 0; n < t.length; n++) { var r = t[n]; if (r.row >= this.$cursorRow) { if (r.row > this.$cursorRow) { var i = this.session.getFoldLine(this.$cursorRow); if (!(n > 0 && i && i.start.row == t[n - 1].row)) break; r = t[n - 1] } r.element.className = "ace_gutter-active-line " + r.element.className, this.$cursorCell = r; break } } }, this.scrollLines = function (e) { var t = this.config; this.config = e, this.$updateCursorRow(); if (this.$lines.pageChanged(t, e)) return this.update(e); this.$lines.moveContainer(e); var n = Math.min(e.lastRow + e.gutterOffset, this.session.getLength() - 1), r = this.oldLastRow; this.oldLastRow = n; if (!t || r < e.firstRow) return this.update(e); if (n < t.firstRow) return this.update(e); if (t.firstRow < e.firstRow) for (var i = this.session.getFoldedRowCount(t.firstRow, e.firstRow - 1); i > 0; i--)this.$lines.shift(); if (r > n) for (var i = this.session.getFoldedRowCount(n + 1, r); i > 0; i--)this.$lines.pop(); e.firstRow < t.firstRow && this.$lines.unshift(this.$renderLines(e, e.firstRow, t.firstRow - 1)), n > r && this.$lines.push(this.$renderLines(e, r + 1, n)), this.updateLineHighlight(), this._signal("afterRender"), this.$updateGutterWidth(e) }, this.$renderLines = function (e, t, n) { var r = [], i = t, s = this.session.getNextFoldLine(i), o = s ? s.start.row : Infinity; for (; ;) { i > o && (i = s.end.row + 1, s = this.session.getNextFoldLine(i, s), o = s ? s.start.row : Infinity); if (i > n) break; var u = this.$lines.createCell(i, e, this.session, f); this.$renderCell(u, e, s, i), r.push(u), i++ } return r }, this.$renderCell = function (e, t, n, i) { var s = e.element, o = this.session, u = s.childNodes[0], a = s.childNodes[1], f = o.$firstLineNumber, l = o.$breakpoints, c = o.$decorations, h = o.gutterRenderer || this.$renderer, p = this.$showFoldWidgets && o.foldWidgets, d = n ? n.start.row : Number.MAX_VALUE, v = "ace_gutter-cell "; this.$highlightGutterLine && (i == this.$cursorRow || n && i < this.$cursorRow && i >= d && this.$cursorRow <= n.end.row) && (v += "ace_gutter-active-line ", this.$cursorCell != e && (this.$cursorCell && (this.$cursorCell.element.className = this.$cursorCell.element.className.replace("ace_gutter-active-line ", "")), this.$cursorCell = e)), l[i] && (v += l[i]), c[i] && (v += c[i]), this.$annotations[i] && (v += this.$annotations[i].className), s.className != v && (s.className = v); if (p) { var m = p[i]; m == null && (m = p[i] = o.getFoldWidget(i)) } if (m) { var v = "ace_fold-widget ace_" + m; m == "start" && i == d && i < n.end.row ? v += " ace_closed" : v += " ace_open", a.className != v && (a.className = v); var g = t.lineHeight + "px"; r.setStyle(a.style, "height", g), r.setStyle(a.style, "display", "inline-block") } else a && r.setStyle(a.style, "display", "none"); var y = (h ? h.getText(o, i) : i + f).toString(); return y !== u.data && (u.data = y), r.setStyle(e.element.style, "height", this.$lines.computeLineHeight(i, t, o) + "px"), r.setStyle(e.element.style, "top", this.$lines.computeLineTop(i, t, o) + "px"), e.text = y, e }, this.$fixedWidth = !1, this.$highlightGutterLine = !0, this.$renderer = "", this.setHighlightGutterLine = function (e) { this.$highlightGutterLine = e }, this.$showLineNumbers = !0, this.$renderer = "", this.setShowLineNumbers = function (e) { this.$renderer = !e && { getWidth: function () { return 0 }, getText: function () { return "" } } }, this.getShowLineNumbers = function () { return this.$showLineNumbers }, this.$showFoldWidgets = !0, this.setShowFoldWidgets = function (e) { e ? r.addCssClass(this.element, "ace_folding-enabled") : r.removeCssClass(this.element, "ace_folding-enabled"), this.$showFoldWidgets = e, this.$padding = null }, this.getShowFoldWidgets = function () { return this.$showFoldWidgets }, this.$computePadding = function () { if (!this.element.firstChild) return { left: 0, right: 0 }; var e = r.computedStyle(this.element.firstChild); return this.$padding = {}, this.$padding.left = (parseInt(e.borderLeftWidth) || 0) + (parseInt(e.paddingLeft) || 0) + 1, this.$padding.right = (parseInt(e.borderRightWidth) || 0) + (parseInt(e.paddingRight) || 0), this.$padding }, this.getRegion = function (e) { var t = this.$padding || this.$computePadding(), n = this.element.getBoundingClientRect(); if (e.x < t.left + n.left) return "markers"; if (this.$showFoldWidgets && e.x > n.right - t.right) return "foldWidgets" } }).call(a.prototype), t.Gutter = a }), define("ace/layer/marker", ["require", "exports", "module", "ace/range", "ace/lib/dom"], function (e, t, n) { "use strict"; var r = e("../range").Range, i = e("../lib/dom"), s = function (e) { this.element = i.createElement("div"), this.element.className = "ace_layer ace_marker-layer", e.appendChild(this.element) }; (function () { function e(e, t, n, r) { return (e ? 1 : 0) | (t ? 2 : 0) | (n ? 4 : 0) | (r ? 8 : 0) } this.$padding = 0, this.setPadding = function (e) { this.$padding = e }, this.setSession = function (e) { this.session = e }, this.setMarkers = function (e) { this.markers = e }, this.elt = function (e, t) { var n = this.i != -1 && this.element.childNodes[this.i]; n ? this.i++ : (n = document.createElement("div"), this.element.appendChild(n), this.i = -1), n.style.cssText = t, n.className = e }, this.update = function (e) { if (!e) return; this.config = e, this.i = 0; var t; for (var n in this.markers) { var r = this.markers[n]; if (!r.range) { r.update(t, this, this.session, e); continue } var i = r.range.clipRows(e.firstRow, e.lastRow); if (i.isEmpty()) continue; i = i.toScreenRange(this.session); if (r.renderer) { var s = this.$getTop(i.start.row, e), o = this.$padding + i.start.column * e.characterWidth; r.renderer(t, i, o, s, e) } else r.type == "fullLine" ? this.drawFullLineMarker(t, i, r.clazz, e) : r.type == "screenLine" ? this.drawScreenLineMarker(t, i, r.clazz, e) : i.isMultiLine() ? r.type == "text" ? this.drawTextMarker(t, i, r.clazz, e) : this.drawMultiLineMarker(t, i, r.clazz, e) : this.drawSingleLineMarker(t, i, r.clazz + " ace_start" + " ace_br15", e) } if (this.i != -1) while (this.i < this.element.childElementCount) this.element.removeChild(this.element.lastChild) }, this.$getTop = function (e, t) { return (e - t.firstRowScreen) * t.lineHeight }, this.drawTextMarker = function (t, n, i, s, o) { var u = this.session, a = n.start.row, f = n.end.row, l = a, c = 0, h = 0, p = u.getScreenLastRowColumn(l), d = new r(l, n.start.column, l, h); for (; l <= f; l++)d.start.row = d.end.row = l, d.start.column = l == a ? n.start.column : u.getRowWrapIndent(l), d.end.column = p, c = h, h = p, p = l + 1 < f ? u.getScreenLastRowColumn(l + 1) : l == f ? 0 : n.end.column, this.drawSingleLineMarker(t, d, i + (l == a ? " ace_start" : "") + " ace_br" + e(l == a || l == a + 1 && n.start.column, c < h, h > p, l == f), s, l == f ? 0 : 1, o) }, this.drawMultiLineMarker = function (e, t, n, r, i) { var s = this.$padding, o = r.lineHeight, u = this.$getTop(t.start.row, r), a = s + t.start.column * r.characterWidth; i = i || ""; if (this.session.$bidiHandler.isBidiRow(t.start.row)) { var f = t.clone(); f.end.row = f.start.row, f.end.column = this.session.getLine(f.start.row).length, this.drawBidiSingleLineMarker(e, f, n + " ace_br1 ace_start", r, null, i) } else this.elt(n + " ace_br1 ace_start", "height:" + o + "px;" + "right:0;" + "top:" + u + "px;left:" + a + "px;" + (i || "")); if (this.session.$bidiHandler.isBidiRow(t.end.row)) { var f = t.clone(); f.start.row = f.end.row, f.start.column = 0, this.drawBidiSingleLineMarker(e, f, n + " ace_br12", r, null, i) } else { u = this.$getTop(t.end.row, r); var l = t.end.column * r.characterWidth; this.elt(n + " ace_br12", "height:" + o + "px;" + "width:" + l + "px;" + "top:" + u + "px;" + "left:" + s + "px;" + (i || "")) } o = (t.end.row - t.start.row - 1) * r.lineHeight; if (o <= 0) return; u = this.$getTop(t.start.row + 1, r); var c = (t.start.column ? 1 : 0) | (t.end.column ? 0 : 8); this.elt(n + (c ? " ace_br" + c : ""), "height:" + o + "px;" + "right:0;" + "top:" + u + "px;" + "left:" + s + "px;" + (i || "")) }, this.drawSingleLineMarker = function (e, t, n, r, i, s) { if (this.session.$bidiHandler.isBidiRow(t.start.row)) return this.drawBidiSingleLineMarker(e, t, n, r, i, s); var o = r.lineHeight, u = (t.end.column + (i || 0) - t.start.column) * r.characterWidth, a = this.$getTop(t.start.row, r), f = this.$padding + t.start.column * r.characterWidth; this.elt(n, "height:" + o + "px;" + "width:" + u + "px;" + "top:" + a + "px;" + "left:" + f + "px;" + (s || "")) }, this.drawBidiSingleLineMarker = function (e, t, n, r, i, s) { var o = r.lineHeight, u = this.$getTop(t.start.row, r), a = this.$padding, f = this.session.$bidiHandler.getSelections(t.start.column, t.end.column); f.forEach(function (e) { this.elt(n, "height:" + o + "px;" + "width:" + e.width + (i || 0) + "px;" + "top:" + u + "px;" + "left:" + (a + e.left) + "px;" + (s || "")) }, this) }, this.drawFullLineMarker = function (e, t, n, r, i) { var s = this.$getTop(t.start.row, r), o = r.lineHeight; t.start.row != t.end.row && (o += this.$getTop(t.end.row, r) - s), this.elt(n, "height:" + o + "px;" + "top:" + s + "px;" + "left:0;right:0;" + (i || "")) }, this.drawScreenLineMarker = function (e, t, n, r, i) { var s = this.$getTop(t.start.row, r), o = r.lineHeight; this.elt(n, "height:" + o + "px;" + "top:" + s + "px;" + "left:0;right:0;" + (i || "")) } }).call(s.prototype), t.Marker = s }), define("ace/layer/text", ["require", "exports", "module", "ace/lib/oop", "ace/lib/dom", "ace/lib/lang", "ace/layer/lines", "ace/lib/event_emitter"], function (e, t, n) { "use strict"; var r = e("../lib/oop"), i = e("../lib/dom"), s = e("../lib/lang"), o = e("./lines").Lines, u = e("../lib/event_emitter").EventEmitter, a = function (e) { this.dom = i, this.element = this.dom.createElement("div"), this.element.className = "ace_layer ace_text-layer", e.appendChild(this.element), this.$updateEolChar = this.$updateEolChar.bind(this), this.$lines = new o(this.element) }; (function () { r.implement(this, u), this.EOF_CHAR = "\u00b6", this.EOL_CHAR_LF = "\u00ac", this.EOL_CHAR_CRLF = "\u00a4", this.EOL_CHAR = this.EOL_CHAR_LF, this.TAB_CHAR = "\u2014", this.SPACE_CHAR = "\u00b7", this.$padding = 0, this.MAX_LINE_LENGTH = 1e4, this.$updateEolChar = function () { var e = this.session.doc, t = e.getNewLineCharacter() == "\n" && e.getNewLineMode() != "windows", n = t ? this.EOL_CHAR_LF : this.EOL_CHAR_CRLF; if (this.EOL_CHAR != n) return this.EOL_CHAR = n, !0 }, this.setPadding = function (e) { this.$padding = e, this.element.style.margin = "0 " + e + "px" }, this.getLineHeight = function () { return this.$fontMetrics.$characterSize.height || 0 }, this.getCharacterWidth = function () { return this.$fontMetrics.$characterSize.width || 0 }, this.$setFontMetrics = function (e) { this.$fontMetrics = e, this.$fontMetrics.on("changeCharacterSize", function (e) { this._signal("changeCharacterSize", e) }.bind(this)), this.$pollSizeChanges() }, this.checkForSizeChanges = function () { this.$fontMetrics.checkForSizeChanges() }, this.$pollSizeChanges = function () { return this.$pollSizeChangesTimer = this.$fontMetrics.$pollSizeChanges() }, this.setSession = function (e) { this.session = e, e && this.$computeTabString() }, this.showInvisibles = !1, this.showSpaces = !1, this.showTabs = !1, this.showEOL = !1, this.setShowInvisibles = function (e) { return this.showInvisibles == e ? !1 : (this.showInvisibles = e, typeof e == "string" ? (this.showSpaces = /tab/i.test(e), this.showTabs = /space/i.test(e), this.showEOL = /eol/i.test(e)) : this.showSpaces = this.showTabs = this.showEOL = e, this.$computeTabString(), !0) }, this.displayIndentGuides = !0, this.setDisplayIndentGuides = function (e) { return this.displayIndentGuides == e ? !1 : (this.displayIndentGuides = e, this.$computeTabString(), !0) }, this.$tabStrings = [], this.onChangeTabSize = this.$computeTabString = function () { var e = this.session.getTabSize(); this.tabSize = e; var t = this.$tabStrings = [0]; for (var n = 1; n < e + 1; n++)if (this.showTabs) { var r = this.dom.createElement("span"); r.className = "ace_invisible ace_invisible_tab", r.textContent = s.stringRepeat(this.TAB_CHAR, n), t.push(r) } else t.push(this.dom.createTextNode(s.stringRepeat(" ", n), this.element)); if (this.displayIndentGuides) { this.$indentGuideRe = /\s\S| \t|\t |\s$/; var i = "ace_indent-guide", o = this.showSpaces ? " ace_invisible ace_invisible_space" : "", u = this.showSpaces ? s.stringRepeat(this.SPACE_CHAR, this.tabSize) : s.stringRepeat(" ", this.tabSize), a = this.showTabs ? " ace_invisible ace_invisible_tab" : "", f = this.showTabs ? s.stringRepeat(this.TAB_CHAR, this.tabSize) : u, r = this.dom.createElement("span"); r.className = i + o, r.textContent = u, this.$tabStrings[" "] = r; var r = this.dom.createElement("span"); r.className = i + a, r.textContent = f, this.$tabStrings[" "] = r } }, this.updateLines = function (e, t, n) { if (this.config.lastRow != e.lastRow || this.config.firstRow != e.firstRow) return this.update(e); this.config = e; var r = Math.max(t, e.firstRow), i = Math.min(n, e.lastRow), s = this.element.childNodes, o = 0; for (var u = e.firstRow; u < r; u++) { var a = this.session.getFoldLine(u); if (a) { if (a.containsRow(r)) { r = a.start.row; break } u = a.end.row } o++ } var f = !1, u = r, a = this.session.getNextFoldLine(u), l = a ? a.start.row : Infinity; for (; ;) { u > l && (u = a.end.row + 1, a = this.session.getNextFoldLine(u, a), l = a ? a.start.row : Infinity); if (u > i) break; var c = s[o++]; if (c) { this.dom.removeChildren(c), this.$renderLine(c, u, u == l ? a : !1), f && (c.style.top = this.$lines.computeLineTop(u, e, this.session) + "px"); var h = e.lineHeight * this.session.getRowLength(u) + "px"; c.style.height != h && (f = !0, c.style.height = h) } u++ } if (f) while (o < this.$lines.cells.length) { var p = this.$lines.cells[o++]; p.element.style.top = this.$lines.computeLineTop(p.row, e, this.session) + "px" } }, this.scrollLines = function (e) { var t = this.config; this.config = e; if (this.$lines.pageChanged(t, e)) return this.update(e); this.$lines.moveContainer(e); var n = e.lastRow, r = t ? t.lastRow : -1; if (!t || r < e.firstRow) return this.update(e); if (n < t.firstRow) return this.update(e); if (!t || t.lastRow < e.firstRow) return this.update(e); if (e.lastRow < t.firstRow) return this.update(e); if (t.firstRow < e.firstRow) for (var i = this.session.getFoldedRowCount(t.firstRow, e.firstRow - 1); i > 0; i--)this.$lines.shift(); if (t.lastRow > e.lastRow) for (var i = this.session.getFoldedRowCount(e.lastRow + 1, t.lastRow); i > 0; i--)this.$lines.pop(); e.firstRow < t.firstRow && this.$lines.unshift(this.$renderLinesFragment(e, e.firstRow, t.firstRow - 1)), e.lastRow > t.lastRow && this.$lines.push(this.$renderLinesFragment(e, t.lastRow + 1, e.lastRow)) }, this.$renderLinesFragment = function (e, t, n) { var r = [], s = t, o = this.session.getNextFoldLine(s), u = o ? o.start.row : Infinity; for (; ;) { s > u && (s = o.end.row + 1, o = this.session.getNextFoldLine(s, o), u = o ? o.start.row : Infinity); if (s > n) break; var a = this.$lines.createCell(s, e, this.session), f = a.element; this.dom.removeChildren(f), i.setStyle(f.style, "height", this.$lines.computeLineHeight(s, e, this.session) + "px"), i.setStyle(f.style, "top", this.$lines.computeLineTop(s, e, this.session) + "px"), this.$renderLine(f, s, s == u ? o : !1), this.$useLineGroups() ? f.className = "ace_line_group" : f.className = "ace_line", r.push(a), s++ } return r }, this.update = function (e) { this.$lines.moveContainer(e), this.config = e; var t = e.firstRow, n = e.lastRow, r = this.$lines; while (r.getLength()) r.pop(); r.push(this.$renderLinesFragment(e, t, n)) }, this.$textToken = { text: !0, rparen: !0, lparen: !0 }, this.$renderToken = function (e, t, n, r) { var i = this, o = /(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC]+)|(\u3000)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]|[\uD800-\uDBFF][\uDC00-\uDFFF])/g, u = this.dom.createFragment(this.element), a, f = 0; while (a = o.exec(r)) { var l = a[1], c = a[2], h = a[3], p = a[4], d = a[5]; if (!i.showSpaces && c) continue; var v = f != a.index ? r.slice(f, a.index) : ""; f = a.index + a[0].length, v && u.appendChild(this.dom.createTextNode(v, this.element)); if (l) { var m = i.session.getScreenTabSize(t + a.index); u.appendChild(i.$tabStrings[m].cloneNode(!0)), t += m - 1 } else if (c) if (i.showSpaces) { var g = this.dom.createElement("span"); g.className = "ace_invisible ace_invisible_space", g.textContent = s.stringRepeat(i.SPACE_CHAR, c.length), u.appendChild(g) } else u.appendChild(this.com.createTextNode(c, this.element)); else if (h) { var g = this.dom.createElement("span"); g.className = "ace_invisible ace_invisible_space ace_invalid", g.textContent = s.stringRepeat(i.SPACE_CHAR, h.length), u.appendChild(g) } else if (p) { t += 1; var g = this.dom.createElement("span"); g.style.width = i.config.characterWidth * 2 + "px", g.className = i.showSpaces ? "ace_cjk ace_invisible ace_invisible_space" : "ace_cjk", g.textContent = i.showSpaces ? i.SPACE_CHAR : p, u.appendChild(g) } else if (d) { t += 1; var g = this.dom.createElement("span"); g.style.width = i.config.characterWidth * 2 + "px", g.className = "ace_cjk", g.textContent = d, u.appendChild(g) } } u.appendChild(this.dom.createTextNode(f ? r.slice(f) : r, this.element)); if (!this.$textToken[n.type]) { var y = "ace_" + n.type.replace(/\./g, " ace_"), g = this.dom.createElement("span"); n.type == "fold" && (g.style.width = n.value.length * this.config.characterWidth + "px"), g.className = y, g.appendChild(u), e.appendChild(g) } else e.appendChild(u); return t + r.length }, this.renderIndentGuide = function (e, t, n) { var r = t.search(this.$indentGuideRe); if (r <= 0 || r >= n) return t; if (t[0] == " ") { r -= r % this.tabSize; var i = r / this.tabSize; for (var s = 0; s < i; s++)e.appendChild(this.$tabStrings[" "].cloneNode(!0)); return t.substr(r) } if (t[0] == " ") { for (var s = 0; s < r; s++)e.appendChild(this.$tabStrings[" "].cloneNode(!0)); return t.substr(r) } return t }, this.$createLineElement = function (e) { var t = this.dom.createElement("div"); return t.className = "ace_line", t.style.height = this.config.lineHeight + "px", t }, this.$renderWrappedLine = function (e, t, n) { var r = 0, i = 0, o = n[0], u = 0, a = this.$createLineElement(); e.appendChild(a); for (var f = 0; f < t.length; f++) { var l = t[f], c = l.value; if (f == 0 && this.displayIndentGuides) { r = c.length, c = this.renderIndentGuide(a, c, o); if (!c) continue; r -= c.length } if (r + c.length < o) u = this.$renderToken(a, u, l, c), r += c.length; else { while (r + c.length >= o) u = this.$renderToken(a, u, l, c.substring(0, o - r)), c = c.substring(o - r), r = o, a = this.$createLineElement(), e.appendChild(a), a.appendChild(this.dom.createTextNode(s.stringRepeat("\u00a0", n.indent), this.element)), i++, u = 0, o = n[i] || Number.MAX_VALUE; c.length != 0 && (r += c.length, u = this.$renderToken(a, u, l, c)) } } n[n.length - 1] > this.MAX_LINE_LENGTH && this.$renderOverflowMessage(a, u, null, "", !0) }, this.$renderSimpleLine = function (e, t) { var n = 0, r = t[0], i = r.value; this.displayIndentGuides && (i = this.renderIndentGuide(e, i)), i && (n = this.$renderToken(e, n, r, i)); for (var s = 1; s < t.length; s++) { r = t[s], i = r.value; if (n + i.length > this.MAX_LINE_LENGTH) return this.$renderOverflowMessage(e, n, r, i); n = this.$renderToken(e, n, r, i) } }, this.$renderOverflowMessage = function (e, t, n, r, i) { n && this.$renderToken(e, t, n, r.slice(0, this.MAX_LINE_LENGTH - t)); var s = this.dom.createElement("span"); s.className = "ace_inline_button ace_keyword ace_toggle_wrap", s.textContent = i ? "" : "", e.appendChild(s) }, this.$renderLine = function (e, t, n) { !n && n != 0 && (n = this.session.getFoldLine(t)); if (n) var r = this.$getFoldLineTokens(t, n); else var r = this.session.getTokens(t); var i = e; if (r.length) { var s = this.session.getRowSplitData(t); if (s && s.length) { this.$renderWrappedLine(e, r, s); var i = e.lastChild } else { var i = e; this.$useLineGroups() && (i = this.$createLineElement(), e.appendChild(i)), this.$renderSimpleLine(i, r) } } else this.$useLineGroups() && (i = this.$createLineElement(), e.appendChild(i)); if (this.showEOL && i) { n && (t = n.end.row); var o = this.dom.createElement("span"); o.className = "ace_invisible ace_invisible_eol", o.textContent = t == this.session.getLength() - 1 ? this.EOF_CHAR : this.EOL_CHAR, i.appendChild(o) } }, this.$getFoldLineTokens = function (e, t) { function i(e, t, n) { var i = 0, s = 0; while (s + e[i].value.length < t) { s += e[i].value.length, i++; if (i == e.length) return } if (s != t) { var o = e[i].value.substring(t - s); o.length > n - t && (o = o.substring(0, n - t)), r.push({ type: e[i].type, value: o }), s = t + o.length, i += 1 } while (s < n && i < e.length) { var o = e[i].value; o.length + s > n ? r.push({ type: e[i].type, value: o.substring(0, n - s) }) : r.push(e[i]), s += o.length, i += 1 } } var n = this.session, r = [], s = n.getTokens(e); return t.walk(function (e, t, o, u, a) { e != null ? r.push({ type: "fold", value: e }) : (a && (s = n.getTokens(t)), s.length && i(s, u, o)) }, t.end.row, this.session.getLine(t.end.row).length), r }, this.$useLineGroups = function () { return this.session.getUseWrapMode() }, this.destroy = function () { } }).call(a.prototype), t.Text = a }), define("ace/layer/cursor", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { "use strict"; var r = e("../lib/dom"), i = function (e) { this.element = r.createElement("div"), this.element.className = "ace_layer ace_cursor-layer", e.appendChild(this.element), this.isVisible = !1, this.isBlinking = !0, this.blinkInterval = 1e3, this.smoothBlinking = !1, this.cursors = [], this.cursor = this.addCursor(), r.addCssClass(this.element, "ace_hidden-cursors"), this.$updateCursors = this.$updateOpacity.bind(this) }; (function () { this.$updateOpacity = function (e) { var t = this.cursors; for (var n = t.length; n--;)r.setStyle(t[n].style, "opacity", e ? "" : "0") }, this.$startCssAnimation = function () { var e = this.cursors; for (var t = e.length; t--;)e[t].style.animationDuration = this.blinkInterval + "ms"; setTimeout(function () { r.addCssClass(this.element, "ace_animate-blinking") }.bind(this)) }, this.$stopCssAnimation = function () { r.removeCssClass(this.element, "ace_animate-blinking") }, this.$padding = 0, this.setPadding = function (e) { this.$padding = e }, this.setSession = function (e) { this.session = e }, this.setBlinking = function (e) { e != this.isBlinking && (this.isBlinking = e, this.restartTimer()) }, this.setBlinkInterval = function (e) { e != this.blinkInterval && (this.blinkInterval = e, this.restartTimer()) }, this.setSmoothBlinking = function (e) { e != this.smoothBlinking && (this.smoothBlinking = e, r.setCssClass(this.element, "ace_smooth-blinking", e), this.$updateCursors(!0), this.restartTimer()) }, this.addCursor = function () { var e = r.createElement("div"); return e.className = "ace_cursor", this.element.appendChild(e), this.cursors.push(e), e }, this.removeCursor = function () { if (this.cursors.length > 1) { var e = this.cursors.pop(); return e.parentNode.removeChild(e), e } }, this.hideCursor = function () { this.isVisible = !1, r.addCssClass(this.element, "ace_hidden-cursors"), this.restartTimer() }, this.showCursor = function () { this.isVisible = !0, r.removeCssClass(this.element, "ace_hidden-cursors"), this.restartTimer() }, this.restartTimer = function () { var e = this.$updateCursors; clearInterval(this.intervalId), clearTimeout(this.timeoutId), this.$stopCssAnimation(), this.smoothBlinking && r.removeCssClass(this.element, "ace_smooth-blinking"), e(!0); if (!this.isBlinking || !this.blinkInterval || !this.isVisible) { this.$stopCssAnimation(); return } this.smoothBlinking && setTimeout(function () { r.addCssClass(this.element, "ace_smooth-blinking") }.bind(this)); if (r.HAS_CSS_ANIMATION) this.$startCssAnimation(); else { var t = function () { this.timeoutId = setTimeout(function () { e(!1) }, .6 * this.blinkInterval) }.bind(this); this.intervalId = setInterval(function () { e(!0), t() }, this.blinkInterval), t() } }, this.getPixelPosition = function (e, t) { if (!this.config || !this.session) return { left: 0, top: 0 }; e || (e = this.session.selection.getCursor()); var n = this.session.documentToScreenPosition(e), r = this.$padding + (this.session.$bidiHandler.isBidiRow(n.row, e.row) ? this.session.$bidiHandler.getPosLeft(n.column) : n.column * this.config.characterWidth), i = (n.row - (t ? this.config.firstRowScreen : 0)) * this.config.lineHeight; return { left: r, top: i } }, this.isCursorInView = function (e, t) { return e.top >= 0 && e.top < t.maxHeight }, this.update = function (e) { this.config = e; var t = this.session.$selectionMarkers, n = 0, i = 0; if (t === undefined || t.length === 0) t = [{ cursor: null }]; for (var n = 0, s = t.length; n < s; n++) { var o = this.getPixelPosition(t[n].cursor, !0); if ((o.top > e.height + e.offset || o.top < 0) && n > 1) continue; var u = this.cursors[i++] || this.addCursor(), a = u.style; this.drawCursor ? this.drawCursor(u, o, e, t[n], this.session) : this.isCursorInView(o, e) ? (r.setStyle(a, "display", "block"), r.translate(u, o.left, o.top), r.setStyle(a, "width", Math.round(e.characterWidth) + "px"), r.setStyle(a, "height", e.lineHeight + "px")) : r.setStyle(a, "display", "none") } while (this.cursors.length > i) this.removeCursor(); var f = this.session.getOverwrite(); this.$setOverwrite(f), this.$pixelPos = o, this.restartTimer() }, this.drawCursor = null, this.$setOverwrite = function (e) { e != this.overwrite && (this.overwrite = e, e ? r.addCssClass(this.element, "ace_overwrite-cursors") : r.removeCssClass(this.element, "ace_overwrite-cursors")) }, this.destroy = function () { clearInterval(this.intervalId), clearTimeout(this.timeoutId) } }).call(i.prototype), t.Cursor = i }), define("ace/scrollbar", ["require", "exports", "module", "ace/lib/oop", "ace/lib/dom", "ace/lib/event", "ace/lib/event_emitter"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./lib/dom"), s = e("./lib/event"), o = e("./lib/event_emitter").EventEmitter, u = 32768, a = function (e) { this.element = i.createElement("div"), this.element.className = "ace_scrollbar ace_scrollbar" + this.classSuffix, this.inner = i.createElement("div"), this.inner.className = "ace_scrollbar-inner", this.inner.textContent = "\u00a0", this.element.appendChild(this.inner), e.appendChild(this.element), this.setVisible(!1), this.skipEvent = !1, s.addListener(this.element, "scroll", this.onScroll.bind(this)), s.addListener(this.element, "mousedown", s.preventDefault) }; (function () { r.implement(this, o), this.setVisible = function (e) { this.element.style.display = e ? "" : "none", this.isVisible = e, this.coeff = 1 } }).call(a.prototype); var f = function (e, t) { a.call(this, e), this.scrollTop = 0, this.scrollHeight = 0, t.$scrollbarWidth = this.width = i.scrollbarWidth(e.ownerDocument), this.inner.style.width = this.element.style.width = (this.width || 15) + 5 + "px", this.$minWidth = 0 }; r.inherits(f, a), function () { this.classSuffix = "-v", this.onScroll = function () { if (!this.skipEvent) { this.scrollTop = this.element.scrollTop; if (this.coeff != 1) { var e = this.element.clientHeight / this.scrollHeight; this.scrollTop = this.scrollTop * (1 - e) / (this.coeff - e) } this._emit("scroll", { data: this.scrollTop }) } this.skipEvent = !1 }, this.getWidth = function () { return Math.max(this.isVisible ? this.width : 0, this.$minWidth || 0) }, this.setHeight = function (e) { this.element.style.height = e + "px" }, this.setInnerHeight = this.setScrollHeight = function (e) { this.scrollHeight = e, e > u ? (this.coeff = u / e, e = u) : this.coeff != 1 && (this.coeff = 1), this.inner.style.height = e + "px" }, this.setScrollTop = function (e) { this.scrollTop != e && (this.skipEvent = !0, this.scrollTop = e, this.element.scrollTop = e * this.coeff) } }.call(f.prototype); var l = function (e, t) { a.call(this, e), this.scrollLeft = 0, this.height = t.$scrollbarWidth, this.inner.style.height = this.element.style.height = (this.height || 15) + 5 + "px" }; r.inherits(l, a), function () { this.classSuffix = "-h", this.onScroll = function () { this.skipEvent || (this.scrollLeft = this.element.scrollLeft, this._emit("scroll", { data: this.scrollLeft })), this.skipEvent = !1 }, this.getHeight = function () { return this.isVisible ? this.height : 0 }, this.setWidth = function (e) { this.element.style.width = e + "px" }, this.setInnerWidth = function (e) { this.inner.style.width = e + "px" }, this.setScrollWidth = function (e) { this.inner.style.width = e + "px" }, this.setScrollLeft = function (e) { this.scrollLeft != e && (this.skipEvent = !0, this.scrollLeft = this.element.scrollLeft = e) } }.call(l.prototype), t.ScrollBar = f, t.ScrollBarV = f, t.ScrollBarH = l, t.VScrollBar = f, t.HScrollBar = l }), define("ace/renderloop", ["require", "exports", "module", "ace/lib/event"], function (e, t, n) { "use strict"; var r = e("./lib/event"), i = function (e, t) { this.onRender = e, this.pending = !1, this.changes = 0, this.$recursionLimit = 2, this.window = t || window; var n = this; this._flush = function (e) { n.pending = !1; var t = n.changes; t && (r.blockIdle(100), n.changes = 0, n.onRender(t)); if (n.changes) { if (n.$recursionLimit-- < 0) return; n.schedule() } else n.$recursionLimit = 2 } }; (function () { this.schedule = function (e) { this.changes = this.changes | e, this.changes && !this.pending && (r.nextFrame(this._flush), this.pending = !0) }, this.clear = function (e) { var t = this.changes; return this.changes = 0, t } }).call(i.prototype), t.RenderLoop = i }), define("ace/layer/font_metrics", ["require", "exports", "module", "ace/lib/oop", "ace/lib/dom", "ace/lib/lang", "ace/lib/event", "ace/lib/useragent", "ace/lib/event_emitter"], function (e, t, n) { var r = e("../lib/oop"), i = e("../lib/dom"), s = e("../lib/lang"), o = e("../lib/event"), u = e("../lib/useragent"), a = e("../lib/event_emitter").EventEmitter, f = 256, l = typeof ResizeObserver == "function", c = 200, h = t.FontMetrics = function (e) { this.el = i.createElement("div"), this.$setMeasureNodeStyles(this.el.style, !0), this.$main = i.createElement("div"), this.$setMeasureNodeStyles(this.$main.style), this.$measureNode = i.createElement("div"), this.$setMeasureNodeStyles(this.$measureNode.style), this.el.appendChild(this.$main), this.el.appendChild(this.$measureNode), e.appendChild(this.el), this.$measureNode.textContent = s.stringRepeat("X", f), this.$characterSize = { width: 0, height: 0 }, l ? this.$addObserver() : this.checkForSizeChanges() }; (function () { r.implement(this, a), this.$characterSize = { width: 0, height: 0 }, this.$setMeasureNodeStyles = function (e, t) { e.width = e.height = "auto", e.left = e.top = "0px", e.visibility = "hidden", e.position = "absolute", e.whiteSpace = "pre", u.isIE < 8 ? e["font-family"] = "inherit" : e.font = "inherit", e.overflow = t ? "hidden" : "visible" }, this.checkForSizeChanges = function (e) { e === undefined && (e = this.$measureSizes()); if (e && (this.$characterSize.width !== e.width || this.$characterSize.height !== e.height)) { this.$measureNode.style.fontWeight = "bold"; var t = this.$measureSizes(); this.$measureNode.style.fontWeight = "", this.$characterSize = e, this.charSizes = Object.create(null), this.allowBoldFonts = t && t.width === e.width && t.height === e.height, this._emit("changeCharacterSize", { data: e }) } }, this.$addObserver = function () { var e = this; this.$observer = new window.ResizeObserver(function (t) { e.checkForSizeChanges() }), this.$observer.observe(this.$measureNode) }, this.$pollSizeChanges = function () { if (this.$pollSizeChangesTimer || this.$observer) return this.$pollSizeChangesTimer; var e = this; return this.$pollSizeChangesTimer = o.onIdle(function t() { e.checkForSizeChanges(), o.onIdle(t, 500) }, 500) }, this.setPolling = function (e) { e ? this.$pollSizeChanges() : this.$pollSizeChangesTimer && (clearInterval(this.$pollSizeChangesTimer), this.$pollSizeChangesTimer = 0) }, this.$measureSizes = function (e) { var t = { height: (e || this.$measureNode).clientHeight, width: (e || this.$measureNode).clientWidth / f }; return t.width === 0 || t.height === 0 ? null : t }, this.$measureCharWidth = function (e) { this.$main.textContent = s.stringRepeat(e, f); var t = this.$main.getBoundingClientRect(); return t.width / f }, this.getCharacterWidth = function (e) { var t = this.charSizes[e]; return t === undefined && (t = this.charSizes[e] = this.$measureCharWidth(e) / this.$characterSize.width), t }, this.destroy = function () { clearInterval(this.$pollSizeChangesTimer), this.$observer && this.$observer.disconnect(), this.el && this.el.parentNode && this.el.parentNode.removeChild(this.el) }, this.$getZoom = function e(t) { return t ? (window.getComputedStyle(t).zoom || 1) * e(t.parentElement) : 1 }, this.$initTransformMeasureNodes = function () { var e = function (e, t) { return ["div", { style: "position: absolute;top:" + e + "px;left:" + t + "px;" }] }; this.els = i.buildDom([e(0, 0), e(c, 0), e(0, c), e(c, c)], this.el) }, this.transformCoordinates = function (e, t) { function r(e, t, n) { var r = e[1] * t[0] - e[0] * t[1]; return [(-t[1] * n[0] + t[0] * n[1]) / r, (+e[1] * n[0] - e[0] * n[1]) / r] } function i(e, t) { return [e[0] - t[0], e[1] - t[1]] } function s(e, t) { return [e[0] + t[0], e[1] + t[1]] } function o(e, t) { return [e * t[0], e * t[1]] } function u(e) { var t = e.getBoundingClientRect(); return [t.left, t.top] } if (e) { var n = this.$getZoom(this.el); e = o(1 / n, e) } this.els || this.$initTransformMeasureNodes(); var a = u(this.els[0]), f = u(this.els[1]), l = u(this.els[2]), h = u(this.els[3]), p = r(i(h, f), i(h, l), i(s(f, l), s(h, a))), d = o(1 + p[0], i(f, a)), v = o(1 + p[1], i(l, a)); if (t) { var m = t, g = p[0] * m[0] / c + p[1] * m[1] / c + 1, y = s(o(m[0], d), o(m[1], v)); return s(o(1 / g / c, y), a) } var b = i(e, a), w = r(i(d, o(p[0], b)), i(v, o(p[1], b)), b); return o(c, w) } }).call(h.prototype) }), define("ace/virtual_renderer", ["require", "exports", "module", "ace/lib/oop", "ace/lib/dom", "ace/config", "ace/layer/gutter", "ace/layer/marker", "ace/layer/text", "ace/layer/cursor", "ace/scrollbar", "ace/scrollbar", "ace/renderloop", "ace/layer/font_metrics", "ace/lib/event_emitter", "ace/lib/useragent"], function (e, t, n) { "use strict"; var r = e("./lib/oop"), i = e("./lib/dom"), s = e("./config"), o = e("./layer/gutter").Gutter, u = e("./layer/marker").Marker, a = e("./layer/text").Text, f = e("./layer/cursor").Cursor, l = e("./scrollbar").HScrollBar, c = e("./scrollbar").VScrollBar, h = e("./renderloop").RenderLoop, p = e("./layer/font_metrics").FontMetrics, d = e("./lib/event_emitter").EventEmitter, v = '.ace_br1 {border-top-left-radius : 3px;}.ace_br2 {border-top-right-radius : 3px;}.ace_br3 {border-top-left-radius : 3px; border-top-right-radius: 3px;}.ace_br4 {border-bottom-right-radius: 3px;}.ace_br5 {border-top-left-radius : 3px; border-bottom-right-radius: 3px;}.ace_br6 {border-top-right-radius : 3px; border-bottom-right-radius: 3px;}.ace_br7 {border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px;}.ace_br8 {border-bottom-left-radius : 3px;}.ace_br9 {border-top-left-radius : 3px; border-bottom-left-radius: 3px;}.ace_br10{border-top-right-radius : 3px; border-bottom-left-radius: 3px;}.ace_br11{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br12{border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br13{border-top-left-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br14{border-top-right-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br15{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_editor {position: relative;overflow: hidden;padding: 0;font: 12px/normal \'Monaco\', \'Menlo\', \'Ubuntu Mono\', \'Consolas\', \'source-code-pro\', monospace;direction: ltr;text-align: left;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}.ace_scroller {position: absolute;overflow: hidden;top: 0;bottom: 0;background-color: inherit;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;cursor: text;}.ace_content {position: absolute;box-sizing: border-box;min-width: 100%;contain: style size layout;font-variant-ligatures: no-common-ligatures;}.ace_dragging .ace_scroller:before{position: absolute;top: 0;left: 0;right: 0;bottom: 0;content: \'\';background: rgba(250, 250, 250, 0.01);z-index: 1000;}.ace_dragging.ace_dark .ace_scroller:before{background: rgba(0, 0, 0, 0.01);}.ace_selecting, .ace_selecting * {cursor: text !important;}.ace_gutter {position: absolute;overflow : hidden;width: auto;top: 0;bottom: 0;left: 0;cursor: default;z-index: 4;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;contain: style size layout;}.ace_gutter-active-line {position: absolute;left: 0;right: 0;}.ace_scroller.ace_scroll-left {box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;}.ace_gutter-cell {position: absolute;top: 0;left: 0;right: 0;padding-left: 19px;padding-right: 6px;background-repeat: no-repeat;}.ace_gutter-cell.ace_error {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABOFBMVEX/////////QRswFAb/Ui4wFAYwFAYwFAaWGAfDRymzOSH/PxswFAb/SiUwFAYwFAbUPRvjQiDllog5HhHdRybsTi3/Tyv9Tir+Syj/UC3////XurebMBIwFAb/RSHbPx/gUzfdwL3kzMivKBAwFAbbvbnhPx66NhowFAYwFAaZJg8wFAaxKBDZurf/RB6mMxb/SCMwFAYwFAbxQB3+RB4wFAb/Qhy4Oh+4QifbNRcwFAYwFAYwFAb/QRzdNhgwFAYwFAbav7v/Uy7oaE68MBK5LxLewr/r2NXewLswFAaxJw4wFAbkPRy2PyYwFAaxKhLm1tMwFAazPiQwFAaUGAb/QBrfOx3bvrv/VC/maE4wFAbRPBq6MRO8Qynew8Dp2tjfwb0wFAbx6eju5+by6uns4uH9/f36+vr/GkHjAAAAYnRSTlMAGt+64rnWu/bo8eAA4InH3+DwoN7j4eLi4xP99Nfg4+b+/u9B/eDs1MD1mO7+4PHg2MXa347g7vDizMLN4eG+Pv7i5evs/v79yu7S3/DV7/498Yv24eH+4ufQ3Ozu/v7+y13sRqwAAADLSURBVHjaZc/XDsFgGIBhtDrshlitmk2IrbHFqL2pvXf/+78DPokj7+Fz9qpU/9UXJIlhmPaTaQ6QPaz0mm+5gwkgovcV6GZzd5JtCQwgsxoHOvJO15kleRLAnMgHFIESUEPmawB9ngmelTtipwwfASilxOLyiV5UVUyVAfbG0cCPHig+GBkzAENHS0AstVF6bacZIOzgLmxsHbt2OecNgJC83JERmePUYq8ARGkJx6XtFsdddBQgZE2nPR6CICZhawjA4Fb/chv+399kfR+MMMDGOQAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: 2px center;}.ace_gutter-cell.ace_warning {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAmVBMVEX///8AAAD///8AAAAAAABPSzb/5sAAAAB/blH/73z/ulkAAAAAAAD85pkAAAAAAAACAgP/vGz/rkDerGbGrV7/pkQICAf////e0IsAAAD/oED/qTvhrnUAAAD/yHD/njcAAADuv2r/nz//oTj/p064oGf/zHAAAAA9Nir/tFIAAAD/tlTiuWf/tkIAAACynXEAAAAAAAAtIRW7zBpBAAAAM3RSTlMAABR1m7RXO8Ln31Z36zT+neXe5OzooRDfn+TZ4p3h2hTf4t3k3ucyrN1K5+Xaks52Sfs9CXgrAAAAjklEQVR42o3PbQ+CIBQFYEwboPhSYgoYunIqqLn6/z8uYdH8Vmdnu9vz4WwXgN/xTPRD2+sgOcZjsge/whXZgUaYYvT8QnuJaUrjrHUQreGczuEafQCO/SJTufTbroWsPgsllVhq3wJEk2jUSzX3CUEDJC84707djRc5MTAQxoLgupWRwW6UB5fS++NV8AbOZgnsC7BpEAAAAABJRU5ErkJggg==");background-position: 2px center;}.ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAJ0Uk5TAAB2k804AAAAPklEQVQY02NgIB68QuO3tiLznjAwpKTgNyDbMegwisCHZUETUZV0ZqOquBpXj2rtnpSJT1AEnnRmL2OgGgAAIKkRQap2htgAAAAASUVORK5CYII=");background-position: 2px center;}.ace_dark .ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAChoaGAgIAqKiq+vr6tra1ZWVmUlJSbm5s8PDxubm56enrdgzg3AAAAAXRSTlMAQObYZgAAAClJREFUeNpjYMAPdsMYHegyJZFQBlsUlMFVCWUYKkAZMxZAGdxlDMQBAG+TBP4B6RyJAAAAAElFTkSuQmCC");}.ace_scrollbar {contain: strict;position: absolute;right: 0;bottom: 0;z-index: 6;}.ace_scrollbar-inner {position: absolute;cursor: text;left: 0;top: 0;}.ace_scrollbar-v{overflow-x: hidden;overflow-y: scroll;top: 0;}.ace_scrollbar-h {overflow-x: scroll;overflow-y: hidden;left: 0;}.ace_print-margin {position: absolute;height: 100%;}.ace_text-input {position: absolute;z-index: 0;width: 0.5em;height: 1em;opacity: 0;background: transparent;-moz-appearance: none;appearance: none;border: none;resize: none;outline: none;overflow: hidden;font: inherit;padding: 0 1px;margin: 0 -1px;contain: strict;-ms-user-select: text;-moz-user-select: text;-webkit-user-select: text;user-select: text;white-space: pre!important;}.ace_text-input.ace_composition {background: transparent;color: inherit;z-index: 1000;opacity: 1;}.ace_composition_placeholder { color: transparent }.ace_composition_marker { border-bottom: 1px solid;position: absolute;border-radius: 0;margin-top: 1px;}[ace_nocontext=true] {transform: none!important;filter: none!important;clip-path: none!important;mask : none!important;contain: none!important;perspective: none!important;mix-blend-mode: initial!important;z-index: auto;}.ace_layer {z-index: 1;position: absolute;overflow: hidden;word-wrap: normal;white-space: pre;height: 100%;width: 100%;box-sizing: border-box;pointer-events: none;}.ace_gutter-layer {position: relative;width: auto;text-align: right;pointer-events: auto;height: 1000000px;contain: style size layout;}.ace_text-layer {font: inherit !important;position: absolute;height: 1000000px;width: 1000000px;contain: style size layout;}.ace_text-layer > .ace_line, .ace_text-layer > .ace_line_group {contain: style size layout;position: absolute;top: 0;left: 0;right: 0;}.ace_hidpi .ace_text-layer,.ace_hidpi .ace_gutter-layer,.ace_hidpi .ace_content,.ace_hidpi .ace_gutter {contain: strict;will-change: transform;}.ace_hidpi .ace_text-layer > .ace_line, .ace_hidpi .ace_text-layer > .ace_line_group {contain: strict;}.ace_cjk {display: inline-block;text-align: center;}.ace_cursor-layer {z-index: 4;}.ace_cursor {z-index: 4;position: absolute;box-sizing: border-box;border-left: 2px solid;transform: translatez(0);}.ace_multiselect .ace_cursor {border-left-width: 1px;}.ace_slim-cursors .ace_cursor {border-left-width: 1px;}.ace_overwrite-cursors .ace_cursor {border-left-width: 0;border-bottom: 1px solid;}.ace_hidden-cursors .ace_cursor {opacity: 0.2;}.ace_hasPlaceholder .ace_hidden-cursors .ace_cursor {opacity: 0;}.ace_smooth-blinking .ace_cursor {transition: opacity 0.18s;}.ace_animate-blinking .ace_cursor {animation-duration: 1000ms;animation-timing-function: step-end;animation-name: blink-ace-animate;animation-iteration-count: infinite;}.ace_animate-blinking.ace_smooth-blinking .ace_cursor {animation-duration: 1000ms;animation-timing-function: ease-in-out;animation-name: blink-ace-animate-smooth;}@keyframes blink-ace-animate {from, to { opacity: 1; }60% { opacity: 0; }}@keyframes blink-ace-animate-smooth {from, to { opacity: 1; }45% { opacity: 1; }60% { opacity: 0; }85% { opacity: 0; }}.ace_marker-layer .ace_step, .ace_marker-layer .ace_stack {position: absolute;z-index: 3;}.ace_marker-layer .ace_selection {position: absolute;z-index: 5;}.ace_marker-layer .ace_bracket {position: absolute;z-index: 6;}.ace_marker-layer .ace_error_bracket {position: absolute;border-bottom: 1px solid #DE5555;border-radius: 0;}.ace_marker-layer .ace_active-line {position: absolute;z-index: 2;}.ace_marker-layer .ace_selected-word {position: absolute;z-index: 4;box-sizing: border-box;}.ace_line .ace_fold {box-sizing: border-box;display: inline-block;height: 11px;margin-top: -2px;vertical-align: middle;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII=");background-repeat: no-repeat, repeat-x;background-position: center center, top left;color: transparent;border: 1px solid black;border-radius: 2px;cursor: pointer;pointer-events: auto;}.ace_dark .ace_fold {}.ace_fold:hover{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC");}.ace_tooltip {background-color: #FFF;background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));border: 1px solid gray;border-radius: 1px;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);color: black;max-width: 100%;padding: 3px 4px;position: fixed;z-index: 999999;box-sizing: border-box;cursor: default;white-space: pre;word-wrap: break-word;line-height: normal;font-style: normal;font-weight: normal;letter-spacing: normal;pointer-events: none;}.ace_folding-enabled > .ace_gutter-cell {padding-right: 13px;}.ace_fold-widget {box-sizing: border-box;margin: 0 -12px 0 1px;display: none;width: 11px;vertical-align: top;background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: center;border-radius: 3px;border: 1px solid transparent;cursor: pointer;}.ace_folding-enabled .ace_fold-widget {display: inline-block; }.ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg==");}.ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==");}.ace_fold-widget:hover {border: 1px solid rgba(0, 0, 0, 0.3);background-color: rgba(255, 255, 255, 0.2);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);}.ace_fold-widget:active {border: 1px solid rgba(0, 0, 0, 0.4);background-color: rgba(0, 0, 0, 0.05);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);}.ace_dark .ace_fold-widget {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC");}.ace_dark .ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget:hover {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);background-color: rgba(255, 255, 255, 0.1);}.ace_dark .ace_fold-widget:active {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);}.ace_inline_button {border: 1px solid lightgray;display: inline-block;margin: -1px 8px;padding: 0 5px;pointer-events: auto;cursor: pointer;}.ace_inline_button:hover {border-color: gray;background: rgba(200,200,200,0.2);display: inline-block;pointer-events: auto;}.ace_fold-widget.ace_invalid {background-color: #FFB4B4;border-color: #DE5555;}.ace_fade-fold-widgets .ace_fold-widget {transition: opacity 0.4s ease 0.05s;opacity: 0;}.ace_fade-fold-widgets:hover .ace_fold-widget {transition: opacity 0.05s ease 0.05s;opacity:1;}.ace_underline {text-decoration: underline;}.ace_bold {font-weight: bold;}.ace_nobold .ace_bold {font-weight: normal;}.ace_italic {font-style: italic;}.ace_error-marker {background-color: rgba(255, 0, 0,0.2);position: absolute;z-index: 9;}.ace_highlight-marker {background-color: rgba(255, 255, 0,0.2);position: absolute;z-index: 8;}.ace_mobile-menu {position: absolute;line-height: 1.5;border-radius: 4px;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;background: white;box-shadow: 1px 3px 2px grey;border: 1px solid #dcdcdc;color: black;}.ace_dark > .ace_mobile-menu {background: #333;color: #ccc;box-shadow: 1px 3px 2px grey;border: 1px solid #444;}.ace_mobile-button {padding: 2px;cursor: pointer;overflow: hidden;}.ace_mobile-button:hover {background-color: #eee;opacity:1;}.ace_mobile-button:active {background-color: #ddd;}.ace_placeholder {font-family: arial;transform: scale(0.9);transform-origin: left;white-space: pre;opacity: 0.7;margin: 0 10px;}', m = e("./lib/useragent"), g = m.isIE; i.importCssString(v, "ace_editor.css"); var y = function (e, t) { var n = this; this.container = e || i.createElement("div"), i.addCssClass(this.container, "ace_editor"), i.HI_DPI && i.addCssClass(this.container, "ace_hidpi"), this.setTheme(t), this.$gutter = i.createElement("div"), this.$gutter.className = "ace_gutter", this.container.appendChild(this.$gutter), this.$gutter.setAttribute("aria-hidden", !0), this.scroller = i.createElement("div"), this.scroller.className = "ace_scroller", this.container.appendChild(this.scroller), this.content = i.createElement("div"), this.content.className = "ace_content", this.scroller.appendChild(this.content), this.$gutterLayer = new o(this.$gutter), this.$gutterLayer.on("changeGutterWidth", this.onGutterResize.bind(this)), this.$markerBack = new u(this.content); var r = this.$textLayer = new a(this.content); this.canvas = r.element, this.$markerFront = new u(this.content), this.$cursorLayer = new f(this.content), this.$horizScroll = !1, this.$vScroll = !1, this.scrollBar = this.scrollBarV = new c(this.container, this), this.scrollBarH = new l(this.container, this), this.scrollBarV.on("scroll", function (e) { n.$scrollAnimation || n.session.setScrollTop(e.data - n.scrollMargin.top) }), this.scrollBarH.on("scroll", function (e) { n.$scrollAnimation || n.session.setScrollLeft(e.data - n.scrollMargin.left) }), this.scrollTop = 0, this.scrollLeft = 0, this.cursorPos = { row: 0, column: 0 }, this.$fontMetrics = new p(this.container), this.$textLayer.$setFontMetrics(this.$fontMetrics), this.$textLayer.on("changeCharacterSize", function (e) { n.updateCharacterSize(), n.onResize(!0, n.gutterWidth, n.$size.width, n.$size.height), n._signal("changeCharacterSize", e) }), this.$size = { width: 0, height: 0, scrollerHeight: 0, scrollerWidth: 0, $dirty: !0 }, this.layerConfig = { width: 1, padding: 0, firstRow: 0, firstRowScreen: 0, lastRow: 0, lineHeight: 0, characterWidth: 0, minHeight: 1, maxHeight: 1, offset: 0, height: 1, gutterOffset: 1 }, this.scrollMargin = { left: 0, right: 0, top: 0, bottom: 0, v: 0, h: 0 }, this.margin = { left: 0, right: 0, top: 0, bottom: 0, v: 0, h: 0 }, this.$keepTextAreaAtCursor = !m.isIOS, this.$loop = new h(this.$renderChanges.bind(this), this.container.ownerDocument.defaultView), this.$loop.schedule(this.CHANGE_FULL), this.updateCharacterSize(), this.setPadding(4), s.resetOptions(this), s._signal("renderer", this) }; (function () { this.CHANGE_CURSOR = 1, this.CHANGE_MARKER = 2, this.CHANGE_GUTTER = 4, this.CHANGE_SCROLL = 8, this.CHANGE_LINES = 16, this.CHANGE_TEXT = 32, this.CHANGE_SIZE = 64, this.CHANGE_MARKER_BACK = 128, this.CHANGE_MARKER_FRONT = 256, this.CHANGE_FULL = 512, this.CHANGE_H_SCROLL = 1024, r.implement(this, d), this.updateCharacterSize = function () { this.$textLayer.allowBoldFonts != this.$allowBoldFonts && (this.$allowBoldFonts = this.$textLayer.allowBoldFonts, this.setStyle("ace_nobold", !this.$allowBoldFonts)), this.layerConfig.characterWidth = this.characterWidth = this.$textLayer.getCharacterWidth(), this.layerConfig.lineHeight = this.lineHeight = this.$textLayer.getLineHeight(), this.$updatePrintMargin(), i.setStyle(this.scroller.style, "line-height", this.lineHeight + "px") }, this.setSession = function (e) { this.session && this.session.doc.off("changeNewLineMode", this.onChangeNewLineMode), this.session = e, e && this.scrollMargin.top && e.getScrollTop() <= 0 && e.setScrollTop(-this.scrollMargin.top), this.$cursorLayer.setSession(e), this.$markerBack.setSession(e), this.$markerFront.setSession(e), this.$gutterLayer.setSession(e), this.$textLayer.setSession(e); if (!e) return; this.$loop.schedule(this.CHANGE_FULL), this.session.$setFontMetrics(this.$fontMetrics), this.scrollBarH.scrollLeft = this.scrollBarV.scrollTop = null, this.onChangeNewLineMode = this.onChangeNewLineMode.bind(this), this.onChangeNewLineMode(), this.session.doc.on("changeNewLineMode", this.onChangeNewLineMode) }, this.updateLines = function (e, t, n) { t === undefined && (t = Infinity), this.$changedLines ? (this.$changedLines.firstRow > e && (this.$changedLines.firstRow = e), this.$changedLines.lastRow < t && (this.$changedLines.lastRow = t)) : this.$changedLines = { firstRow: e, lastRow: t }; if (this.$changedLines.lastRow < this.layerConfig.firstRow) { if (!n) return; this.$changedLines.lastRow = this.layerConfig.lastRow } if (this.$changedLines.firstRow > this.layerConfig.lastRow) return; this.$loop.schedule(this.CHANGE_LINES) }, this.onChangeNewLineMode = function () { this.$loop.schedule(this.CHANGE_TEXT), this.$textLayer.$updateEolChar(), this.session.$bidiHandler.setEolChar(this.$textLayer.EOL_CHAR) }, this.onChangeTabSize = function () { this.$loop.schedule(this.CHANGE_TEXT | this.CHANGE_MARKER), this.$textLayer.onChangeTabSize() }, this.updateText = function () { this.$loop.schedule(this.CHANGE_TEXT) }, this.updateFull = function (e) { e ? this.$renderChanges(this.CHANGE_FULL, !0) : this.$loop.schedule(this.CHANGE_FULL) }, this.updateFontSize = function () { this.$textLayer.checkForSizeChanges() }, this.$changes = 0, this.$updateSizeAsync = function () { this.$loop.pending ? this.$size.$dirty = !0 : this.onResize() }, this.onResize = function (e, t, n, r) { if (this.resizing > 2) return; this.resizing > 0 ? this.resizing++ : this.resizing = e ? 1 : 0; var i = this.container; r || (r = i.clientHeight || i.scrollHeight), n || (n = i.clientWidth || i.scrollWidth); var s = this.$updateCachedSize(e, t, n, r); if (!this.$size.scrollerHeight || !n && !r) return this.resizing = 0; e && (this.$gutterLayer.$padding = null), e ? this.$renderChanges(s | this.$changes, !0) : this.$loop.schedule(s | this.$changes), this.resizing && (this.resizing = 0), this.scrollBarV.scrollLeft = this.scrollBarV.scrollTop = null }, this.$updateCachedSize = function (e, t, n, r) { r -= this.$extraHeight || 0; var s = 0, o = this.$size, u = { width: o.width, height: o.height, scrollerHeight: o.scrollerHeight, scrollerWidth: o.scrollerWidth }; r && (e || o.height != r) && (o.height = r, s |= this.CHANGE_SIZE, o.scrollerHeight = o.height, this.$horizScroll && (o.scrollerHeight -= this.scrollBarH.getHeight()), this.scrollBarV.element.style.bottom = this.scrollBarH.getHeight() + "px", s |= this.CHANGE_SCROLL); if (n && (e || o.width != n)) { s |= this.CHANGE_SIZE, o.width = n, t == null && (t = this.$showGutter ? this.$gutter.offsetWidth : 0), this.gutterWidth = t, i.setStyle(this.scrollBarH.element.style, "left", t + "px"), i.setStyle(this.scroller.style, "left", t + this.margin.left + "px"), o.scrollerWidth = Math.max(0, n - t - this.scrollBarV.getWidth() - this.margin.h), i.setStyle(this.$gutter.style, "left", this.margin.left + "px"); var a = this.scrollBarV.getWidth() + "px"; i.setStyle(this.scrollBarH.element.style, "right", a), i.setStyle(this.scroller.style, "right", a), i.setStyle(this.scroller.style, "bottom", this.scrollBarH.getHeight()); if (this.session && this.session.getUseWrapMode() && this.adjustWrapLimit() || e) s |= this.CHANGE_FULL } return o.$dirty = !n || !r, s && this._signal("resize", u), s }, this.onGutterResize = function (e) { var t = this.$showGutter ? e : 0; t != this.gutterWidth && (this.$changes |= this.$updateCachedSize(!0, t, this.$size.width, this.$size.height)), this.session.getUseWrapMode() && this.adjustWrapLimit() ? this.$loop.schedule(this.CHANGE_FULL) : this.$size.$dirty ? this.$loop.schedule(this.CHANGE_FULL) : this.$computeLayerConfig() }, this.adjustWrapLimit = function () { var e = this.$size.scrollerWidth - this.$padding * 2, t = Math.floor(e / this.characterWidth); return this.session.adjustWrapLimit(t, this.$showPrintMargin && this.$printMarginColumn) }, this.setAnimatedScroll = function (e) { this.setOption("animatedScroll", e) }, this.getAnimatedScroll = function () { return this.$animatedScroll }, this.setShowInvisibles = function (e) { this.setOption("showInvisibles", e), this.session.$bidiHandler.setShowInvisibles(e) }, this.getShowInvisibles = function () { return this.getOption("showInvisibles") }, this.getDisplayIndentGuides = function () { return this.getOption("displayIndentGuides") }, this.setDisplayIndentGuides = function (e) { this.setOption("displayIndentGuides", e) }, this.setShowPrintMargin = function (e) { this.setOption("showPrintMargin", e) }, this.getShowPrintMargin = function () { return this.getOption("showPrintMargin") }, this.setPrintMarginColumn = function (e) { this.setOption("printMarginColumn", e) }, this.getPrintMarginColumn = function () { return this.getOption("printMarginColumn") }, this.getShowGutter = function () { return this.getOption("showGutter") }, this.setShowGutter = function (e) { return this.setOption("showGutter", e) }, this.getFadeFoldWidgets = function () { return this.getOption("fadeFoldWidgets") }, this.setFadeFoldWidgets = function (e) { this.setOption("fadeFoldWidgets", e) }, this.setHighlightGutterLine = function (e) { this.setOption("highlightGutterLine", e) }, this.getHighlightGutterLine = function () { return this.getOption("highlightGutterLine") }, this.$updatePrintMargin = function () { if (!this.$showPrintMargin && !this.$printMarginEl) return; if (!this.$printMarginEl) { var e = i.createElement("div"); e.className = "ace_layer ace_print-margin-layer", this.$printMarginEl = i.createElement("div"), this.$printMarginEl.className = "ace_print-margin", e.appendChild(this.$printMarginEl), this.content.insertBefore(e, this.content.firstChild) } var t = this.$printMarginEl.style; t.left = Math.round(this.characterWidth * this.$printMarginColumn + this.$padding) + "px", t.visibility = this.$showPrintMargin ? "visible" : "hidden", this.session && this.session.$wrap == -1 && this.adjustWrapLimit() }, this.getContainerElement = function () { return this.container }, this.getMouseEventTarget = function () { return this.scroller }, this.getTextAreaContainer = function () { return this.container }, this.$moveTextAreaToCursor = function () { if (this.$isMousePressed) return; var e = this.textarea.style, t = this.$composition; if (!this.$keepTextAreaAtCursor && !t) { i.translate(this.textarea, -100, 0); return } var n = this.$cursorLayer.$pixelPos; if (!n) return; t && t.markerRange && (n = this.$cursorLayer.getPixelPosition(t.markerRange.start, !0)); var r = this.layerConfig, s = n.top, o = n.left; s -= r.offset; var u = t && t.useTextareaForIME ? this.lineHeight : g ? 0 : 1; if (s < 0 || s > r.height - u) { i.translate(this.textarea, 0, 0); return } var a = 1, f = this.$size.height - u; if (!t) s += this.lineHeight; else if (t.useTextareaForIME) { var l = this.textarea.value; a = this.characterWidth * this.session.$getStringScreenWidth(l)[0] } else s += this.lineHeight + 2; o -= this.scrollLeft, o > this.$size.scrollerWidth - a && (o = this.$size.scrollerWidth - a), o += this.gutterWidth + this.margin.left, i.setStyle(e, "height", u + "px"), i.setStyle(e, "width", a + "px"), i.translate(this.textarea, Math.min(o, this.$size.scrollerWidth - a), Math.min(s, f)) }, this.getFirstVisibleRow = function () { return this.layerConfig.firstRow }, this.getFirstFullyVisibleRow = function () { return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1) }, this.getLastFullyVisibleRow = function () { var e = this.layerConfig, t = e.lastRow, n = this.session.documentToScreenRow(t, 0) * e.lineHeight; return n - this.session.getScrollTop() > e.height - e.lineHeight ? t - 1 : t }, this.getLastVisibleRow = function () { return this.layerConfig.lastRow }, this.$padding = null, this.setPadding = function (e) { this.$padding = e, this.$textLayer.setPadding(e), this.$cursorLayer.setPadding(e), this.$markerFront.setPadding(e), this.$markerBack.setPadding(e), this.$loop.schedule(this.CHANGE_FULL), this.$updatePrintMargin() }, this.setScrollMargin = function (e, t, n, r) { var i = this.scrollMargin; i.top = e | 0, i.bottom = t | 0, i.right = r | 0, i.left = n | 0, i.v = i.top + i.bottom, i.h = i.left + i.right, i.top && this.scrollTop <= 0 && this.session && this.session.setScrollTop(-i.top), this.updateFull() }, this.setMargin = function (e, t, n, r) { var i = this.margin; i.top = e | 0, i.bottom = t | 0, i.right = r | 0, i.left = n | 0, i.v = i.top + i.bottom, i.h = i.left + i.right, this.$updateCachedSize(!0, this.gutterWidth, this.$size.width, this.$size.height), this.updateFull() }, this.getHScrollBarAlwaysVisible = function () { return this.$hScrollBarAlwaysVisible }, this.setHScrollBarAlwaysVisible = function (e) { this.setOption("hScrollBarAlwaysVisible", e) }, this.getVScrollBarAlwaysVisible = function () { return this.$vScrollBarAlwaysVisible }, this.setVScrollBarAlwaysVisible = function (e) { this.setOption("vScrollBarAlwaysVisible", e) }, this.$updateScrollBarV = function () { var e = this.layerConfig.maxHeight, t = this.$size.scrollerHeight; !this.$maxLines && this.$scrollPastEnd && (e -= (t - this.lineHeight) * this.$scrollPastEnd, this.scrollTop > e - t && (e = this.scrollTop + t, this.scrollBarV.scrollTop = null)), this.scrollBarV.setScrollHeight(e + this.scrollMargin.v), this.scrollBarV.setScrollTop(this.scrollTop + this.scrollMargin.top) }, this.$updateScrollBarH = function () { this.scrollBarH.setScrollWidth(this.layerConfig.width + 2 * this.$padding + this.scrollMargin.h), this.scrollBarH.setScrollLeft(this.scrollLeft + this.scrollMargin.left) }, this.$frozen = !1, this.freeze = function () { this.$frozen = !0 }, this.unfreeze = function () { this.$frozen = !1 }, this.$renderChanges = function (e, t) { this.$changes && (e |= this.$changes, this.$changes = 0); if (!this.session || !this.container.offsetWidth || this.$frozen || !e && !t) { this.$changes |= e; return } if (this.$size.$dirty) return this.$changes |= e, this.onResize(!0); this.lineHeight || this.$textLayer.checkForSizeChanges(), this._signal("beforeRender", e), this.session && this.session.$bidiHandler && this.session.$bidiHandler.updateCharacterWidths(this.$fontMetrics); var n = this.layerConfig; if (e & this.CHANGE_FULL || e & this.CHANGE_SIZE || e & this.CHANGE_TEXT || e & this.CHANGE_LINES || e & this.CHANGE_SCROLL || e & this.CHANGE_H_SCROLL) { e |= this.$computeLayerConfig() | this.$loop.clear(); if (n.firstRow != this.layerConfig.firstRow && n.firstRowScreen == this.layerConfig.firstRowScreen) { var r = this.scrollTop + (n.firstRow - this.layerConfig.firstRow) * this.lineHeight; r > 0 && (this.scrollTop = r, e |= this.CHANGE_SCROLL, e |= this.$computeLayerConfig() | this.$loop.clear()) } n = this.layerConfig, this.$updateScrollBarV(), e & this.CHANGE_H_SCROLL && this.$updateScrollBarH(), i.translate(this.content, -this.scrollLeft, -n.offset); var s = n.width + 2 * this.$padding + "px", o = n.minHeight + "px"; i.setStyle(this.content.style, "width", s), i.setStyle(this.content.style, "height", o) } e & this.CHANGE_H_SCROLL && (i.translate(this.content, -this.scrollLeft, -n.offset), this.scroller.className = this.scrollLeft <= 0 ? "ace_scroller" : "ace_scroller ace_scroll-left"); if (e & this.CHANGE_FULL) { this.$changedLines = null, this.$textLayer.update(n), this.$showGutter && this.$gutterLayer.update(n), this.$markerBack.update(n), this.$markerFront.update(n), this.$cursorLayer.update(n), this.$moveTextAreaToCursor(), this._signal("afterRender", e); return } if (e & this.CHANGE_SCROLL) { this.$changedLines = null, e & this.CHANGE_TEXT || e & this.CHANGE_LINES ? this.$textLayer.update(n) : this.$textLayer.scrollLines(n), this.$showGutter && (e & this.CHANGE_GUTTER || e & this.CHANGE_LINES ? this.$gutterLayer.update(n) : this.$gutterLayer.scrollLines(n)), this.$markerBack.update(n), this.$markerFront.update(n), this.$cursorLayer.update(n), this.$moveTextAreaToCursor(), this._signal("afterRender", e); return } e & this.CHANGE_TEXT ? (this.$changedLines = null, this.$textLayer.update(n), this.$showGutter && this.$gutterLayer.update(n)) : e & this.CHANGE_LINES ? (this.$updateLines() || e & this.CHANGE_GUTTER && this.$showGutter) && this.$gutterLayer.update(n) : e & this.CHANGE_TEXT || e & this.CHANGE_GUTTER ? this.$showGutter && this.$gutterLayer.update(n) : e & this.CHANGE_CURSOR && this.$highlightGutterLine && this.$gutterLayer.updateLineHighlight(n), e & this.CHANGE_CURSOR && (this.$cursorLayer.update(n), this.$moveTextAreaToCursor()), e & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT) && this.$markerFront.update(n), e & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK) && this.$markerBack.update(n), this._signal("afterRender", e) }, this.$autosize = function () { var e = this.session.getScreenLength() * this.lineHeight, t = this.$maxLines * this.lineHeight, n = Math.min(t, Math.max((this.$minLines || 1) * this.lineHeight, e)) + this.scrollMargin.v + (this.$extraHeight || 0); this.$horizScroll && (n += this.scrollBarH.getHeight()), this.$maxPixelHeight && n > this.$maxPixelHeight && (n = this.$maxPixelHeight); var r = n <= 2 * this.lineHeight, i = !r && e > t; if (n != this.desiredHeight || this.$size.height != this.desiredHeight || i != this.$vScroll) { i != this.$vScroll && (this.$vScroll = i, this.scrollBarV.setVisible(i)); var s = this.container.clientWidth; this.container.style.height = n + "px", this.$updateCachedSize(!0, this.$gutterWidth, s, n), this.desiredHeight = n, this._signal("autosize") } }, this.$computeLayerConfig = function () { var e = this.session, t = this.$size, n = t.height <= 2 * this.lineHeight, r = this.session.getScreenLength(), i = r * this.lineHeight, s = this.$getLongestLine(), o = !n && (this.$hScrollBarAlwaysVisible || t.scrollerWidth - s - 2 * this.$padding < 0), u = this.$horizScroll !== o; u && (this.$horizScroll = o, this.scrollBarH.setVisible(o)); var a = this.$vScroll; this.$maxLines && this.lineHeight > 1 && this.$autosize(); var f = t.scrollerHeight + this.lineHeight, l = !this.$maxLines && this.$scrollPastEnd ? (t.scrollerHeight - this.lineHeight) * this.$scrollPastEnd : 0; i += l; var c = this.scrollMargin; this.session.setScrollTop(Math.max(-c.top, Math.min(this.scrollTop, i - t.scrollerHeight + c.bottom))), this.session.setScrollLeft(Math.max(-c.left, Math.min(this.scrollLeft, s + 2 * this.$padding - t.scrollerWidth + c.right))); var h = !n && (this.$vScrollBarAlwaysVisible || t.scrollerHeight - i + l < 0 || this.scrollTop > c.top), p = a !== h; p && (this.$vScroll = h, this.scrollBarV.setVisible(h)); var d = this.scrollTop % this.lineHeight, v = Math.ceil(f / this.lineHeight) - 1, m = Math.max(0, Math.round((this.scrollTop - d) / this.lineHeight)), g = m + v, y, b, w = this.lineHeight; m = e.screenToDocumentRow(m, 0); var E = e.getFoldLine(m); E && (m = E.start.row), y = e.documentToScreenRow(m, 0), b = e.getRowLength(m) * w, g = Math.min(e.screenToDocumentRow(g, 0), e.getLength() - 1), f = t.scrollerHeight + e.getRowLength(g) * w + b, d = this.scrollTop - y * w; var S = 0; if (this.layerConfig.width != s || u) S = this.CHANGE_H_SCROLL; if (u || p) S |= this.$updateCachedSize(!0, this.gutterWidth, t.width, t.height), this._signal("scrollbarVisibilityChanged"), p && (s = this.$getLongestLine()); return this.layerConfig = { width: s, padding: this.$padding, firstRow: m, firstRowScreen: y, lastRow: g, lineHeight: w, characterWidth: this.characterWidth, minHeight: f, maxHeight: i, offset: d, gutterOffset: w ? Math.max(0, Math.ceil((d + t.height - t.scrollerHeight) / w)) : 0, height: this.$size.scrollerHeight }, this.session.$bidiHandler && this.session.$bidiHandler.setContentWidth(s - this.$padding), S }, this.$updateLines = function () { if (!this.$changedLines) return; var e = this.$changedLines.firstRow, t = this.$changedLines.lastRow; this.$changedLines = null; var n = this.layerConfig; if (e > n.lastRow + 1) return; if (t < n.firstRow) return; if (t === Infinity) { this.$showGutter && this.$gutterLayer.update(n), this.$textLayer.update(n); return } return this.$textLayer.updateLines(n, e, t), !0 }, this.$getLongestLine = function () { var e = this.session.getScreenWidth(); return this.showInvisibles && !this.session.$useWrapMode && (e += 1), this.$textLayer && e > this.$textLayer.MAX_LINE_LENGTH && (e = this.$textLayer.MAX_LINE_LENGTH + 30), Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(e * this.characterWidth)) }, this.updateFrontMarkers = function () { this.$markerFront.setMarkers(this.session.getMarkers(!0)), this.$loop.schedule(this.CHANGE_MARKER_FRONT) }, this.updateBackMarkers = function () { this.$markerBack.setMarkers(this.session.getMarkers()), this.$loop.schedule(this.CHANGE_MARKER_BACK) }, this.addGutterDecoration = function (e, t) { this.$gutterLayer.addGutterDecoration(e, t) }, this.removeGutterDecoration = function (e, t) { this.$gutterLayer.removeGutterDecoration(e, t) }, this.updateBreakpoints = function (e) { this.$loop.schedule(this.CHANGE_GUTTER) }, this.setAnnotations = function (e) { this.$gutterLayer.setAnnotations(e), this.$loop.schedule(this.CHANGE_GUTTER) }, this.updateCursor = function () { this.$loop.schedule(this.CHANGE_CURSOR) }, this.hideCursor = function () { this.$cursorLayer.hideCursor() }, this.showCursor = function () { this.$cursorLayer.showCursor() }, this.scrollSelectionIntoView = function (e, t, n) { this.scrollCursorIntoView(e, n), this.scrollCursorIntoView(t, n) }, this.scrollCursorIntoView = function (e, t, n) { if (this.$size.scrollerHeight === 0) return; var r = this.$cursorLayer.getPixelPosition(e), i = r.left, s = r.top, o = n && n.top || 0, u = n && n.bottom || 0, a = this.$scrollAnimation ? this.session.getScrollTop() : this.scrollTop; a + o > s ? (t && a + o > s + this.lineHeight && (s -= t * this.$size.scrollerHeight), s === 0 && (s = -this.scrollMargin.top), this.session.setScrollTop(s)) : a + this.$size.scrollerHeight - u < s + this.lineHeight && (t && a + this.$size.scrollerHeight - u < s - this.lineHeight && (s += t * this.$size.scrollerHeight), this.session.setScrollTop(s + this.lineHeight + u - this.$size.scrollerHeight)); var f = this.scrollLeft; f > i ? (i < this.$padding + 2 * this.layerConfig.characterWidth && (i = -this.scrollMargin.left), this.session.setScrollLeft(i)) : f + this.$size.scrollerWidth < i + this.characterWidth ? this.session.setScrollLeft(Math.round(i + this.characterWidth - this.$size.scrollerWidth)) : f <= this.$padding && i - f < this.characterWidth && this.session.setScrollLeft(0) }, this.getScrollTop = function () { return this.session.getScrollTop() }, this.getScrollLeft = function () { return this.session.getScrollLeft() }, this.getScrollTopRow = function () { return this.scrollTop / this.lineHeight }, this.getScrollBottomRow = function () { return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1) }, this.scrollToRow = function (e) { this.session.setScrollTop(e * this.lineHeight) }, this.alignCursor = function (e, t) { typeof e == "number" && (e = { row: e, column: 0 }); var n = this.$cursorLayer.getPixelPosition(e), r = this.$size.scrollerHeight - this.lineHeight, i = n.top - r * (t || 0); return this.session.setScrollTop(i), i }, this.STEPS = 8, this.$calcSteps = function (e, t) { var n = 0, r = this.STEPS, i = [], s = function (e, t, n) { return n * (Math.pow(e - 1, 3) + 1) + t }; for (n = 0; n < r; ++n)i.push(s(n / this.STEPS, e, t - e)); return i }, this.scrollToLine = function (e, t, n, r) { var i = this.$cursorLayer.getPixelPosition({ row: e, column: 0 }), s = i.top; t && (s -= this.$size.scrollerHeight / 2); var o = this.scrollTop; this.session.setScrollTop(s), n !== !1 && this.animateScrolling(o, r) }, this.animateScrolling = function (e, t) { var n = this.scrollTop; if (!this.$animatedScroll) return; var r = this; if (e == n) return; if (this.$scrollAnimation) { var i = this.$scrollAnimation.steps; if (i.length) { e = i[0]; if (e == n) return } } var s = r.$calcSteps(e, n); this.$scrollAnimation = { from: e, to: n, steps: s }, clearInterval(this.$timer), r.session.setScrollTop(s.shift()), r.session.$scrollTop = n, this.$timer = setInterval(function () { if (!r.session) return clearInterval(r.$timer); s.length ? (r.session.setScrollTop(s.shift()), r.session.$scrollTop = n) : n != null ? (r.session.$scrollTop = -1, r.session.setScrollTop(n), n = null) : (r.$timer = clearInterval(r.$timer), r.$scrollAnimation = null, t && t()) }, 10) }, this.scrollToY = function (e) { this.scrollTop !== e && (this.$loop.schedule(this.CHANGE_SCROLL), this.scrollTop = e) }, this.scrollToX = function (e) { this.scrollLeft !== e && (this.scrollLeft = e), this.$loop.schedule(this.CHANGE_H_SCROLL) }, this.scrollTo = function (e, t) { this.session.setScrollTop(t), this.session.setScrollLeft(t) }, this.scrollBy = function (e, t) { t && this.session.setScrollTop(this.session.getScrollTop() + t), e && this.session.setScrollLeft(this.session.getScrollLeft() + e) }, this.isScrollableBy = function (e, t) { if (t < 0 && this.session.getScrollTop() >= 1 - this.scrollMargin.top) return !0; if (t > 0 && this.session.getScrollTop() + this.$size.scrollerHeight - this.layerConfig.maxHeight < -1 + this.scrollMargin.bottom) return !0; if (e < 0 && this.session.getScrollLeft() >= 1 - this.scrollMargin.left) return !0; if (e > 0 && this.session.getScrollLeft() + this.$size.scrollerWidth - this.layerConfig.width < -1 + this.scrollMargin.right) return !0 }, this.pixelToScreenCoordinates = function (e, t) { var n; if (this.$hasCssTransforms) { n = { top: 0, left: 0 }; var r = this.$fontMetrics.transformCoordinates([e, t]); e = r[1] - this.gutterWidth - this.margin.left, t = r[0] } else n = this.scroller.getBoundingClientRect(); var i = e + this.scrollLeft - n.left - this.$padding, s = i / this.characterWidth, o = Math.floor((t + this.scrollTop - n.top) / this.lineHeight), u = this.$blockCursor ? Math.floor(s) : Math.round(s); return { row: o, column: u, side: s - u > 0 ? 1 : -1, offsetX: i } }, this.screenToTextCoordinates = function (e, t) { var n; if (this.$hasCssTransforms) { n = { top: 0, left: 0 }; var r = this.$fontMetrics.transformCoordinates([e, t]); e = r[1] - this.gutterWidth - this.margin.left, t = r[0] } else n = this.scroller.getBoundingClientRect(); var i = e + this.scrollLeft - n.left - this.$padding, s = i / this.characterWidth, o = this.$blockCursor ? Math.floor(s) : Math.round(s), u = Math.floor((t + this.scrollTop - n.top) / this.lineHeight); return this.session.screenToDocumentPosition(u, Math.max(o, 0), i) }, this.textToScreenCoordinates = function (e, t) { var n = this.scroller.getBoundingClientRect(), r = this.session.documentToScreenPosition(e, t), i = this.$padding + (this.session.$bidiHandler.isBidiRow(r.row, e) ? this.session.$bidiHandler.getPosLeft(r.column) : Math.round(r.column * this.characterWidth)), s = r.row * this.lineHeight; return { pageX: n.left + i - this.scrollLeft, pageY: n.top + s - this.scrollTop } }, this.visualizeFocus = function () { i.addCssClass(this.container, "ace_focus") }, this.visualizeBlur = function () { i.removeCssClass(this.container, "ace_focus") }, this.showComposition = function (e) { this.$composition = e, e.cssText || (e.cssText = this.textarea.style.cssText), e.useTextareaForIME == undefined && (e.useTextareaForIME = this.$useTextareaForIME), this.$useTextareaForIME ? (i.addCssClass(this.textarea, "ace_composition"), this.textarea.style.cssText = "", this.$moveTextAreaToCursor(), this.$cursorLayer.element.style.display = "none") : e.markerId = this.session.addMarker(e.markerRange, "ace_composition_marker", "text") }, this.setCompositionText = function (e) { var t = this.session.selection.cursor; this.addToken(e, "composition_placeholder", t.row, t.column), this.$moveTextAreaToCursor() }, this.hideComposition = function () { if (!this.$composition) return; this.$composition.markerId && this.session.removeMarker(this.$composition.markerId), i.removeCssClass(this.textarea, "ace_composition"), this.textarea.style.cssText = this.$composition.cssText; var e = this.session.selection.cursor; this.removeExtraToken(e.row, e.column), this.$composition = null, this.$cursorLayer.element.style.display = "" }, this.addToken = function (e, t, n, r) { var i = this.session; i.bgTokenizer.lines[n] = null; var s = { type: t, value: e }, o = i.getTokens(n); if (r == null) o.push(s); else { var u = 0; for (var a = 0; a < o.length; a++) { var f = o[a]; u += f.value.length; if (r <= u) { var l = f.value.length - (u - r), c = f.value.slice(0, l), h = f.value.slice(l); o.splice(a, 1, { type: f.type, value: c }, s, { type: f.type, value: h }); break } } } this.updateLines(n, n) }, this.removeExtraToken = function (e, t) { this.updateLines(e, e) }, this.setTheme = function (e, t) { function o(r) { if (n.$themeId != e) return t && t(); if (!r || !r.cssClass) throw new Error("couldn't load module " + e + " or it didn't call define"); r.$id && (n.$themeId = r.$id), i.importCssString(r.cssText, r.cssClass, n.container), n.theme && i.removeCssClass(n.container, n.theme.cssClass); var s = "padding" in r ? r.padding : "padding" in (n.theme || {}) ? 4 : n.$padding; n.$padding && s != n.$padding && n.setPadding(s), n.$theme = r.cssClass, n.theme = r, i.addCssClass(n.container, r.cssClass), i.setCssClass(n.container, "ace_dark", r.isDark), n.$size && (n.$size.width = 0, n.$updateSizeAsync()), n._dispatchEvent("themeLoaded", { theme: r }), t && t() } var n = this; this.$themeId = e, n._dispatchEvent("themeChange", { theme: e }); if (!e || typeof e == "string") { var r = e || this.$options.theme.initialValue; s.loadModule(["theme", r], o) } else o(e) }, this.getTheme = function () { return this.$themeId }, this.setStyle = function (e, t) { i.setCssClass(this.container, e, t !== !1) }, this.unsetStyle = function (e) { i.removeCssClass(this.container, e) }, this.setCursorStyle = function (e) { i.setStyle(this.scroller.style, "cursor", e) }, this.setMouseCursor = function (e) { i.setStyle(this.scroller.style, "cursor", e) }, this.attachToShadowRoot = function () { i.importCssString(v, "ace_editor.css", this.container) }, this.destroy = function () { this.freeze(), this.$fontMetrics.destroy(), this.$cursorLayer.destroy(), this.removeAllListeners(), this.container.textContent = "" } }).call(y.prototype), s.defineOptions(y.prototype, "renderer", { animatedScroll: { initialValue: !1 }, showInvisibles: { set: function (e) { this.$textLayer.setShowInvisibles(e) && this.$loop.schedule(this.CHANGE_TEXT) }, initialValue: !1 }, showPrintMargin: { set: function () { this.$updatePrintMargin() }, initialValue: !0 }, printMarginColumn: { set: function () { this.$updatePrintMargin() }, initialValue: 80 }, printMargin: { set: function (e) { typeof e == "number" && (this.$printMarginColumn = e), this.$showPrintMargin = !!e, this.$updatePrintMargin() }, get: function () { return this.$showPrintMargin && this.$printMarginColumn } }, showGutter: { set: function (e) { this.$gutter.style.display = e ? "block" : "none", this.$loop.schedule(this.CHANGE_FULL), this.onGutterResize() }, initialValue: !0 }, fadeFoldWidgets: { set: function (e) { i.setCssClass(this.$gutter, "ace_fade-fold-widgets", e) }, initialValue: !1 }, showFoldWidgets: { set: function (e) { this.$gutterLayer.setShowFoldWidgets(e), this.$loop.schedule(this.CHANGE_GUTTER) }, initialValue: !0 }, displayIndentGuides: { set: function (e) { this.$textLayer.setDisplayIndentGuides(e) && this.$loop.schedule(this.CHANGE_TEXT) }, initialValue: !0 }, highlightGutterLine: { set: function (e) { this.$gutterLayer.setHighlightGutterLine(e), this.$loop.schedule(this.CHANGE_GUTTER) }, initialValue: !0 }, hScrollBarAlwaysVisible: { set: function (e) { (!this.$hScrollBarAlwaysVisible || !this.$horizScroll) && this.$loop.schedule(this.CHANGE_SCROLL) }, initialValue: !1 }, vScrollBarAlwaysVisible: { set: function (e) { (!this.$vScrollBarAlwaysVisible || !this.$vScroll) && this.$loop.schedule(this.CHANGE_SCROLL) }, initialValue: !1 }, fontSize: { set: function (e) { typeof e == "number" && (e += "px"), this.container.style.fontSize = e, this.updateFontSize() }, initialValue: 12 }, fontFamily: { set: function (e) { this.container.style.fontFamily = e, this.updateFontSize() } }, maxLines: { set: function (e) { this.updateFull() } }, minLines: { set: function (e) { this.$minLines < 562949953421311 || (this.$minLines = 0), this.updateFull() } }, maxPixelHeight: { set: function (e) { this.updateFull() }, initialValue: 0 }, scrollPastEnd: { set: function (e) { e = +e || 0; if (this.$scrollPastEnd == e) return; this.$scrollPastEnd = e, this.$loop.schedule(this.CHANGE_SCROLL) }, initialValue: 0, handlesSet: !0 }, fixedWidthGutter: { set: function (e) { this.$gutterLayer.$fixedWidth = !!e, this.$loop.schedule(this.CHANGE_GUTTER) } }, theme: { set: function (e) { this.setTheme(e) }, get: function () { return this.$themeId || this.theme }, initialValue: "./theme/textmate", handlesSet: !0 }, hasCssTransforms: {}, useTextareaForIME: { initialValue: !m.isMobile && !m.isIE } }), t.VirtualRenderer = y }), define("ace/worker/worker_client", ["require", "exports", "module", "ace/lib/oop", "ace/lib/net", "ace/lib/event_emitter", "ace/config"], function (e, t, n) { "use strict"; function u(e) { var t = "importScripts('" + i.qualifyURL(e) + "');"; try { return new Blob([t], { type: "application/javascript" }) } catch (n) { var r = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder, s = new r; return s.append(t), s.getBlob("application/javascript") } } function a(e) { if (typeof Worker == "undefined") return { postMessage: function () { }, terminate: function () { } }; if (o.get("loadWorkerFromBlob")) { var t = u(e), n = window.URL || window.webkitURL, r = n.createObjectURL(t); return new Worker(r) } return new Worker(e) } var r = e("../lib/oop"), i = e("../lib/net"), s = e("../lib/event_emitter").EventEmitter, o = e("../config"), f = function (e) { e.postMessage || (e = this.$createWorkerFromOldConfig.apply(this, arguments)), this.$worker = e, this.$sendDeltaQueue = this.$sendDeltaQueue.bind(this), this.changeListener = this.changeListener.bind(this), this.onMessage = this.onMessage.bind(this), this.callbackId = 1, this.callbacks = {}, this.$worker.onmessage = this.onMessage }; (function () { r.implement(this, s), this.$createWorkerFromOldConfig = function (t, n, r, i, s) { e.nameToUrl && !e.toUrl && (e.toUrl = e.nameToUrl); if (o.get("packaged") || !e.toUrl) i = i || o.moduleUrl(n, "worker"); else { var u = this.$normalizePath; i = i || u(e.toUrl("ace/worker/worker.js", null, "_")); var f = {}; t.forEach(function (t) { f[t] = u(e.toUrl(t, null, "_").replace(/(\.js)?(\?.*)?$/, "")) }) } return this.$worker = a(i), s && this.send("importScripts", s), this.$worker.postMessage({ init: !0, tlns: f, module: n, classname: r }), this.$worker }, this.onMessage = function (e) { var t = e.data; switch (t.type) { case "event": this._signal(t.name, { data: t.data }); break; case "call": var n = this.callbacks[t.id]; n && (n(t.data), delete this.callbacks[t.id]); break; case "error": this.reportError(t.data); break; case "log": window.console && console.log && console.log.apply(console, t.data) } }, this.reportError = function (e) { window.console && console.error && console.error(e) }, this.$normalizePath = function (e) { return i.qualifyURL(e) }, this.terminate = function () { this._signal("terminate", {}), this.deltaQueue = null, this.$worker.terminate(), this.$worker = null, this.$doc && this.$doc.off("change", this.changeListener), this.$doc = null }, this.send = function (e, t) { this.$worker.postMessage({ command: e, args: t }) }, this.call = function (e, t, n) { if (n) { var r = this.callbackId++; this.callbacks[r] = n, t.push(r) } this.send(e, t) }, this.emit = function (e, t) { try { t.data && t.data.err && (t.data.err = { message: t.data.err.message, stack: t.data.err.stack, code: t.data.err.code }), this.$worker.postMessage({ event: e, data: { data: t.data } }) } catch (n) { console.error(n.stack) } }, this.attachToDocument = function (e) { this.$doc && this.terminate(), this.$doc = e, this.call("setValue", [e.getValue()]), e.on("change", this.changeListener) }, this.changeListener = function (e) { this.deltaQueue || (this.deltaQueue = [], setTimeout(this.$sendDeltaQueue, 0)), e.action == "insert" ? this.deltaQueue.push(e.start, e.lines) : this.deltaQueue.push(e.start, e.end) }, this.$sendDeltaQueue = function () { var e = this.deltaQueue; if (!e) return; this.deltaQueue = null, e.length > 50 && e.length > this.$doc.getLength() >> 1 ? this.call("setValue", [this.$doc.getValue()]) : this.emit("change", { data: e }) } }).call(f.prototype); var l = function (e, t, n) { var r = null, i = !1, u = Object.create(s), a = [], l = new f({ messageBuffer: a, terminate: function () { }, postMessage: function (e) { a.push(e); if (!r) return; i ? setTimeout(c) : c() } }); l.setEmitSync = function (e) { i = e }; var c = function () { var e = a.shift(); e.command ? r[e.command].apply(r, e.args) : e.event && u._signal(e.event, e.data) }; return u.postMessage = function (e) { l.onMessage({ data: e }) }, u.callback = function (e, t) { this.postMessage({ type: "call", id: t, data: e }) }, u.emit = function (e, t) { this.postMessage({ type: "event", name: e, data: t }) }, o.loadModule(["worker", t], function (e) { r = new e[n](u); while (a.length) c() }), l }; t.UIWorkerClient = l, t.WorkerClient = f, t.createWorker = a }), define("ace/placeholder", ["require", "exports", "module", "ace/range", "ace/lib/event_emitter", "ace/lib/oop"], function (e, t, n) { "use strict"; var r = e("./range").Range, i = e("./lib/event_emitter").EventEmitter, s = e("./lib/oop"), o = function (e, t, n, r, i, s) { var o = this; this.length = t, this.session = e, this.doc = e.getDocument(), this.mainClass = i, this.othersClass = s, this.$onUpdate = this.onUpdate.bind(this), this.doc.on("change", this.$onUpdate), this.$others = r, this.$onCursorChange = function () { setTimeout(function () { o.onCursorChange() }) }, this.$pos = n; var u = e.getUndoManager().$undoStack || e.getUndoManager().$undostack || { length: -1 }; this.$undoStackDepth = u.length, this.setup(), e.selection.on("changeCursor", this.$onCursorChange) }; (function () { s.implement(this, i), this.setup = function () { var e = this, t = this.doc, n = this.session; this.selectionBefore = n.selection.toJSON(), n.selection.inMultiSelectMode && n.selection.toSingleRange(), this.pos = t.createAnchor(this.$pos.row, this.$pos.column); var i = this.pos; i.$insertRight = !0, i.detach(), i.markerId = n.addMarker(new r(i.row, i.column, i.row, i.column + this.length), this.mainClass, null, !1), this.others = [], this.$others.forEach(function (n) { var r = t.createAnchor(n.row, n.column); r.$insertRight = !0, r.detach(), e.others.push(r) }), n.setUndoSelect(!1) }, this.showOtherMarkers = function () { if (this.othersActive) return; var e = this.session, t = this; this.othersActive = !0, this.others.forEach(function (n) { n.markerId = e.addMarker(new r(n.row, n.column, n.row, n.column + t.length), t.othersClass, null, !1) }) }, this.hideOtherMarkers = function () { if (!this.othersActive) return; this.othersActive = !1; for (var e = 0; e < this.others.length; e++)this.session.removeMarker(this.others[e].markerId) }, this.onUpdate = function (e) { if (this.$updating) return this.updateAnchors(e); var t = e; if (t.start.row !== t.end.row) return; if (t.start.row !== this.pos.row) return; this.$updating = !0; var n = e.action === "insert" ? t.end.column - t.start.column : t.start.column - t.end.column, i = t.start.column >= this.pos.column && t.start.column <= this.pos.column + this.length + 1, s = t.start.column - this.pos.column; this.updateAnchors(e), i && (this.length += n); if (i && !this.session.$fromUndo) if (e.action === "insert") for (var o = this.others.length - 1; o >= 0; o--) { var u = this.others[o], a = { row: u.row, column: u.column + s }; this.doc.insertMergedLines(a, e.lines) } else if (e.action === "remove") for (var o = this.others.length - 1; o >= 0; o--) { var u = this.others[o], a = { row: u.row, column: u.column + s }; this.doc.remove(new r(a.row, a.column, a.row, a.column - n)) } this.$updating = !1, this.updateMarkers() }, this.updateAnchors = function (e) { this.pos.onChange(e); for (var t = this.others.length; t--;)this.others[t].onChange(e); this.updateMarkers() }, this.updateMarkers = function () { if (this.$updating) return; var e = this, t = this.session, n = function (n, i) { t.removeMarker(n.markerId), n.markerId = t.addMarker(new r(n.row, n.column, n.row, n.column + e.length), i, null, !1) }; n(this.pos, this.mainClass); for (var i = this.others.length; i--;)n(this.others[i], this.othersClass) }, this.onCursorChange = function (e) { if (this.$updating || !this.session) return; var t = this.session.selection.getCursor(); t.row === this.pos.row && t.column >= this.pos.column && t.column <= this.pos.column + this.length ? (this.showOtherMarkers(), this._emit("cursorEnter", e)) : (this.hideOtherMarkers(), this._emit("cursorLeave", e)) }, this.detach = function () { this.session.removeMarker(this.pos && this.pos.markerId), this.hideOtherMarkers(), this.doc.off("change", this.$onUpdate), this.session.selection.off("changeCursor", this.$onCursorChange), this.session.setUndoSelect(!0), this.session = null }, this.cancel = function () { if (this.$undoStackDepth === -1) return; var e = this.session.getUndoManager(), t = (e.$undoStack || e.$undostack).length - this.$undoStackDepth; for (var n = 0; n < t; n++)e.undo(this.session, !0); this.selectionBefore && this.session.selection.fromJSON(this.selectionBefore) } }).call(o.prototype), t.PlaceHolder = o }), define("ace/mouse/multi_select_handler", ["require", "exports", "module", "ace/lib/event", "ace/lib/useragent"], function (e, t, n) { function s(e, t) { return e.row == t.row && e.column == t.column } function o(e) { var t = e.domEvent, n = t.altKey, o = t.shiftKey, u = t.ctrlKey, a = e.getAccelKey(), f = e.getButton(); u && i.isMac && (f = t.button); if (e.editor.inMultiSelectMode && f == 2) { e.editor.textInput.onContextMenu(e.domEvent); return } if (!u && !n && !a) { f === 0 && e.editor.inMultiSelectMode && e.editor.exitMultiSelectMode(); return } if (f !== 0) return; var l = e.editor, c = l.selection, h = l.inMultiSelectMode, p = e.getDocumentPosition(), d = c.getCursor(), v = e.inSelection() || c.isEmpty() && s(p, d), m = e.x, g = e.y, y = function (e) { m = e.clientX, g = e.clientY }, b = l.session, w = l.renderer.pixelToScreenCoordinates(m, g), E = w, S; if (l.$mouseHandler.$enableJumpToDef) u && n || a && n ? S = o ? "block" : "add" : n && l.$blockSelectEnabled && (S = "block"); else if (a && !n) { S = "add"; if (!h && o) return } else n && l.$blockSelectEnabled && (S = "block"); S && i.isMac && t.ctrlKey && l.$mouseHandler.cancelContextMenu(); if (S == "add") { if (!h && v) return; if (!h) { var x = c.toOrientedRange(); l.addSelectionMarker(x) } var T = c.rangeList.rangeAtPoint(p); l.inVirtualSelectionMode = !0, o && (T = null, x = c.ranges[0] || x, l.removeSelectionMarker(x)), l.once("mouseup", function () { var e = c.toOrientedRange(); T && e.isEmpty() && s(T.cursor, e.cursor) ? c.substractPoint(e.cursor) : (o ? c.substractPoint(x.cursor) : x && (l.removeSelectionMarker(x), c.addRange(x)), c.addRange(e)), l.inVirtualSelectionMode = !1 }) } else if (S == "block") { e.stop(), l.inVirtualSelectionMode = !0; var N, C = [], k = function () { var e = l.renderer.pixelToScreenCoordinates(m, g), t = b.screenToDocumentPosition(e.row, e.column, e.offsetX); if (s(E, e) && s(t, c.lead)) return; E = e, l.selection.moveToPosition(t), l.renderer.scrollCursorIntoView(), l.removeSelectionMarkers(C), C = c.rectangularRangeBlock(E, w), l.$mouseHandler.$clickSelection && C.length == 1 && C[0].isEmpty() && (C[0] = l.$mouseHandler.$clickSelection.clone()), C.forEach(l.addSelectionMarker, l), l.updateSelectionMarkers() }; h && !a ? c.toSingleRange() : !h && a && (N = c.toOrientedRange(), l.addSelectionMarker(N)), o ? w = b.documentToScreenPosition(c.lead) : c.moveToPosition(p), E = { row: -1, column: -1 }; var L = function (e) { k(), clearInterval(O), l.removeSelectionMarkers(C), C.length || (C = [c.toOrientedRange()]), N && (l.removeSelectionMarker(N), c.toSingleRange(N)); for (var t = 0; t < C.length; t++)c.addRange(C[t]); l.inVirtualSelectionMode = !1, l.$mouseHandler.$clickSelection = null }, A = k; r.capture(l.container, y, L); var O = setInterval(function () { A() }, 20); return e.preventDefault() } } var r = e("../lib/event"), i = e("../lib/useragent"); t.onMouseDown = o }), define("ace/commands/multi_select_commands", ["require", "exports", "module", "ace/keyboard/hash_handler"], function (e, t, n) { t.defaultCommands = [{ name: "addCursorAbove", description: "Add cursor above", exec: function (e) { e.selectMoreLines(-1) }, bindKey: { win: "Ctrl-Alt-Up", mac: "Ctrl-Alt-Up" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "addCursorBelow", description: "Add cursor below", exec: function (e) { e.selectMoreLines(1) }, bindKey: { win: "Ctrl-Alt-Down", mac: "Ctrl-Alt-Down" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "addCursorAboveSkipCurrent", description: "Add cursor above (skip current)", exec: function (e) { e.selectMoreLines(-1, !0) }, bindKey: { win: "Ctrl-Alt-Shift-Up", mac: "Ctrl-Alt-Shift-Up" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "addCursorBelowSkipCurrent", description: "Add cursor below (skip current)", exec: function (e) { e.selectMoreLines(1, !0) }, bindKey: { win: "Ctrl-Alt-Shift-Down", mac: "Ctrl-Alt-Shift-Down" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "selectMoreBefore", description: "Select more before", exec: function (e) { e.selectMore(-1) }, bindKey: { win: "Ctrl-Alt-Left", mac: "Ctrl-Alt-Left" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "selectMoreAfter", description: "Select more after", exec: function (e) { e.selectMore(1) }, bindKey: { win: "Ctrl-Alt-Right", mac: "Ctrl-Alt-Right" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "selectNextBefore", description: "Select next before", exec: function (e) { e.selectMore(-1, !0) }, bindKey: { win: "Ctrl-Alt-Shift-Left", mac: "Ctrl-Alt-Shift-Left" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "selectNextAfter", description: "Select next after", exec: function (e) { e.selectMore(1, !0) }, bindKey: { win: "Ctrl-Alt-Shift-Right", mac: "Ctrl-Alt-Shift-Right" }, scrollIntoView: "cursor", readOnly: !0 }, { name: "toggleSplitSelectionIntoLines", description: "Split into lines", exec: function (e) { e.multiSelect.rangeCount > 1 ? e.multiSelect.joinSelections() : e.multiSelect.splitIntoLines() }, bindKey: { win: "Ctrl-Alt-L", mac: "Ctrl-Alt-L" }, readOnly: !0 }, { name: "splitSelectionIntoLines", description: "Split into lines", exec: function (e) { e.multiSelect.splitIntoLines() }, readOnly: !0 }, { name: "alignCursors", description: "Align cursors", exec: function (e) { e.alignCursors() }, bindKey: { win: "Ctrl-Alt-A", mac: "Ctrl-Alt-A" }, scrollIntoView: "cursor" }, { name: "findAll", description: "Find all", exec: function (e) { e.findAll() }, bindKey: { win: "Ctrl-Alt-K", mac: "Ctrl-Alt-G" }, scrollIntoView: "cursor", readOnly: !0 }], t.multiSelectCommands = [{ name: "singleSelection", description: "Single selection", bindKey: "esc", exec: function (e) { e.exitMultiSelectMode() }, scrollIntoView: "cursor", readOnly: !0, isAvailable: function (e) { return e && e.inMultiSelectMode } }]; var r = e("../keyboard/hash_handler").HashHandler; t.keyboardHandler = new r(t.multiSelectCommands) }), define("ace/multi_select", ["require", "exports", "module", "ace/range_list", "ace/range", "ace/selection", "ace/mouse/multi_select_handler", "ace/lib/event", "ace/lib/lang", "ace/commands/multi_select_commands", "ace/search", "ace/edit_session", "ace/editor", "ace/config"], function (e, t, n) { function h(e, t, n) { return c.$options.wrap = !0, c.$options.needle = t, c.$options.backwards = n == -1, c.find(e) } function v(e, t) { return e.row == t.row && e.column == t.column } function m(e) { if (e.$multiselectOnSessionChange) return; e.$onAddRange = e.$onAddRange.bind(e), e.$onRemoveRange = e.$onRemoveRange.bind(e), e.$onMultiSelect = e.$onMultiSelect.bind(e), e.$onSingleSelect = e.$onSingleSelect.bind(e), e.$multiselectOnSessionChange = t.onSessionChange.bind(e), e.$checkMultiselectChange = e.$checkMultiselectChange.bind(e), e.$multiselectOnSessionChange(e), e.on("changeSession", e.$multiselectOnSessionChange), e.on("mousedown", o), e.commands.addCommands(f.defaultCommands), g(e) } function g(e) { function r(t) { n && (e.renderer.setMouseCursor(""), n = !1) } if (!e.textInput) return; var t = e.textInput.getElement(), n = !1; u.addListener(t, "keydown", function (t) { var i = t.keyCode == 18 && !(t.ctrlKey || t.shiftKey || t.metaKey); e.$blockSelectEnabled && i ? n || (e.renderer.setMouseCursor("crosshair"), n = !0) : n && r() }, e), u.addListener(t, "keyup", r, e), u.addListener(t, "blur", r, e) } var r = e("./range_list").RangeList, i = e("./range").Range, s = e("./selection").Selection, o = e("./mouse/multi_select_handler").onMouseDown, u = e("./lib/event"), a = e("./lib/lang"), f = e("./commands/multi_select_commands"); t.commands = f.defaultCommands.concat(f.multiSelectCommands); var l = e("./search").Search, c = new l, p = e("./edit_session").EditSession; (function () { this.getSelectionMarkers = function () { return this.$selectionMarkers } }).call(p.prototype), function () { this.ranges = null, this.rangeList = null, this.addRange = function (e, t) { if (!e) return; if (!this.inMultiSelectMode && this.rangeCount === 0) { var n = this.toOrientedRange(); this.rangeList.add(n), this.rangeList.add(e); if (this.rangeList.ranges.length != 2) return this.rangeList.removeAll(), t || this.fromOrientedRange(e); this.rangeList.removeAll(), this.rangeList.add(n), this.$onAddRange(n) } e.cursor || (e.cursor = e.end); var r = this.rangeList.add(e); return this.$onAddRange(e), r.length && this.$onRemoveRange(r), this.rangeCount > 1 && !this.inMultiSelectMode && (this._signal("multiSelect"), this.inMultiSelectMode = !0, this.session.$undoSelect = !1, this.rangeList.attach(this.session)), t || this.fromOrientedRange(e) }, this.toSingleRange = function (e) { e = e || this.ranges[0]; var t = this.rangeList.removeAll(); t.length && this.$onRemoveRange(t), e && this.fromOrientedRange(e) }, this.substractPoint = function (e) { var t = this.rangeList.substractPoint(e); if (t) return this.$onRemoveRange(t), t[0] }, this.mergeOverlappingRanges = function () { var e = this.rangeList.merge(); e.length && this.$onRemoveRange(e) }, this.$onAddRange = function (e) { this.rangeCount = this.rangeList.ranges.length, this.ranges.unshift(e), this._signal("addRange", { range: e }) }, this.$onRemoveRange = function (e) { this.rangeCount = this.rangeList.ranges.length; if (this.rangeCount == 1 && this.inMultiSelectMode) { var t = this.rangeList.ranges.pop(); e.push(t), this.rangeCount = 0 } for (var n = e.length; n--;) { var r = this.ranges.indexOf(e[n]); this.ranges.splice(r, 1) } this._signal("removeRange", { ranges: e }), this.rangeCount === 0 && this.inMultiSelectMode && (this.inMultiSelectMode = !1, this._signal("singleSelect"), this.session.$undoSelect = !0, this.rangeList.detach(this.session)), t = t || this.ranges[0], t && !t.isEqual(this.getRange()) && this.fromOrientedRange(t) }, this.$initRangeList = function () { if (this.rangeList) return; this.rangeList = new r, this.ranges = [], this.rangeCount = 0 }, this.getAllRanges = function () { return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()] }, this.splitIntoLines = function () { var e = this.ranges.length ? this.ranges : [this.getRange()], t = []; for (var n = 0; n < e.length; n++) { var r = e[n], s = r.start.row, o = r.end.row; if (s === o) t.push(r.clone()); else { t.push(new i(s, r.start.column, s, this.session.getLine(s).length)); while (++s < o) t.push(this.getLineRange(s, !0)); t.push(new i(o, 0, o, r.end.column)) } n == 0 && !this.isBackwards() && (t = t.reverse()) } this.toSingleRange(); for (var n = t.length; n--;)this.addRange(t[n]) }, this.joinSelections = function () { var e = this.rangeList.ranges, t = e[e.length - 1], n = i.fromPoints(e[0].start, t.end); this.toSingleRange(), this.setSelectionRange(n, t.cursor == t.start) }, this.toggleBlockSelection = function () { if (this.rangeCount > 1) { var e = this.rangeList.ranges, t = e[e.length - 1], n = i.fromPoints(e[0].start, t.end); this.toSingleRange(), this.setSelectionRange(n, t.cursor == t.start) } else { var r = this.session.documentToScreenPosition(this.cursor), s = this.session.documentToScreenPosition(this.anchor), o = this.rectangularRangeBlock(r, s); o.forEach(this.addRange, this) } }, this.rectangularRangeBlock = function (e, t, n) { var r = [], s = e.column < t.column; if (s) var o = e.column, u = t.column, a = e.offsetX, f = t.offsetX; else var o = t.column, u = e.column, a = t.offsetX, f = e.offsetX; var l = e.row < t.row; if (l) var c = e.row, h = t.row; else var c = t.row, h = e.row; o < 0 && (o = 0), c < 0 && (c = 0), c == h && (n = !0); var p; for (var d = c; d <= h; d++) { var m = i.fromPoints(this.session.screenToDocumentPosition(d, o, a), this.session.screenToDocumentPosition(d, u, f)); if (m.isEmpty()) { if (p && v(m.end, p)) break; p = m.end } m.cursor = s ? m.start : m.end, r.push(m) } l && r.reverse(); if (!n) { var g = r.length - 1; while (r[g].isEmpty() && g > 0) g--; if (g > 0) { var y = 0; while (r[y].isEmpty()) y++ } for (var b = g; b >= y; b--)r[b].isEmpty() && r.splice(b, 1) } return r } }.call(s.prototype); var d = e("./editor").Editor; (function () { this.updateSelectionMarkers = function () { this.renderer.updateCursor(), this.renderer.updateBackMarkers() }, this.addSelectionMarker = function (e) { e.cursor || (e.cursor = e.end); var t = this.getSelectionStyle(); return e.marker = this.session.addMarker(e, "ace_selection", t), this.session.$selectionMarkers.push(e), this.session.selectionMarkerCount = this.session.$selectionMarkers.length, e }, this.removeSelectionMarker = function (e) { if (!e.marker) return; this.session.removeMarker(e.marker); var t = this.session.$selectionMarkers.indexOf(e); t != -1 && this.session.$selectionMarkers.splice(t, 1), this.session.selectionMarkerCount = this.session.$selectionMarkers.length }, this.removeSelectionMarkers = function (e) { var t = this.session.$selectionMarkers; for (var n = e.length; n--;) { var r = e[n]; if (!r.marker) continue; this.session.removeMarker(r.marker); var i = t.indexOf(r); i != -1 && t.splice(i, 1) } this.session.selectionMarkerCount = t.length }, this.$onAddRange = function (e) { this.addSelectionMarker(e.range), this.renderer.updateCursor(), this.renderer.updateBackMarkers() }, this.$onRemoveRange = function (e) { this.removeSelectionMarkers(e.ranges), this.renderer.updateCursor(), this.renderer.updateBackMarkers() }, this.$onMultiSelect = function (e) { if (this.inMultiSelectMode) return; this.inMultiSelectMode = !0, this.setStyle("ace_multiselect"), this.keyBinding.addKeyboardHandler(f.keyboardHandler), this.commands.setDefaultHandler("exec", this.$onMultiSelectExec), this.renderer.updateCursor(), this.renderer.updateBackMarkers() }, this.$onSingleSelect = function (e) { if (this.session.multiSelect.inVirtualMode) return; this.inMultiSelectMode = !1, this.unsetStyle("ace_multiselect"), this.keyBinding.removeKeyboardHandler(f.keyboardHandler), this.commands.removeDefaultHandler("exec", this.$onMultiSelectExec), this.renderer.updateCursor(), this.renderer.updateBackMarkers(), this._emit("changeSelection") }, this.$onMultiSelectExec = function (e) { var t = e.command, n = e.editor; if (!n.multiSelect) return; if (!t.multiSelectAction) { var r = t.exec(n, e.args || {}); n.multiSelect.addRange(n.multiSelect.toOrientedRange()), n.multiSelect.mergeOverlappingRanges() } else t.multiSelectAction == "forEach" ? r = n.forEachSelection(t, e.args) : t.multiSelectAction == "forEachLine" ? r = n.forEachSelection(t, e.args, !0) : t.multiSelectAction == "single" ? (n.exitMultiSelectMode(), r = t.exec(n, e.args || {})) : r = t.multiSelectAction(n, e.args || {}); return r }, this.forEachSelection = function (e, t, n) { if (this.inVirtualSelectionMode) return; var r = n && n.keepOrder, i = n == 1 || n && n.$byLines, o = this.session, u = this.selection, a = u.rangeList, f = (r ? u : a).ranges, l; if (!f.length) return e.exec ? e.exec(this, t || {}) : e(this, t || {}); var c = u._eventRegistry; u._eventRegistry = {}; var h = new s(o); this.inVirtualSelectionMode = !0; for (var p = f.length; p--;) { if (i) while (p > 0 && f[p].start.row == f[p - 1].end.row) p--; h.fromOrientedRange(f[p]), h.index = p, this.selection = o.selection = h; var d = e.exec ? e.exec(this, t || {}) : e(this, t || {}); !l && d !== undefined && (l = d), h.toOrientedRange(f[p]) } h.detach(), this.selection = o.selection = u, this.inVirtualSelectionMode = !1, u._eventRegistry = c, u.mergeOverlappingRanges(), u.ranges[0] && u.fromOrientedRange(u.ranges[0]); var v = this.renderer.$scrollAnimation; return this.onCursorChange(), this.onSelectionChange(), v && v.from == v.to && this.renderer.animateScrolling(v.from), l }, this.exitMultiSelectMode = function () { if (!this.inMultiSelectMode || this.inVirtualSelectionMode) return; this.multiSelect.toSingleRange() }, this.getSelectedText = function () { var e = ""; if (this.inMultiSelectMode && !this.inVirtualSelectionMode) { var t = this.multiSelect.rangeList.ranges, n = []; for (var r = 0; r < t.length; r++)n.push(this.session.getTextRange(t[r])); var i = this.session.getDocument().getNewLineCharacter(); e = n.join(i), e.length == (n.length - 1) * i.length && (e = "") } else this.selection.isEmpty() || (e = this.session.getTextRange(this.getSelectionRange())); return e }, this.$checkMultiselectChange = function (e, t) { if (this.inMultiSelectMode && !this.inVirtualSelectionMode) { var n = this.multiSelect.ranges[0]; if (this.multiSelect.isEmpty() && t == this.multiSelect.anchor) return; var r = t == this.multiSelect.anchor ? n.cursor == n.start ? n.end : n.start : n.cursor; r.row != t.row || this.session.$clipPositionToDocument(r.row, r.column).column != t.column ? this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange()) : this.multiSelect.mergeOverlappingRanges() } }, this.findAll = function (e, t, n) { t = t || {}, t.needle = e || t.needle; if (t.needle == undefined) { var r = this.selection.isEmpty() ? this.selection.getWordRange() : this.selection.getRange(); t.needle = this.session.getTextRange(r) } this.$search.set(t); var i = this.$search.findAll(this.session); if (!i.length) return 0; var s = this.multiSelect; n || s.toSingleRange(i[0]); for (var o = i.length; o--;)s.addRange(i[o], !0); return r && s.rangeList.rangeAtPoint(r.start) && s.addRange(r, !0), i.length }, this.selectMoreLines = function (e, t) { var n = this.selection.toOrientedRange(), r = n.cursor == n.end, s = this.session.documentToScreenPosition(n.cursor); this.selection.$desiredColumn && (s.column = this.selection.$desiredColumn); var o = this.session.screenToDocumentPosition(s.row + e, s.column); if (!n.isEmpty()) var u = this.session.documentToScreenPosition(r ? n.end : n.start), a = this.session.screenToDocumentPosition(u.row + e, u.column); else var a = o; if (r) { var f = i.fromPoints(o, a); f.cursor = f.start } else { var f = i.fromPoints(a, o); f.cursor = f.end } f.desiredColumn = s.column; if (!this.selection.inMultiSelectMode) this.selection.addRange(n); else if (t) var l = n.cursor; this.selection.addRange(f), l && this.selection.substractPoint(l) }, this.transposeSelections = function (e) { var t = this.session, n = t.multiSelect, r = n.ranges; for (var i = r.length; i--;) { var s = r[i]; if (s.isEmpty()) { var o = t.getWordRange(s.start.row, s.start.column); s.start.row = o.start.row, s.start.column = o.start.column, s.end.row = o.end.row, s.end.column = o.end.column } } n.mergeOverlappingRanges(); var u = []; for (var i = r.length; i--;) { var s = r[i]; u.unshift(t.getTextRange(s)) } e < 0 ? u.unshift(u.pop()) : u.push(u.shift()); for (var i = r.length; i--;) { var s = r[i], o = s.clone(); t.replace(s, u[i]), s.start.row = o.start.row, s.start.column = o.start.column } n.fromOrientedRange(n.ranges[0]) }, this.selectMore = function (e, t, n) { var r = this.session, i = r.multiSelect, s = i.toOrientedRange(); if (s.isEmpty()) { s = r.getWordRange(s.start.row, s.start.column), s.cursor = e == -1 ? s.start : s.end, this.multiSelect.addRange(s); if (n) return } var o = r.getTextRange(s), u = h(r, o, e); u && (u.cursor = e == -1 ? u.start : u.end, this.session.unfold(u), this.multiSelect.addRange(u), this.renderer.scrollCursorIntoView(null, .5)), t && this.multiSelect.substractPoint(s.cursor) }, this.alignCursors = function () { var e = this.session, t = e.multiSelect, n = t.ranges, r = -1, s = n.filter(function (e) { if (e.cursor.row == r) return !0; r = e.cursor.row }); if (!n.length || s.length == n.length - 1) { var o = this.selection.getRange(), u = o.start.row, f = o.end.row, l = u == f; if (l) { var c = this.session.getLength(), h; do h = this.session.getLine(f); while (/[=:]/.test(h) && ++f < c); do h = this.session.getLine(u); while (/[=:]/.test(h) && --u > 0); u < 0 && (u = 0), f >= c && (f = c - 1) } var p = this.session.removeFullLines(u, f); p = this.$reAlignText(p, l), this.session.insert({ row: u, column: 0 }, p.join("\n") + "\n"), l || (o.start.column = 0, o.end.column = p[p.length - 1].length), this.selection.setRange(o) } else { s.forEach(function (e) { t.substractPoint(e.cursor) }); var d = 0, v = Infinity, m = n.map(function (t) { var n = t.cursor, r = e.getLine(n.row), i = r.substr(n.column).search(/\S/g); return i == -1 && (i = 0), n.column > d && (d = n.column), i < v && (v = i), i }); n.forEach(function (t, n) { var r = t.cursor, s = d - r.column, o = m[n] - v; s > o ? e.insert(r, a.stringRepeat(" ", s - o)) : e.remove(new i(r.row, r.column, r.row, r.column - s + o)), t.start.column = t.end.column = d, t.start.row = t.end.row = r.row, t.cursor = t.end }), t.fromOrientedRange(n[0]), this.renderer.updateCursor(), this.renderer.updateBackMarkers() } }, this.$reAlignText = function (e, t) { function u(e) { return a.stringRepeat(" ", e) } function f(e) { return e[2] ? u(i) + e[2] + u(s - e[2].length + o) + e[4].replace(/^([=:])\s+/, "$1 ") : e[0] } function l(e) { return e[2] ? u(i + s - e[2].length) + e[2] + u(o) + e[4].replace(/^([=:])\s+/, "$1 ") : e[0] } function c(e) { return e[2] ? u(i) + e[2] + u(o) + e[4].replace(/^([=:])\s+/, "$1 ") : e[0] } var n = !0, r = !0, i, s, o; return e.map(function (e) { var t = e.match(/(\s*)(.*?)(\s*)([=:].*)/); return t ? i == null ? (i = t[1].length, s = t[2].length, o = t[3].length, t) : (i + s + o != t[1].length + t[2].length + t[3].length && (r = !1), i != t[1].length && (n = !1), i > t[1].length && (i = t[1].length), s < t[2].length && (s = t[2].length), o > t[3].length && (o = t[3].length), t) : [e] }).map(t ? f : n ? r ? l : f : c) } }).call(d.prototype), t.onSessionChange = function (e) { var t = e.session; t && !t.multiSelect && (t.$selectionMarkers = [], t.selection.$initRangeList(), t.multiSelect = t.selection), this.multiSelect = t && t.multiSelect; var n = e.oldSession; n && (n.multiSelect.off("addRange", this.$onAddRange), n.multiSelect.off("removeRange", this.$onRemoveRange), n.multiSelect.off("multiSelect", this.$onMultiSelect), n.multiSelect.off("singleSelect", this.$onSingleSelect), n.multiSelect.lead.off("change", this.$checkMultiselectChange), n.multiSelect.anchor.off("change", this.$checkMultiselectChange)), t && (t.multiSelect.on("addRange", this.$onAddRange), t.multiSelect.on("removeRange", this.$onRemoveRange), t.multiSelect.on("multiSelect", this.$onMultiSelect), t.multiSelect.on("singleSelect", this.$onSingleSelect), t.multiSelect.lead.on("change", this.$checkMultiselectChange), t.multiSelect.anchor.on("change", this.$checkMultiselectChange)), t && this.inMultiSelectMode != t.selection.inMultiSelectMode && (t.selection.inMultiSelectMode ? this.$onMultiSelect() : this.$onSingleSelect()) }, t.MultiSelect = m, e("./config").defineOptions(d.prototype, "editor", { enableMultiselect: { set: function (e) { m(this), e ? (this.on("changeSession", this.$multiselectOnSessionChange), this.on("mousedown", o)) : (this.off("changeSession", this.$multiselectOnSessionChange), this.off("mousedown", o)) }, value: !0 }, enableBlockSelect: { set: function (e) { this.$blockSelectEnabled = e }, value: !0 } }) }), define("ace/mode/folding/fold_mode", ["require", "exports", "module", "ace/range"], function (e, t, n) { "use strict"; var r = e("../../range").Range, i = t.FoldMode = function () { }; (function () { this.foldingStartMarker = null, this.foldingStopMarker = null, this.getFoldWidget = function (e, t, n) { var r = e.getLine(n); return this.foldingStartMarker.test(r) ? "start" : t == "markbeginend" && this.foldingStopMarker && this.foldingStopMarker.test(r) ? "end" : "" }, this.getFoldWidgetRange = function (e, t, n) { return null }, this.indentationBlock = function (e, t, n) { var i = /\S/, s = e.getLine(t), o = s.search(i); if (o == -1) return; var u = n || s.length, a = e.getLength(), f = t, l = t; while (++t < a) { var c = e.getLine(t).search(i); if (c == -1) continue; if (c <= o) { var h = e.getTokenAt(t, 0); if (!h || h.type !== "string") break } l = t } if (l > f) { var p = e.getLine(l).length; return new r(f, u, l, p) } }, this.openingBracketBlock = function (e, t, n, i, s) { var o = { row: n, column: i + 1 }, u = e.$findClosingBracket(t, o, s); if (!u) return; var a = e.foldWidgets[u.row]; return a == null && (a = e.getFoldWidget(u.row)), a == "start" && u.row > o.row && (u.row--, u.column = e.getLine(u.row).length), r.fromPoints(o, u) }, this.closingBracketBlock = function (e, t, n, i, s) { var o = { row: n, column: i }, u = e.$findOpeningBracket(t, o); if (!u) return; return u.column++, o.column--, r.fromPoints(u, o) } }).call(i.prototype) }), define("ace/theme/textmate", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { "use strict"; t.isDark = !1, t.cssClass = "ace-tm", t.cssText = '.ace-tm .ace_gutter {background: #f0f0f0;color: #333;}.ace-tm .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-tm .ace_fold {background-color: #6B72E6;}.ace-tm {background-color: #FFFFFF;color: black;}.ace-tm .ace_cursor {color: black;}.ace-tm .ace_invisible {color: rgb(191, 191, 191);}.ace-tm .ace_storage,.ace-tm .ace_keyword {color: blue;}.ace-tm .ace_constant {color: rgb(197, 6, 11);}.ace-tm .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-tm .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-tm .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-tm .ace_invalid {background-color: rgba(255, 0, 0, 0.1);color: red;}.ace-tm .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-tm .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-tm .ace_support.ace_type,.ace-tm .ace_support.ace_class {color: rgb(109, 121, 222);}.ace-tm .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-tm .ace_string {color: rgb(3, 106, 7);}.ace-tm .ace_comment {color: rgb(76, 136, 107);}.ace-tm .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-tm .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-tm .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-tm .ace_variable {color: rgb(49, 132, 149);}.ace-tm .ace_xml-pe {color: rgb(104, 104, 91);}.ace-tm .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-tm .ace_heading {color: rgb(12, 7, 255);}.ace-tm .ace_list {color:rgb(185, 6, 144);}.ace-tm .ace_meta.ace_tag {color:rgb(0, 22, 142);}.ace-tm .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-tm .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-tm.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;}.ace-tm .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-tm .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-tm .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-tm .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-tm .ace_gutter-active-line {background-color : #dcdcdc;}.ace-tm .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-tm .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}', t.$id = "ace/theme/textmate"; var r = e("../lib/dom"); r.importCssString(t.cssText, t.cssClass) }), define("ace/line_widgets", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { "use strict"; function i(e) { this.session = e, this.session.widgetManager = this, this.session.getRowLength = this.getRowLength, this.session.$getWidgetScreenLength = this.$getWidgetScreenLength, this.updateOnChange = this.updateOnChange.bind(this), this.renderWidgets = this.renderWidgets.bind(this), this.measureWidgets = this.measureWidgets.bind(this), this.session._changedWidgets = [], this.$onChangeEditor = this.$onChangeEditor.bind(this), this.session.on("change", this.updateOnChange), this.session.on("changeFold", this.updateOnFold), this.session.on("changeEditor", this.$onChangeEditor) } var r = e("./lib/dom"); (function () { this.getRowLength = function (e) { var t; return this.lineWidgets ? t = this.lineWidgets[e] && this.lineWidgets[e].rowCount || 0 : t = 0, !this.$useWrapMode || !this.$wrapData[e] ? 1 + t : this.$wrapData[e].length + 1 + t }, this.$getWidgetScreenLength = function () { var e = 0; return this.lineWidgets.forEach(function (t) { t && t.rowCount && !t.hidden && (e += t.rowCount) }), e }, this.$onChangeEditor = function (e) { this.attach(e.editor) }, this.attach = function (e) { e && e.widgetManager && e.widgetManager != this && e.widgetManager.detach(); if (this.editor == e) return; this.detach(), this.editor = e, e && (e.widgetManager = this, e.renderer.on("beforeRender", this.measureWidgets), e.renderer.on("afterRender", this.renderWidgets)) }, this.detach = function (e) { var t = this.editor; if (!t) return; this.editor = null, t.widgetManager = null, t.renderer.off("beforeRender", this.measureWidgets), t.renderer.off("afterRender", this.renderWidgets); var n = this.session.lineWidgets; n && n.forEach(function (e) { e && e.el && e.el.parentNode && (e._inDocument = !1, e.el.parentNode.removeChild(e.el)) }) }, this.updateOnFold = function (e, t) { var n = t.lineWidgets; if (!n || !e.action) return; var r = e.data, i = r.start.row, s = r.end.row, o = e.action == "add"; for (var u = i + 1; u < s; u++)n[u] && (n[u].hidden = o); n[s] && (o ? n[i] ? n[s].hidden = o : n[i] = n[s] : (n[i] == n[s] && (n[i] = undefined), n[s].hidden = o)) }, this.updateOnChange = function (e) { var t = this.session.lineWidgets; if (!t) return; var n = e.start.row, r = e.end.row - n; if (r !== 0) if (e.action == "remove") { var i = t.splice(n + 1, r); !t[n] && i[i.length - 1] && (t[n] = i.pop()), i.forEach(function (e) { e && this.removeLineWidget(e) }, this), this.$updateRows() } else { var s = new Array(r); t[n] && t[n].column != null && e.start.column > t[n].column && n++, s.unshift(n, 0), t.splice.apply(t, s), this.$updateRows() } }, this.$updateRows = function () { var e = this.session.lineWidgets; if (!e) return; var t = !0; e.forEach(function (e, n) { if (e) { t = !1, e.row = n; while (e.$oldWidget) e.$oldWidget.row = n, e = e.$oldWidget } }), t && (this.session.lineWidgets = null) }, this.$registerLineWidget = function (e) { this.session.lineWidgets || (this.session.lineWidgets = new Array(this.session.getLength())); var t = this.session.lineWidgets[e.row]; return t && (e.$oldWidget = t, t.el && t.el.parentNode && (t.el.parentNode.removeChild(t.el), t._inDocument = !1)), this.session.lineWidgets[e.row] = e, e }, this.addLineWidget = function (e) { this.$registerLineWidget(e), e.session = this.session; if (!this.editor) return e; var t = this.editor.renderer; e.html && !e.el && (e.el = r.createElement("div"), e.el.innerHTML = e.html), e.el && (r.addCssClass(e.el, "ace_lineWidgetContainer"), e.el.style.position = "absolute", e.el.style.zIndex = 5, t.container.appendChild(e.el), e._inDocument = !0, e.coverGutter || (e.el.style.zIndex = 3), e.pixelHeight == null && (e.pixelHeight = e.el.offsetHeight)), e.rowCount == null && (e.rowCount = e.pixelHeight / t.layerConfig.lineHeight); var n = this.session.getFoldAt(e.row, 0); e.$fold = n; if (n) { var i = this.session.lineWidgets; e.row == n.end.row && !i[n.start.row] ? i[n.start.row] = e : e.hidden = !0 } return this.session._emit("changeFold", { data: { start: { row: e.row } } }), this.$updateRows(), this.renderWidgets(null, t), this.onWidgetChanged(e), e }, this.removeLineWidget = function (e) { e._inDocument = !1, e.session = null, e.el && e.el.parentNode && e.el.parentNode.removeChild(e.el); if (e.editor && e.editor.destroy) try { e.editor.destroy() } catch (t) { } if (this.session.lineWidgets) { var n = this.session.lineWidgets[e.row]; if (n == e) this.session.lineWidgets[e.row] = e.$oldWidget, e.$oldWidget && this.onWidgetChanged(e.$oldWidget); else while (n) { if (n.$oldWidget == e) { n.$oldWidget = e.$oldWidget; break } n = n.$oldWidget } } this.session._emit("changeFold", { data: { start: { row: e.row } } }), this.$updateRows() }, this.getWidgetsAtRow = function (e) { var t = this.session.lineWidgets, n = t && t[e], r = []; while (n) r.push(n), n = n.$oldWidget; return r }, this.onWidgetChanged = function (e) { this.session._changedWidgets.push(e), this.editor && this.editor.renderer.updateFull() }, this.measureWidgets = function (e, t) { var n = this.session._changedWidgets, r = t.layerConfig; if (!n || !n.length) return; var i = Infinity; for (var s = 0; s < n.length; s++) { var o = n[s]; if (!o || !o.el) continue; if (o.session != this.session) continue; if (!o._inDocument) { if (this.session.lineWidgets[o.row] != o) continue; o._inDocument = !0, t.container.appendChild(o.el) } o.h = o.el.offsetHeight, o.fixedWidth || (o.w = o.el.offsetWidth, o.screenWidth = Math.ceil(o.w / r.characterWidth)); var u = o.h / r.lineHeight; o.coverLine && (u -= this.session.getRowLineCount(o.row), u < 0 && (u = 0)), o.rowCount != u && (o.rowCount = u, o.row < i && (i = o.row)) } i != Infinity && (this.session._emit("changeFold", { data: { start: { row: i } } }), this.session.lineWidgetWidth = null), this.session._changedWidgets = [] }, this.renderWidgets = function (e, t) { var n = t.layerConfig, r = this.session.lineWidgets; if (!r) return; var i = Math.min(this.firstRow, n.firstRow), s = Math.max(this.lastRow, n.lastRow, r.length); while (i > 0 && !r[i]) i--; this.firstRow = n.firstRow, this.lastRow = n.lastRow, t.$cursorLayer.config = n; for (var o = i; o <= s; o++) { var u = r[o]; if (!u || !u.el) continue; if (u.hidden) { u.el.style.top = -100 - (u.pixelHeight || 0) + "px"; continue } u._inDocument || (u._inDocument = !0, t.container.appendChild(u.el)); var a = t.$cursorLayer.getPixelPosition({ row: o, column: 0 }, !0).top; u.coverLine || (a += n.lineHeight * this.session.getRowLineCount(u.row)), u.el.style.top = a - n.offset + "px"; var f = u.coverGutter ? 0 : t.gutterWidth; u.fixedWidth || (f -= t.scrollLeft), u.el.style.left = f + "px", u.fullWidth && u.screenWidth && (u.el.style.minWidth = n.width + 2 * n.padding + "px"), u.fixedWidth ? u.el.style.right = t.scrollBar.getWidth() + "px" : u.el.style.right = "" } } }).call(i.prototype), t.LineWidgets = i }), define("ace/ext/error_marker", ["require", "exports", "module", "ace/line_widgets", "ace/lib/dom", "ace/range"], function (e, t, n) { "use strict"; function o(e, t, n) { var r = 0, i = e.length - 1; while (r <= i) { var s = r + i >> 1, o = n(t, e[s]); if (o > 0) r = s + 1; else { if (!(o < 0)) return s; i = s - 1 } } return -(r + 1) } function u(e, t, n) { var r = e.getAnnotations().sort(s.comparePoints); if (!r.length) return; var i = o(r, { row: t, column: -1 }, s.comparePoints); i < 0 && (i = -i - 1), i >= r.length ? i = n > 0 ? 0 : r.length - 1 : i === 0 && n < 0 && (i = r.length - 1); var u = r[i]; if (!u || !n) return; if (u.row === t) { do u = r[i += n]; while (u && u.row === t); if (!u) return r.slice() } var a = []; t = u.row; do a[n < 0 ? "unshift" : "push"](u), u = r[i += n]; while (u && u.row == t); return a.length && a } var r = e("../line_widgets").LineWidgets, i = e("../lib/dom"), s = e("../range").Range; t.showErrorMarker = function (e, t) { var n = e.session; n.widgetManager || (n.widgetManager = new r(n), n.widgetManager.attach(e)); var s = e.getCursorPosition(), o = s.row, a = n.widgetManager.getWidgetsAtRow(o).filter(function (e) { return e.type == "errorMarker" })[0]; a ? a.destroy() : o -= t; var f = u(n, o, t), l; if (f) { var c = f[0]; s.column = (c.pos && typeof c.column != "number" ? c.pos.sc : c.column) || 0, s.row = c.row, l = e.renderer.$gutterLayer.$annotations[s.row] } else { if (a) return; l = { text: ["Looks good!"], className: "ace_ok" } } e.session.unfold(s.row), e.selection.moveToPosition(s); var h = { row: s.row, fixedWidth: !0, coverGutter: !0, el: i.createElement("div"), type: "errorMarker" }, p = h.el.appendChild(i.createElement("div")), d = h.el.appendChild(i.createElement("div")); d.className = "error_widget_arrow " + l.className; var v = e.renderer.$cursorLayer.getPixelPosition(s).left; d.style.left = v + e.renderer.gutterWidth - 5 + "px", h.el.className = "error_widget_wrapper", p.className = "error_widget " + l.className, p.innerHTML = l.text.join("
"), p.appendChild(i.createElement("div")); var m = function (e, t, n) { if (t === 0 && (n === "esc" || n === "return")) return h.destroy(), { command: "null" } }; h.destroy = function () { if (e.$mouseHandler.isMousePressed) return; e.keyBinding.removeKeyboardHandler(m), n.widgetManager.removeLineWidget(h), e.off("changeSelection", h.destroy), e.off("changeSession", h.destroy), e.off("mouseup", h.destroy), e.off("change", h.destroy) }, e.keyBinding.addKeyboardHandler(m), e.on("changeSelection", h.destroy), e.on("changeSession", h.destroy), e.on("mouseup", h.destroy), e.on("change", h.destroy), e.session.widgetManager.addLineWidget(h), h.el.onmousedown = e.focus.bind(e), e.renderer.scrollCursorIntoView(null, .5, { bottom: h.el.offsetHeight }) }, i.importCssString(" .error_widget_wrapper { background: inherit; color: inherit; border:none } .error_widget { border-top: solid 2px; border-bottom: solid 2px; margin: 5px 0; padding: 10px 40px; white-space: pre-wrap; } .error_widget.ace_error, .error_widget_arrow.ace_error{ border-color: #ff5a5a } .error_widget.ace_warning, .error_widget_arrow.ace_warning{ border-color: #F1D817 } .error_widget.ace_info, .error_widget_arrow.ace_info{ border-color: #5a5a5a } .error_widget.ace_ok, .error_widget_arrow.ace_ok{ border-color: #5aaa5a } .error_widget_arrow { position: absolute; border: solid 5px; border-top-color: transparent!important; border-right-color: transparent!important; border-left-color: transparent!important; top: -5px; }", "") }), define("ace/ace", ["require", "exports", "module", "ace/lib/fixoldbrowsers", "ace/lib/dom", "ace/lib/event", "ace/range", "ace/editor", "ace/edit_session", "ace/undomanager", "ace/virtual_renderer", "ace/worker/worker_client", "ace/keyboard/hash_handler", "ace/placeholder", "ace/multi_select", "ace/mode/folding/fold_mode", "ace/theme/textmate", "ace/ext/error_marker", "ace/config"], function (e, t, n) { "use strict"; e("./lib/fixoldbrowsers"); var r = e("./lib/dom"), i = e("./lib/event"), s = e("./range").Range, o = e("./editor").Editor, u = e("./edit_session").EditSession, a = e("./undomanager").UndoManager, f = e("./virtual_renderer").VirtualRenderer; e("./worker/worker_client"), e("./keyboard/hash_handler"), e("./placeholder"), e("./multi_select"), e("./mode/folding/fold_mode"), e("./theme/textmate"), e("./ext/error_marker"), t.config = e("./config"), t.require = e, typeof define == "function" && (t.define = define), t.edit = function (e, n) { if (typeof e == "string") { var s = e; e = document.getElementById(s); if (!e) throw new Error("ace.edit can't find div #" + s) } if (e && e.env && e.env.editor instanceof o) return e.env.editor; var u = ""; if (e && /input|textarea/i.test(e.tagName)) { var a = e; u = a.value, e = r.createElement("pre"), a.parentNode.replaceChild(e, a) } else e && (u = e.textContent, e.innerHTML = ""); var l = t.createEditSession(u), c = new o(new f(e), l, n), h = { document: l, editor: c, onResize: c.resize.bind(c, null) }; return a && (h.textarea = a), i.addListener(window, "resize", h.onResize), c.on("destroy", function () { i.removeListener(window, "resize", h.onResize), h.editor.container.env = null }), c.container.env = c.env = h, c }, t.createEditSession = function (e, t) { var n = new u(e, t); return n.setUndoManager(new a), n }, t.Range = s, t.Editor = o, t.EditSession = u, t.UndoManager = a, t.VirtualRenderer = f, t.version = t.config.version }); (function () { - window.require(["ace/ace"], function (a) { - if (a) { - a.config.init(true); - a.define = window.define; - } - if (!window.ace) - window.ace = a; - for (var key in a) if (a.hasOwnProperty(key)) - window.ace[key] = a[key]; - window.ace["default"] = window.ace; - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = window.ace; - } - }); -})(); diff --git a/esphome/dashboard/static/js/vendor/ace/ext-searchbox.js b/esphome/dashboard/static/js/vendor/ace/ext-searchbox.js deleted file mode 100644 index cf28c3b012..0000000000 --- a/esphome/dashboard/static/js/vendor/ace/ext-searchbox.js +++ /dev/null @@ -1,7 +0,0 @@ -define("ace/ext/searchbox", ["require", "exports", "module", "ace/lib/dom", "ace/lib/lang", "ace/lib/event", "ace/keyboard/hash_handler", "ace/lib/keys"], function (e, t, n) { "use strict"; var r = e("../lib/dom"), i = e("../lib/lang"), s = e("../lib/event"), o = '.ace_search {background-color: #ddd;color: #666;border: 1px solid #cbcbcb;border-top: 0 none;overflow: hidden;margin: 0;padding: 4px 6px 0 4px;position: absolute;top: 0;z-index: 99;white-space: normal;}.ace_search.left {border-left: 0 none;border-radius: 0px 0px 5px 0px;left: 0;}.ace_search.right {border-radius: 0px 0px 0px 5px;border-right: 0 none;right: 0;}.ace_search_form, .ace_replace_form {margin: 0 20px 4px 0;overflow: hidden;line-height: 1.9;}.ace_replace_form {margin-right: 0;}.ace_search_form.ace_nomatch {outline: 1px solid red;}.ace_search_field {border-radius: 3px 0 0 3px;background-color: white;color: black;border: 1px solid #cbcbcb;border-right: 0 none;outline: 0;padding: 0;font-size: inherit;margin: 0;line-height: inherit;padding: 0 6px;min-width: 17em;vertical-align: top;min-height: 1.8em;box-sizing: content-box;}.ace_searchbtn {border: 1px solid #cbcbcb;line-height: inherit;display: inline-block;padding: 0 6px;background: #fff;border-right: 0 none;border-left: 1px solid #dcdcdc;cursor: pointer;margin: 0;position: relative;color: #666;}.ace_searchbtn:last-child {border-radius: 0 3px 3px 0;border-right: 1px solid #cbcbcb;}.ace_searchbtn:disabled {background: none;cursor: default;}.ace_searchbtn:hover {background-color: #eef1f6;}.ace_searchbtn.prev, .ace_searchbtn.next {padding: 0px 0.7em}.ace_searchbtn.prev:after, .ace_searchbtn.next:after {content: "";border: solid 2px #888;width: 0.5em;height: 0.5em;border-width: 2px 0 0 2px;display:inline-block;transform: rotate(-45deg);}.ace_searchbtn.next:after {border-width: 0 2px 2px 0 ;}.ace_searchbtn_close {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAAZ0lEQVR42u2SUQrAMAhDvazn8OjZBilCkYVVxiis8H4CT0VrAJb4WHT3C5xU2a2IQZXJjiQIRMdkEoJ5Q2yMqpfDIo+XY4k6h+YXOyKqTIj5REaxloNAd0xiKmAtsTHqW8sR2W5f7gCu5nWFUpVjZwAAAABJRU5ErkJggg==) no-repeat 50% 0;border-radius: 50%;border: 0 none;color: #656565;cursor: pointer;font: 16px/16px Arial;padding: 0;height: 14px;width: 14px;top: 9px;right: 7px;position: absolute;}.ace_searchbtn_close:hover {background-color: #656565;background-position: 50% 100%;color: white;}.ace_button {margin-left: 2px;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;-ms-user-select: none;user-select: none;overflow: hidden;opacity: 0.7;border: 1px solid rgba(100,100,100,0.23);padding: 1px;box-sizing: border-box!important;color: black;}.ace_button:hover {background-color: #eee;opacity:1;}.ace_button:active {background-color: #ddd;}.ace_button.checked {border-color: #3399ff;opacity:1;}.ace_search_options{margin-bottom: 3px;text-align: right;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;-ms-user-select: none;user-select: none;clear: both;}.ace_search_counter {float: left;font-family: arial;padding: 0 8px;}', u = e("../keyboard/hash_handler").HashHandler, a = e("../lib/keys"), f = 999; r.importCssString(o, "ace_searchbox"); var l = function (e, t, n) { var i = r.createElement("div"); r.buildDom(["div", { "class": "ace_search right" }, ["span", { action: "hide", "class": "ace_searchbtn_close" }], ["div", { "class": "ace_search_form" }, ["input", { "class": "ace_search_field", placeholder: "Search for", spellcheck: "false" }], ["span", { action: "findPrev", "class": "ace_searchbtn prev" }, "\u200b"], ["span", { action: "findNext", "class": "ace_searchbtn next" }, "\u200b"], ["span", { action: "findAll", "class": "ace_searchbtn", title: "Alt-Enter" }, "All"]], ["div", { "class": "ace_replace_form" }, ["input", { "class": "ace_search_field", placeholder: "Replace with", spellcheck: "false" }], ["span", { action: "replaceAndFindNext", "class": "ace_searchbtn" }, "Replace"], ["span", { action: "replaceAll", "class": "ace_searchbtn" }, "All"]], ["div", { "class": "ace_search_options" }, ["span", { action: "toggleReplace", "class": "ace_button", title: "Toggle Replace mode", style: "float:left;margin-top:-2px;padding:0 5px;" }, "+"], ["span", { "class": "ace_search_counter" }], ["span", { action: "toggleRegexpMode", "class": "ace_button", title: "RegExp Search" }, ".*"], ["span", { action: "toggleCaseSensitive", "class": "ace_button", title: "CaseSensitive Search" }, "Aa"], ["span", { action: "toggleWholeWords", "class": "ace_button", title: "Whole Word Search" }, "\\b"], ["span", { action: "searchInSelection", "class": "ace_button", title: "Search In Selection" }, "S"]]], i), this.element = i.firstChild, this.setSession = this.setSession.bind(this), this.$init(), this.setEditor(e), r.importCssString(o, "ace_searchbox", e.container) }; (function () { this.setEditor = function (e) { e.searchBox = this, e.renderer.scroller.appendChild(this.element), this.editor = e }, this.setSession = function (e) { this.searchRange = null, this.$syncOptions(!0) }, this.$initElements = function (e) { this.searchBox = e.querySelector(".ace_search_form"), this.replaceBox = e.querySelector(".ace_replace_form"), this.searchOption = e.querySelector("[action=searchInSelection]"), this.replaceOption = e.querySelector("[action=toggleReplace]"), this.regExpOption = e.querySelector("[action=toggleRegexpMode]"), this.caseSensitiveOption = e.querySelector("[action=toggleCaseSensitive]"), this.wholeWordOption = e.querySelector("[action=toggleWholeWords]"), this.searchInput = this.searchBox.querySelector(".ace_search_field"), this.replaceInput = this.replaceBox.querySelector(".ace_search_field"), this.searchCounter = e.querySelector(".ace_search_counter") }, this.$init = function () { var e = this.element; this.$initElements(e); var t = this; s.addListener(e, "mousedown", function (e) { setTimeout(function () { t.activeInput.focus() }, 0), s.stopPropagation(e) }), s.addListener(e, "click", function (e) { var n = e.target || e.srcElement, r = n.getAttribute("action"); r && t[r] ? t[r]() : t.$searchBarKb.commands[r] && t.$searchBarKb.commands[r].exec(t), s.stopPropagation(e) }), s.addCommandKeyListener(e, function (e, n, r) { var i = a.keyCodeToString(r), o = t.$searchBarKb.findKeyCommand(n, i); o && o.exec && (o.exec(t), s.stopEvent(e)) }), this.$onChange = i.delayedCall(function () { t.find(!1, !1) }), s.addListener(this.searchInput, "input", function () { t.$onChange.schedule(20) }), s.addListener(this.searchInput, "focus", function () { t.activeInput = t.searchInput, t.searchInput.value && t.highlight() }), s.addListener(this.replaceInput, "focus", function () { t.activeInput = t.replaceInput, t.searchInput.value && t.highlight() }) }, this.$closeSearchBarKb = new u([{ bindKey: "Esc", name: "closeSearchBar", exec: function (e) { e.searchBox.hide() } }]), this.$searchBarKb = new u, this.$searchBarKb.bindKeys({ "Ctrl-f|Command-f": function (e) { var t = e.isReplace = !e.isReplace; e.replaceBox.style.display = t ? "" : "none", e.replaceOption.checked = !1, e.$syncOptions(), e.searchInput.focus() }, "Ctrl-H|Command-Option-F": function (e) { if (e.editor.getReadOnly()) return; e.replaceOption.checked = !0, e.$syncOptions(), e.replaceInput.focus() }, "Ctrl-G|Command-G": function (e) { e.findNext() }, "Ctrl-Shift-G|Command-Shift-G": function (e) { e.findPrev() }, esc: function (e) { setTimeout(function () { e.hide() }) }, Return: function (e) { e.activeInput == e.replaceInput && e.replace(), e.findNext() }, "Shift-Return": function (e) { e.activeInput == e.replaceInput && e.replace(), e.findPrev() }, "Alt-Return": function (e) { e.activeInput == e.replaceInput && e.replaceAll(), e.findAll() }, Tab: function (e) { (e.activeInput == e.replaceInput ? e.searchInput : e.replaceInput).focus() } }), this.$searchBarKb.addCommands([{ name: "toggleRegexpMode", bindKey: { win: "Alt-R|Alt-/", mac: "Ctrl-Alt-R|Ctrl-Alt-/" }, exec: function (e) { e.regExpOption.checked = !e.regExpOption.checked, e.$syncOptions() } }, { name: "toggleCaseSensitive", bindKey: { win: "Alt-C|Alt-I", mac: "Ctrl-Alt-R|Ctrl-Alt-I" }, exec: function (e) { e.caseSensitiveOption.checked = !e.caseSensitiveOption.checked, e.$syncOptions() } }, { name: "toggleWholeWords", bindKey: { win: "Alt-B|Alt-W", mac: "Ctrl-Alt-B|Ctrl-Alt-W" }, exec: function (e) { e.wholeWordOption.checked = !e.wholeWordOption.checked, e.$syncOptions() } }, { name: "toggleReplace", exec: function (e) { e.replaceOption.checked = !e.replaceOption.checked, e.$syncOptions() } }, { name: "searchInSelection", exec: function (e) { e.searchOption.checked = !e.searchRange, e.setSearchRange(e.searchOption.checked && e.editor.getSelectionRange()), e.$syncOptions() } }]), this.setSearchRange = function (e) { this.searchRange = e, e ? this.searchRangeMarker = this.editor.session.addMarker(e, "ace_active-line") : this.searchRangeMarker && (this.editor.session.removeMarker(this.searchRangeMarker), this.searchRangeMarker = null) }, this.$syncOptions = function (e) { r.setCssClass(this.replaceOption, "checked", this.searchRange), r.setCssClass(this.searchOption, "checked", this.searchOption.checked), this.replaceOption.textContent = this.replaceOption.checked ? "-" : "+", r.setCssClass(this.regExpOption, "checked", this.regExpOption.checked), r.setCssClass(this.wholeWordOption, "checked", this.wholeWordOption.checked), r.setCssClass(this.caseSensitiveOption, "checked", this.caseSensitiveOption.checked); var t = this.editor.getReadOnly(); this.replaceOption.style.display = t ? "none" : "", this.replaceBox.style.display = this.replaceOption.checked && !t ? "" : "none", this.find(!1, !1, e) }, this.highlight = function (e) { this.editor.session.highlight(e || this.editor.$search.$options.re), this.editor.renderer.updateBackMarkers() }, this.find = function (e, t, n) { var i = this.editor.find(this.searchInput.value, { skipCurrent: e, backwards: t, wrap: !0, regExp: this.regExpOption.checked, caseSensitive: this.caseSensitiveOption.checked, wholeWord: this.wholeWordOption.checked, preventScroll: n, range: this.searchRange }), s = !i && this.searchInput.value; r.setCssClass(this.searchBox, "ace_nomatch", s), this.editor._emit("findSearchBox", { match: !s }), this.highlight(), this.updateCounter() }, this.updateCounter = function () { var e = this.editor, t = e.$search.$options.re, n = 0, r = 0; if (t) { var i = this.searchRange ? e.session.getTextRange(this.searchRange) : e.getValue(), s = e.session.doc.positionToIndex(e.selection.anchor); this.searchRange && (s -= e.session.doc.positionToIndex(this.searchRange.start)); var o = t.lastIndex = 0, u; while (u = t.exec(i)) { n++, o = u.index, o <= s && r++; if (n > f) break; if (!u[0]) { t.lastIndex = o += 1; if (o >= i.length) break } } } this.searchCounter.textContent = r + " of " + (n > f ? f + "+" : n) }, this.findNext = function () { this.find(!0, !1) }, this.findPrev = function () { this.find(!0, !0) }, this.findAll = function () { var e = this.editor.findAll(this.searchInput.value, { regExp: this.regExpOption.checked, caseSensitive: this.caseSensitiveOption.checked, wholeWord: this.wholeWordOption.checked }), t = !e && this.searchInput.value; r.setCssClass(this.searchBox, "ace_nomatch", t), this.editor._emit("findSearchBox", { match: !t }), this.highlight(), this.hide() }, this.replace = function () { this.editor.getReadOnly() || this.editor.replace(this.replaceInput.value) }, this.replaceAndFindNext = function () { this.editor.getReadOnly() || (this.editor.replace(this.replaceInput.value), this.findNext()) }, this.replaceAll = function () { this.editor.getReadOnly() || this.editor.replaceAll(this.replaceInput.value) }, this.hide = function () { this.active = !1, this.setSearchRange(null), this.editor.off("changeSession", this.setSession), this.element.style.display = "none", this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb), this.editor.focus() }, this.show = function (e, t) { this.active = !0, this.editor.on("changeSession", this.setSession), this.element.style.display = "", this.replaceOption.checked = t, e && (this.searchInput.value = e), this.searchInput.focus(), this.searchInput.select(), this.editor.keyBinding.addKeyboardHandler(this.$closeSearchBarKb), this.$syncOptions(!0) }, this.isFocused = function () { var e = document.activeElement; return e == this.searchInput || e == this.replaceInput } }).call(l.prototype), t.SearchBox = l, t.Search = function (e, t) { var n = e.searchBox || new l(e); n.show(e.session.getTextRange(), t) } }); (function () { - window.require(["ace/ext/searchbox"], function (m) { - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = m; - } - }); -})(); diff --git a/esphome/dashboard/static/js/vendor/ace/mode-yaml.js b/esphome/dashboard/static/js/vendor/ace/mode-yaml.js deleted file mode 100644 index 81cf343aa5..0000000000 --- a/esphome/dashboard/static/js/vendor/ace/mode-yaml.js +++ /dev/null @@ -1,7 +0,0 @@ -define("ace/mode/yaml_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function (e, t, n) { "use strict"; var r = e("../lib/oop"), i = e("./text_highlight_rules").TextHighlightRules, s = function () { this.$rules = { start: [{ token: "comment", regex: "#.*$" }, { token: "list.markup", regex: /^(?:-{3}|\.{3})\s*(?=#|$)/ }, { token: "list.markup", regex: /^\s*[\-?](?:$|\s)/ }, { token: "constant", regex: "!![\\w//]+" }, { token: "constant.language", regex: "[&\\*][a-zA-Z0-9-_]+" }, { token: ["meta.tag", "keyword"], regex: /^(\s*\w.*?)(:(?=\s|$))/ }, { token: ["meta.tag", "keyword"], regex: /(\w+?)(\s*:(?=\s|$))/ }, { token: "keyword.operator", regex: "<<\\w*:\\w*" }, { token: "keyword.operator", regex: "-\\s*(?=[{])" }, { token: "string", regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' }, { token: "string", regex: /[|>][-+\d]*(?:$|\s+(?:$|#))/, onMatch: function (e, t, n, r) { r = r.replace(/ #.*/, ""); var i = /^ *((:\s*)?-(\s*[^|>])?)?/.exec(r)[0].replace(/\S\s*$/, "").length, s = parseInt(/\d+[\s+-]*$/.exec(r)); return s ? (i += s - 1, this.next = "mlString") : this.next = "mlStringPre", n.length ? (n[0] = this.next, n[1] = i) : (n.push(this.next), n.push(i)), this.token }, next: "mlString" }, { token: "string", regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']" }, { token: "constant.numeric", regex: /(\b|[+\-\.])[\d_]+(?:(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)(?=[^\d-\w]|$)/ }, { token: "constant.numeric", regex: /[+\-]?\.inf\b|NaN\b|0x[\dA-Fa-f_]+|0b[10_]+/ }, { token: "constant.language.boolean", regex: "\\b(?:true|false|TRUE|FALSE|True|False|yes|no)\\b" }, { token: "paren.lparen", regex: "[[({]" }, { token: "paren.rparen", regex: "[\\])}]" }, { token: "text", regex: /[^\s,:\[\]\{\}]+/ }], mlStringPre: [{ token: "indent", regex: /^ *$/ }, { token: "indent", regex: /^ */, onMatch: function (e, t, n) { var r = n[1]; return r >= e.length ? (this.next = "start", n.shift(), n.shift()) : (n[1] = e.length - 1, this.next = n[0] = "mlString"), this.token }, next: "mlString" }, { defaultToken: "string" }], mlString: [{ token: "indent", regex: /^ *$/ }, { token: "indent", regex: /^ */, onMatch: function (e, t, n) { var r = n[1]; return r >= e.length ? (this.next = "start", n.splice(0)) : this.next = "mlString", this.token }, next: "mlString" }, { token: "string", regex: ".+" }] }, this.normalizeRules() }; r.inherits(s, i), t.YamlHighlightRules = s }), define("ace/mode/matching_brace_outdent", ["require", "exports", "module", "ace/range"], function (e, t, n) { "use strict"; var r = e("../range").Range, i = function () { }; (function () { this.checkOutdent = function (e, t) { return /^\s+$/.test(e) ? /^\s*\}/.test(t) : !1 }, this.autoOutdent = function (e, t) { var n = e.getLine(t), i = n.match(/^(\s*\})/); if (!i) return 0; var s = i[1].length, o = e.findMatchingBracket({ row: t, column: s }); if (!o || o.row == t) return 0; var u = this.$getIndent(e.getLine(o.row)); e.replace(new r(t, 0, t, s - 1), u) }, this.$getIndent = function (e) { return e.match(/^\s*/)[0] } }).call(i.prototype), t.MatchingBraceOutdent = i }), define("ace/mode/folding/coffee", ["require", "exports", "module", "ace/lib/oop", "ace/mode/folding/fold_mode", "ace/range"], function (e, t, n) { "use strict"; var r = e("../../lib/oop"), i = e("./fold_mode").FoldMode, s = e("../../range").Range, o = t.FoldMode = function () { }; r.inherits(o, i), function () { this.getFoldWidgetRange = function (e, t, n) { var r = this.indentationBlock(e, n); if (r) return r; var i = /\S/, o = e.getLine(n), u = o.search(i); if (u == -1 || o[u] != "#") return; var a = o.length, f = e.getLength(), l = n, c = n; while (++n < f) { o = e.getLine(n); var h = o.search(i); if (h == -1) continue; if (o[h] != "#") break; c = n } if (c > l) { var p = e.getLine(c).length; return new s(l, a, c, p) } }, this.getFoldWidget = function (e, t, n) { var r = e.getLine(n), i = r.search(/\S/), s = e.getLine(n + 1), o = e.getLine(n - 1), u = o.search(/\S/), a = s.search(/\S/); if (i == -1) return e.foldWidgets[n - 1] = u != -1 && u < a ? "start" : "", ""; if (u == -1) { if (i == a && r[i] == "#" && s[i] == "#") return e.foldWidgets[n - 1] = "", e.foldWidgets[n + 1] = "", "start" } else if (u == i && r[i] == "#" && o[i] == "#" && e.getLine(n - 2).search(/\S/) == -1) return e.foldWidgets[n - 1] = "start", e.foldWidgets[n + 1] = "", ""; return u != -1 && u < i ? e.foldWidgets[n - 1] = "start" : e.foldWidgets[n - 1] = "", i < a ? "start" : "" } }.call(o.prototype) }), define("ace/mode/yaml", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text", "ace/mode/yaml_highlight_rules", "ace/mode/matching_brace_outdent", "ace/mode/folding/coffee"], function (e, t, n) { "use strict"; var r = e("../lib/oop"), i = e("./text").Mode, s = e("./yaml_highlight_rules").YamlHighlightRules, o = e("./matching_brace_outdent").MatchingBraceOutdent, u = e("./folding/coffee").FoldMode, a = function () { this.HighlightRules = s, this.$outdent = new o, this.foldingRules = new u, this.$behaviour = this.$defaultBehaviour }; r.inherits(a, i), function () { this.lineCommentStart = ["#"], this.getNextLineIndent = function (e, t, n) { var r = this.$getIndent(t); if (e == "start") { var i = t.match(/^.*[\{\(\[]\s*$/); i && (r += n) } return r }, this.checkOutdent = function (e, t, n) { return this.$outdent.checkOutdent(t, n) }, this.autoOutdent = function (e, t, n) { this.$outdent.autoOutdent(t, n) }, this.$id = "ace/mode/yaml" }.call(a.prototype), t.Mode = a }); (function () { - window.require(["ace/mode/yaml"], function (m) { - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = m; - } - }); -})(); diff --git a/esphome/dashboard/static/js/vendor/ace/theme-dreamweaver.js b/esphome/dashboard/static/js/vendor/ace/theme-dreamweaver.js deleted file mode 100644 index 2335f6d773..0000000000 --- a/esphome/dashboard/static/js/vendor/ace/theme-dreamweaver.js +++ /dev/null @@ -1,7 +0,0 @@ -define("ace/theme/dreamweaver", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { t.isDark = !1, t.cssClass = "ace-dreamweaver", t.cssText = '.ace-dreamweaver .ace_gutter {background: #e8e8e8;color: #333;}.ace-dreamweaver .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-dreamweaver {background-color: #FFFFFF;color: black;}.ace-dreamweaver .ace_fold {background-color: #757AD8;}.ace-dreamweaver .ace_cursor {color: black;}.ace-dreamweaver .ace_invisible {color: rgb(191, 191, 191);}.ace-dreamweaver .ace_storage,.ace-dreamweaver .ace_keyword {color: blue;}.ace-dreamweaver .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-dreamweaver .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-dreamweaver .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-dreamweaver .ace_invalid {background-color: rgb(153, 0, 0);color: white;}.ace-dreamweaver .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-dreamweaver .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-dreamweaver .ace_support.ace_type,.ace-dreamweaver .ace_support.ace_class {color: #009;}.ace-dreamweaver .ace_support.ace_php_tag {color: #f00;}.ace-dreamweaver .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-dreamweaver .ace_string {color: #00F;}.ace-dreamweaver .ace_comment {color: rgb(76, 136, 107);}.ace-dreamweaver .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-dreamweaver .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-dreamweaver .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-dreamweaver .ace_variable {color: #06F}.ace-dreamweaver .ace_xml-pe {color: rgb(104, 104, 91);}.ace-dreamweaver .ace_entity.ace_name.ace_function {color: #00F;}.ace-dreamweaver .ace_heading {color: rgb(12, 7, 255);}.ace-dreamweaver .ace_list {color:rgb(185, 6, 144);}.ace-dreamweaver .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-dreamweaver .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-dreamweaver .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-dreamweaver .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-dreamweaver .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-dreamweaver .ace_gutter-active-line {background-color : #DCDCDC;}.ace-dreamweaver .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-dreamweaver .ace_meta.ace_tag {color:#009;}.ace-dreamweaver .ace_meta.ace_tag.ace_anchor {color:#060;}.ace-dreamweaver .ace_meta.ace_tag.ace_form {color:#F90;}.ace-dreamweaver .ace_meta.ace_tag.ace_image {color:#909;}.ace-dreamweaver .ace_meta.ace_tag.ace_script {color:#900;}.ace-dreamweaver .ace_meta.ace_tag.ace_style {color:#909;}.ace-dreamweaver .ace_meta.ace_tag.ace_table {color:#099;}.ace-dreamweaver .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-dreamweaver .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}'; var r = e("../lib/dom"); r.importCssString(t.cssText, t.cssClass) }); (function () { - window.require(["ace/theme/dreamweaver"], function (m) { - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = m; - } - }); -})(); diff --git a/esphome/dashboard/static/js/vendor/jquery-ui/jquery-ui.min.js b/esphome/dashboard/static/js/vendor/jquery-ui/jquery-ui.min.js deleted file mode 100644 index 862a649869..0000000000 --- a/esphome/dashboard/static/js/vendor/jquery-ui/jquery-ui.min.js +++ /dev/null @@ -1,13 +0,0 @@ -/*! jQuery UI - v1.12.1 - 2016-09-14 -* http://jqueryui.com -* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js -* Copyright jQuery Foundation and other contributors; Licensed MIT */ - -(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function e(t){for(var e=t.css("visibility");"inherit"===e;)t=t.parent(),e=t.css("visibility");return"hidden"!==e}function i(t){for(var e,i;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(i=parseInt(t.css("zIndex"),10),!isNaN(i)&&0!==i))return i;t=t.parent()}return 0}function s(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=n(t("
"))}function n(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,o)}function o(){t.datepicker._isDisabledDatepicker(m.inline?m.dpDiv.parent()[0]:m.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function a(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}function r(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.ui=t.ui||{},t.ui.version="1.12.1";var h=0,l=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},h=e.split(".")[0];e=e.split(".")[1];var l=h+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][l.toLowerCase()]=function(e){return!!t.data(e,l)},t[h]=t[h]||{},n=t[h][e],o=t[h][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:h,widgetName:e,widgetFullName:l}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=l.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=l.call(arguments,1),r=this;return o?this.length||"instance"!==n?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(r=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(r=i&&i.jquery?r.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):r=void 0:(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),r}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=h++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+o.eventNamespace,c=h[2];c?n.on(l,c,r):i.on(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(u.test(t[0])?e/100:1),parseFloat(t[1])*(u.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o=Math.max,a=Math.abs,r=/left|center|right/,h=/top|center|bottom/,l=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,u=/%$/,d=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
"),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widthi?"left":e>0?"right":"center",vertical:0>r?"top":s>0?"bottom":"middle"};l>p&&p>a(e+i)&&(u.horizontal="center"),c>f&&f>a(s+r)&&(u.vertical="middle"),u.important=o(a(e),a(i))>o(a(s),a(r))?"horizontal":"vertical",n.using.call(this,t,u)}),h.offset(t.extend(D,{using:r}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,r=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-r-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-r-o,(0>i||a(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>a(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,r=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-r-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-r-o,(0>s||a(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-h,(i>0||u>a(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}});var c="ui-effects-",u="ui-effects-style",d="ui-effects-animated",p=t;t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,o){var a,r=o.re.exec(i),h=r&&o.parse(r),l=o.space||"rgba";return h?(a=s[l](h),s[c[l].cache]=a[c[l].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("

")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,a,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,h],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),o=c[n],a=0===this.alpha()?l("transparent"):this,r=a[o.cache]||o.to(a._rgba),h=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],l=s[o],c=u[n.type]||{};null!==l&&(null===a?h[o]=l:(c.mod&&(l-a>c.mod/2?a+=c.mod:a-l>c.mod/2&&(a-=c.mod)),h[o]=i((l-a)*e+a,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),h=Math.min(s,n,o),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-o)/l+360:n===r?60*(o-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[a]&&(this[a]=h(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[a]=d,n):l(d)},f(o,function(e,i){l.fn[e]||(l.fn[e]=function(n){var o,a=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=l(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(p),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,o,a={};for(s in i)o=i[s],e[s]!==o&&(n[s]||(t.fx.step[s]||!isNaN(parseFloat(o)))&&(a[s]=o));return a}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(p.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var h=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",l=h.children?a.find("*").addBack():a;l=l.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),l=l.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function e(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function i(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function s(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(d)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,e){for(var i=0,s=e.length;s>i;i++)null!==e[i]&&t.data(c+e[i],t[0].style[e[i]])},restore:function(t,e){for(var i,s=0,n=e.length;n>s;s++)null!==e[s]&&(i=t.data(c+e[s]),t.css(e[s],i))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(u,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(u)||"",t.removeData(u)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(e){var i,s=e.css("position"),n=e.position();return e.css({marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()),/^(static|relative)/.test(s)&&(s="absolute",i=t("<"+e[0].nodeName+">").insertAfter(e).css({display:/^(inline|ruby)/.test(e.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight"),"float":e.css("float")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).addClass("ui-effects-placeholder"),e.data(c+"placeholder",i)),e.css({position:s,left:n.left,top:n.top}),i},removePlaceholder:function(t){var e=c+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function i(e){function i(){r.removeData(d),t.effects.cleanUp(r),"hide"===s.mode&&r.hide(),a()}function a(){t.isFunction(h)&&h.call(r[0]),t.isFunction(e)&&e()}var r=t(this);s.mode=c.shift(),t.uiBackCompat===!1||o?"none"===s.mode?(r[l](),a()):n.call(r[0],s,i):(r.is(":hidden")?"hide"===l:"show"===l)?(r[l](),a()):n.call(r[0],s,a)}var s=e.apply(this,arguments),n=t.effects.effect[s.effect],o=n.mode,a=s.queue,r=a||"fx",h=s.complete,l=s.mode,c=[],u=function(e){var i=t(this),s=t.effects.mode(i,l)||o;i.data(d,!0),c.push(s),o&&("show"===s||s===o&&"hide"===s)&&i.show(),o&&"none"===s||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!n?l?this[l](s.duration,h):this.each(function(){h&&h.call(this)}):a===!1?this.each(u).each(i):this.queue(r,u).queue(r,i)},show:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="show",this.effect.call(this,n) -}}(t.fn.show),hide:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(t.fn.hide),toggle:function(t){return function(s){if(i(s)||"boolean"==typeof s)return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):s(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,h=o?a.scrollLeft():0,l=n.offset(),c={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-h,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=s(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}();var f=t.effects;t.effects.define("blind","hide",function(e,i){var s={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},n=t(this),o=e.direction||"up",a=n.cssClip(),r={clip:t.extend({},a)},h=t.effects.createPlaceholder(n);r.clip[s[o][0]]=r.clip[s[o][1]],"show"===e.mode&&(n.cssClip(r.clip),h&&h.css(t.effects.clipToBox(r)),r.clip=a),h&&h.animate(t.effects.clipToBox(r),e.duration,e.easing),n.animate(r,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("bounce",function(e,i){var s,n,o,a=t(this),r=e.mode,h="hide"===r,l="show"===r,c=e.direction||"up",u=e.distance,d=e.times||5,p=2*d+(l||h?1:0),f=e.duration/p,g=e.easing,m="up"===c||"down"===c?"top":"left",_="up"===c||"left"===c,v=0,b=a.queue().length;for(t.effects.createPlaceholder(a),o=a.css(m),u||(u=a["top"===m?"outerHeight":"outerWidth"]()/3),l&&(n={opacity:1},n[m]=o,a.css("opacity",0).css(m,_?2*-u:2*u).animate(n,f,g)),h&&(u/=Math.pow(2,d-1)),n={},n[m]=o;d>v;v++)s={},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g).animate(n,f,g),u=h?2*u:u/2;h&&(s={opacity:0},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g)),a.queue(i),t.effects.unshift(a,b,p+1)}),t.effects.define("clip","hide",function(e,i){var s,n={},o=t(this),a=e.direction||"vertical",r="both"===a,h=r||"horizontal"===a,l=r||"vertical"===a;s=o.cssClip(),n.clip={top:l?(s.bottom-s.top)/2:s.top,right:h?(s.right-s.left)/2:s.right,bottom:l?(s.bottom-s.top)/2:s.bottom,left:h?(s.right-s.left)/2:s.left},t.effects.createPlaceholder(o),"show"===e.mode&&(o.cssClip(n.clip),n.clip=s),o.animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("drop","hide",function(e,i){var s,n=t(this),o=e.mode,a="show"===o,r=e.direction||"left",h="up"===r||"down"===r?"top":"left",l="up"===r||"left"===r?"-=":"+=",c="+="===l?"-=":"+=",u={opacity:0};t.effects.createPlaceholder(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,u[h]=l+s,a&&(n.css(u),u[h]=c+s,u.opacity=1),n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("explode","hide",function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),i()}var o,a,r,h,l,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=e.mode,g="show"===f,m=p.show().css("visibility","hidden").offset(),_=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/u),b=[];for(o=0;u>o;o++)for(h=m.top+o*v,c=o-(u-1)/2,a=0;d>a;a++)r=m.left+a*_,l=a-(d-1)/2,p.clone().appendTo("body").wrap("
").css({position:"absolute",visibility:"visible",left:-a*_,top:-o*v}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:_,height:v,left:r+(g?l*_:0),top:h+(g?c*v:0),opacity:g?0:1}).animate({left:r+(g?0:l*_),top:h+(g?0:c*v),opacity:g?1:0},e.duration||500,e.easing,s)}),t.effects.define("fade","toggle",function(e,i){var s="show"===e.mode;t(this).css("opacity",s?0:1).animate({opacity:s?1:0},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("fold","hide",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=e.size||15,h=/([0-9]+)%/.exec(r),l=!!e.horizFirst,c=l?["right","bottom"]:["bottom","right"],u=e.duration/2,d=t.effects.createPlaceholder(s),p=s.cssClip(),f={clip:t.extend({},p)},g={clip:t.extend({},p)},m=[p[c[0]],p[c[1]]],_=s.queue().length;h&&(r=parseInt(h[1],10)/100*m[a?0:1]),f.clip[c[0]]=r,g.clip[c[0]]=r,g.clip[c[1]]=0,o&&(s.cssClip(g.clip),d&&d.css(t.effects.clipToBox(g)),g.clip=p),s.queue(function(i){d&&d.animate(t.effects.clipToBox(f),u,e.easing).animate(t.effects.clipToBox(g),u,e.easing),i()}).animate(f,u,e.easing).animate(g,u,e.easing).queue(i),t.effects.unshift(s,_,4)}),t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("size",function(e,i){var s,n,o,a=t(this),r=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],l=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],c=e.mode,u="effect"!==c,d=e.scale||"both",p=e.origin||["middle","center"],f=a.css("position"),g=a.position(),m=t.effects.scaledDimensions(a),_=e.from||m,v=e.to||t.effects.scaledDimensions(a,0);t.effects.createPlaceholder(a),"show"===c&&(o=_,_=v,v=o),n={from:{y:_.height/m.height,x:_.width/m.width},to:{y:v.height/m.height,x:v.width/m.width}},("box"===d||"both"===d)&&(n.from.y!==n.to.y&&(_=t.effects.setTransition(a,h,n.from.y,_),v=t.effects.setTransition(a,h,n.to.y,v)),n.from.x!==n.to.x&&(_=t.effects.setTransition(a,l,n.from.x,_),v=t.effects.setTransition(a,l,n.to.x,v))),("content"===d||"both"===d)&&n.from.y!==n.to.y&&(_=t.effects.setTransition(a,r,n.from.y,_),v=t.effects.setTransition(a,r,n.to.y,v)),p&&(s=t.effects.getBaseline(p,m),_.top=(m.outerHeight-_.outerHeight)*s.y+g.top,_.left=(m.outerWidth-_.outerWidth)*s.x+g.left,v.top=(m.outerHeight-v.outerHeight)*s.y+g.top,v.left=(m.outerWidth-v.outerWidth)*s.x+g.left),a.css(_),("content"===d||"both"===d)&&(h=h.concat(["marginTop","marginBottom"]).concat(r),l=l.concat(["marginLeft","marginRight"]),a.find("*[width]").each(function(){var i=t(this),s=t.effects.scaledDimensions(i),o={height:s.height*n.from.y,width:s.width*n.from.x,outerHeight:s.outerHeight*n.from.y,outerWidth:s.outerWidth*n.from.x},a={height:s.height*n.to.y,width:s.width*n.to.x,outerHeight:s.height*n.to.y,outerWidth:s.width*n.to.x};n.from.y!==n.to.y&&(o=t.effects.setTransition(i,h,n.from.y,o),a=t.effects.setTransition(i,h,n.to.y,a)),n.from.x!==n.to.x&&(o=t.effects.setTransition(i,l,n.from.x,o),a=t.effects.setTransition(i,l,n.to.x,a)),u&&t.effects.saveStyle(i),i.css(o),i.animate(a,e.duration,e.easing,function(){u&&t.effects.restoreStyle(i)})})),a.animate(v,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){var e=a.offset();0===v.opacity&&a.css("opacity",_.opacity),u||(a.css("position","static"===f?"relative":f).offset(e),t.effects.saveStyle(a)),i()}})}),t.effects.define("scale",function(e,i){var s=t(this),n=e.mode,o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"effect"!==n?0:100),a=t.extend(!0,{from:t.effects.scaledDimensions(s),to:t.effects.scaledDimensions(s,o,e.direction||"both"),origin:e.origin||["middle","center"]},e);e.fade&&(a.from.opacity=1,a.to.opacity=0),t.effects.effect.size.call(this,a,i)}),t.effects.define("puff","hide",function(e,i){var s=t.extend(!0,{},e,{fade:!0,percent:parseInt(e.percent,10)||150});t.effects.effect.scale.call(this,s,i)}),t.effects.define("pulsate","show",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=o||a,h=2*(e.times||5)+(r?1:0),l=e.duration/h,c=0,u=1,d=s.queue().length;for((o||!s.is(":visible"))&&(s.css("opacity",0).show(),c=1);h>u;u++)s.animate({opacity:c},l,e.easing),c=1-c;s.animate({opacity:c},l,e.easing),s.queue(i),t.effects.unshift(s,d,h+1)}),t.effects.define("shake",function(e,i){var s=1,n=t(this),o=e.direction||"left",a=e.distance||20,r=e.times||3,h=2*r+1,l=Math.round(e.duration/h),c="up"===o||"down"===o?"top":"left",u="up"===o||"left"===o,d={},p={},f={},g=n.queue().length;for(t.effects.createPlaceholder(n),d[c]=(u?"-=":"+=")+a,p[c]=(u?"+=":"-=")+2*a,f[c]=(u?"-=":"+=")+2*a,n.animate(d,l,e.easing);r>s;s++)n.animate(p,l,e.easing).animate(f,l,e.easing);n.animate(p,l,e.easing).animate(d,l/2,e.easing).queue(i),t.effects.unshift(n,g,h+1)}),t.effects.define("slide","show",function(e,i){var s,n,o=t(this),a={up:["bottom","top"],down:["top","bottom"],left:["right","left"],right:["left","right"]},r=e.mode,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h,u=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0),d={};t.effects.createPlaceholder(o),s=o.cssClip(),n=o.position()[l],d[l]=(c?-1:1)*u+n,d.clip=o.cssClip(),d.clip[a[h][1]]=d.clip[a[h][0]],"show"===r&&(o.cssClip(d.clip),o.css(l,d[l]),d.clip=s,d[l]=n),o.animate(d,{queue:!1,duration:e.duration,easing:e.easing,complete:i})});var f;t.uiBackCompat!==!1&&(f=t.effects.define("transfer",function(e,i){t(this).transfer(e,i)})),t.ui.focusable=function(i,s){var n,o,a,r,h,l=i.nodeName.toLowerCase();return"area"===l?(n=i.parentNode,o=n.name,i.href&&o&&"map"===n.nodeName.toLowerCase()?(a=t("img[usemap='#"+o+"']"),a.length>0&&a.is(":visible")):!1):(/^(input|select|textarea|button|object)$/.test(l)?(r=!i.disabled,r&&(h=t(i).closest("fieldset")[0],h&&(r=!h.disabled))):r="a"===l?i.href||s:s,r&&t(i).is(":visible")&&e(t(i)))},t.extend(t.expr[":"],{focusable:function(e){return t.ui.focusable(e,null!=t.attr(e,"tabindex"))}}),t.ui.focusable,t.fn.form=function(){return"string"==typeof this[0].form?this.closest("form"):t(this[0].form)},t.ui.formResetMixin={_formResetHandler:function(){var e=t(this);setTimeout(function(){var i=e.data("ui-form-reset-instances");t.each(i,function(){this.refresh()})})},_bindFormResetHandler:function(){if(this.form=this.element.form(),this.form.length){var t=this.form.data("ui-form-reset-instances")||[];t.length||this.form.on("reset.ui-form-reset",this._formResetHandler),t.push(this),this.form.data("ui-form-reset-instances",t)}},_unbindFormResetHandler:function(){if(this.form.length){var e=this.form.data("ui-form-reset-instances");e.splice(t.inArray(this,e),1),e.length?this.form.data("ui-form-reset-instances",e):this.form.removeData("ui-form-reset-instances").off("reset.ui-form-reset")}}},"1.7"===t.fn.jquery.substring(0,3)&&(t.each(["Width","Height"],function(e,i){function s(e,i,s,o){return t.each(n,function(){i-=parseFloat(t.css(e,"padding"+this))||0,s&&(i-=parseFloat(t.css(e,"border"+this+"Width"))||0),o&&(i-=parseFloat(t.css(e,"margin"+this))||0)}),i}var n="Width"===i?["Left","Right"]:["Top","Bottom"],o=i.toLowerCase(),a={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+i]=function(e){return void 0===e?a["inner"+i].call(this):this.each(function(){t(this).css(o,s(this,e)+"px")})},t.fn["outer"+i]=function(e,n){return"number"!=typeof e?a["outer"+i].call(this,e):this.each(function(){t(this).css(o,s(this,e,!0,n)+"px")})}}),t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.ui.escapeSelector=function(){var t=/([!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g;return function(e){return e.replace(t,"\\$1")}}(),t.fn.labels=function(){var e,i,s,n,o;return this[0].labels&&this[0].labels.length?this.pushStack(this[0].labels):(n=this.eq(0).parents("label"),s=this.attr("id"),s&&(e=this.eq(0).parents().last(),o=e.add(e.length?e.siblings():this.siblings()),i="label[for='"+t.ui.escapeSelector(s)+"']",n=n.add(o.find(i).addBack(i))),this.pushStack(n))},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.extend(t.expr[":"],{tabbable:function(e){var i=t.attr(e,"tabindex"),s=null!=i;return(!s||i>=0)&&t.ui.focusable(e,s)}}),t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.widget("ui.accordion",{version:"1.12.1",options:{active:0,animate:{},classes:{"ui-accordion-header":"ui-corner-top","ui-accordion-header-collapsed":"ui-corner-all","ui-accordion-content":"ui-corner-bottom"},collapsible:!1,event:"click",header:"> li > :first-child, > :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var e=this.options;this.prevShow=this.prevHide=t(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),e.collapsible||e.active!==!1&&null!=e.active||(e.active=0),this._processPanels(),0>e.active&&(e.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():t()}},_createIcons:function(){var e,i,s=this.options.icons;s&&(e=t(""),this._addClass(e,"ui-accordion-header-icon","ui-icon "+s.header),e.prependTo(this.headers),i=this.active.children(".ui-accordion-header-icon"),this._removeClass(i,s.header)._addClass(i,null,s.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){return"active"===t?(this._activate(e),void 0):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||this.options.active!==!1||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons()),void 0)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t),this._toggleClass(this.headers.add(this.headers.next()),null,"ui-state-disabled",!!t)},_keydown:function(e){if(!e.altKey&&!e.ctrlKey){var i=t.ui.keyCode,s=this.headers.length,n=this.headers.index(e.target),o=!1;switch(e.keyCode){case i.RIGHT:case i.DOWN:o=this.headers[(n+1)%s];break;case i.LEFT:case i.UP:o=this.headers[(n-1+s)%s];break;case i.SPACE:case i.ENTER:this._eventHandler(e);break;case i.HOME:o=this.headers[0];break;case i.END:o=this.headers[s-1]}o&&(t(e.target).attr("tabIndex",-1),t(o).attr("tabIndex",0),t(o).trigger("focus"),e.preventDefault())}},_panelKeyDown:function(e){e.keyCode===t.ui.keyCode.UP&&e.ctrlKey&&t(e.currentTarget).prev().trigger("focus")},refresh:function(){var e=this.options;this._processPanels(),e.active===!1&&e.collapsible===!0||!this.headers.length?(e.active=!1,this.active=t()):e.active===!1?this._activate(0):this.active.length&&!t.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(e.active=!1,this.active=t()):this._activate(Math.max(0,e.active-1)):e.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var e,i=this.options,s=i.heightStyle,n=this.element.parent();this.active=this._findActive(i.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var e=t(this),i=e.uniqueId().attr("id"),s=e.next(),n=s.uniqueId().attr("id");e.attr("aria-controls",n),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(i.event),"fill"===s?(e=n.height(),this.element.siblings(":visible").each(function(){var i=t(this),s=i.css("position");"absolute"!==s&&"fixed"!==s&&(e-=i.outerHeight(!0))}),this.headers.each(function(){e-=t(this).outerHeight(!0)}),this.headers.next().each(function(){t(this).height(Math.max(0,e-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===s&&(e=0,this.headers.next().each(function(){var i=t(this).is(":visible");i||t(this).show(),e=Math.max(e,t(this).css("height","").height()),i||t(this).hide()}).height(e))},_activate:function(e){var i=this._findActive(e)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return"number"==typeof e?this.headers.eq(e):t()},_setupEvents:function(e){var i={keydown:"_keydown"};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(e){var i,s,n=this.options,o=this.active,a=t(e.currentTarget),r=a[0]===o[0],h=r&&n.collapsible,l=h?t():a.next(),c=o.next(),u={oldHeader:o,oldPanel:c,newHeader:h?t():a,newPanel:l};e.preventDefault(),r&&!n.collapsible||this._trigger("beforeActivate",e,u)===!1||(n.active=h?!1:this.headers.index(a),this.active=r?t():a,this._toggle(u),this._removeClass(o,"ui-accordion-header-active","ui-state-active"),n.icons&&(i=o.children(".ui-accordion-header-icon"),this._removeClass(i,null,n.icons.activeHeader)._addClass(i,null,n.icons.header)),r||(this._removeClass(a,"ui-accordion-header-collapsed")._addClass(a,"ui-accordion-header-active","ui-state-active"),n.icons&&(s=a.children(".ui-accordion-header-icon"),this._removeClass(s,null,n.icons.header)._addClass(s,null,n.icons.activeHeader)),this._addClass(a.next(),"ui-accordion-content-active")))},_toggle:function(e){var i=e.newPanel,s=this.prevShow.length?this.prevShow:e.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=s,this.options.animate?this._animate(i,s,e):(s.hide(),i.show(),this._toggleComplete(e)),s.attr({"aria-hidden":"true"}),s.prev().attr({"aria-selected":"false","aria-expanded":"false"}),i.length&&s.length?s.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===parseInt(t(this).attr("tabIndex"),10)}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,e,i){var s,n,o,a=this,r=0,h=t.css("box-sizing"),l=t.length&&(!e.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",o=!1,n=e.keyCode>=96&&105>=e.keyCode?""+(e.keyCode-96):String.fromCharCode(e.keyCode),clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,h=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=h.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=h.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n; -this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t("'),this.$slides.each(function(t,e){var i=s('
  • ');n.$indicators.append(i[0])}),this.$el.append(this.$indicators[0]),this.$indicators=this.$indicators.children("li.indicator-item"))}},{key:"_removeIndicators",value:function(){this.$el.find("ul.indicators").remove()}},{key:"set",value:function(t){var e=this;if(t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.activeIndex!=t){this.$active=this.$slides.eq(this.activeIndex);var i=this.$active.find(".caption");this.$active.removeClass("active"),o({targets:this.$active[0],opacity:0,duration:this.options.duration,easing:"easeOutQuad",complete:function(){e.$slides.not(".active").each(function(t){o({targets:t,opacity:0,translateX:0,translateY:0,duration:0,easing:"easeOutQuad"})})}}),this._animateCaptionIn(i[0],this.options.duration),this.options.indicators&&(this.$indicators.eq(this.activeIndex).removeClass("active"),this.$indicators.eq(t).addClass("active")),o({targets:this.$slides.eq(t)[0],opacity:1,duration:this.options.duration,easing:"easeOutQuad"}),o({targets:this.$slides.eq(t).find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:this.options.duration,delay:this.options.duration,easing:"easeOutQuad"}),this.$slides.eq(t).addClass("active"),this.activeIndex=t,this.start()}}},{key:"pause",value:function(){clearInterval(this.interval)}},{key:"start",value:function(){clearInterval(this.interval),this.interval=setInterval(this._handleIntervalBound,this.options.duration+this.options.interval)}},{key:"next",value:function(){var t=this.activeIndex+1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}},{key:"prev",value:function(){var t=this.activeIndex-1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Slider}},{key:"defaults",get:function(){return e}}]),n}();M.Slider=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"slider","M_Slider")}(cash,M.anime),function(n,s){n(document).on("click",".card",function(t){if(n(this).children(".card-reveal").length){var i=n(t.target).closest(".card");void 0===i.data("initialOverflow")&&i.data("initialOverflow",void 0===i.css("overflow")?"":i.css("overflow"));var e=n(this).find(".card-reveal");n(t.target).is(n(".card-reveal .card-title"))||n(t.target).is(n(".card-reveal .card-title i"))?s({targets:e[0],translateY:0,duration:225,easing:"easeInOutQuad",complete:function(t){var e=t.animatables[0].target;n(e).css({display:"none"}),i.css("overflow",i.data("initialOverflow"))}}):(n(t.target).is(n(".card .activator"))||n(t.target).is(n(".card .activator i")))&&(i.css("overflow","hidden"),e.css({display:"block"}),s({targets:e[0],translateY:"-100%",duration:300,easing:"easeInOutQuad"}))}})}(cash,M.anime),function(h){"use strict";var e={data:[],placeholder:"",secondaryPlaceholder:"",autocompleteOptions:{},limit:1/0,onChipAdd:null,onChipSelect:null,onChipDelete:null},t=function(t){function l(t,e){_classCallCheck(this,l);var i=_possibleConstructorReturn(this,(l.__proto__||Object.getPrototypeOf(l)).call(this,l,t,e));return(i.el.M_Chips=i).options=h.extend({},l.defaults,e),i.$el.addClass("chips input-field"),i.chipsData=[],i.$chips=h(),i._setupInput(),i.hasAutocomplete=0"),this.$el.append(this.$input)),this.$input.addClass("input")}},{key:"_setupLabel",value:function(){this.$label=this.$el.find("label"),this.$label.length&&this.$label.setAttribute("for",this.$input.attr("id"))}},{key:"_setPlaceholder",value:function(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?h(this.$input).prop("placeholder",this.options.placeholder):(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&h(this.$input).prop("placeholder",this.options.secondaryPlaceholder)}},{key:"_isValid",value:function(t){if(t.hasOwnProperty("tag")&&""!==t.tag){for(var e=!1,i=0;i=this.options.limit)){var e=this._renderChip(t);this.$chips.add(e),this.chipsData.push(t),h(this.$input).before(e),this._setPlaceholder(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd.call(this,this.$el,e)}}},{key:"deleteChip",value:function(t){var e=this.$chips.eq(t);this.$chips.eq(t).remove(),this.$chips=this.$chips.filter(function(t){return 0<=h(t).index()}),this.chipsData.splice(t,1),this._setPlaceholder(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete.call(this,this.$el,e[0])}},{key:"selectChip",value:function(t){var e=this.$chips.eq(t);(this._selectedChip=e)[0].focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect.call(this,this.$el,e[0])}}],[{key:"init",value:function(t,e){return _get(l.__proto__||Object.getPrototypeOf(l),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Chips}},{key:"_handleChipsKeydown",value:function(t){l._keydown=!0;var e=h(t.target).closest(".chips"),i=t.target&&e.length;if(!h(t.target).is("input, textarea")&&i){var n=e[0].M_Chips;if(8===t.keyCode||46===t.keyCode){t.preventDefault();var s=n.chipsData.length;if(n._selectedChip){var o=n._selectedChip.index();n.deleteChip(o),n._selectedChip=null,s=Math.max(o-1,0)}n.chipsData.length&&n.selectChip(s)}else if(37===t.keyCode){if(n._selectedChip){var a=n._selectedChip.index()-1;if(a<0)return;n.selectChip(a)}}else if(39===t.keyCode&&n._selectedChip){var r=n._selectedChip.index()+1;r>=n.chipsData.length?n.$input[0].focus():n.selectChip(r)}}}},{key:"_handleChipsKeyup",value:function(t){l._keydown=!1}},{key:"_handleChipsBlur",value:function(t){l._keydown||(h(t.target).closest(".chips")[0].M_Chips._selectedChip=null)}},{key:"defaults",get:function(){return e}}]),l}();t._keydown=!1,M.Chips=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"chips","M_Chips"),h(document).ready(function(){h(document.body).on("click",".chip .close",function(){var t=h(this).closest(".chips");t.length&&t[0].M_Chips||h(this).closest(".chip").remove()})})}(cash),function(s){"use strict";var e={top:0,bottom:1/0,offset:0,onPositionChange:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Pushpin=i).options=s.extend({},n.defaults,e),i.originalOffset=i.el.offsetTop,n._pushpins.push(i),i._setupEventHandlers(),i._updatePosition(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.el.style.top=null,this._removePinClasses(),this._removeEventHandlers();var t=n._pushpins.indexOf(this);n._pushpins.splice(t,1)}},{key:"_setupEventHandlers",value:function(){document.addEventListener("scroll",n._updateElements)}},{key:"_removeEventHandlers",value:function(){document.removeEventListener("scroll",n._updateElements)}},{key:"_updatePosition",value:function(){var t=M.getDocumentScrollTop()+this.options.offset;this.options.top<=t&&this.options.bottom>=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=this.options.offset+"px",this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),tthis.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}},{key:"_removePinClasses",value:function(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Pushpin}},{key:"_updateElements",value:function(){for(var t in n._pushpins){n._pushpins[t]._updatePosition()}}},{key:"defaults",get:function(){return e}}]),n}();t._pushpins=[],M.Pushpin=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"pushpin","M_Pushpin")}(cash),function(r,s){"use strict";var e={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};r.fn.reverse=[].reverse;var t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_FloatingActionButton=i).options=r.extend({},n.defaults,e),i.isOpen=!1,i.$anchor=i.$el.children("a").first(),i.$menu=i.$el.children("ul").first(),i.$floatingBtns=i.$el.find("ul .btn-floating"),i.$floatingBtnsReverse=i.$el.find("ul .btn-floating").reverse(),i.offsetY=0,i.offsetX=0,i.$el.addClass("direction-"+i.options.direction),"top"===i.options.direction?i.offsetY=40:"right"===i.options.direction?i.offsetX=-40:"bottom"===i.options.direction?i.offsetY=-40:i.offsetX=40,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_FloatingActionButton=void 0}},{key:"_setupEventHandlers",value:function(){this._handleFABClickBound=this._handleFABClick.bind(this),this._handleOpenBound=this.open.bind(this),this._handleCloseBound=this.close.bind(this),this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this._handleOpenBound),this.el.addEventListener("mouseleave",this._handleCloseBound)):this.el.addEventListener("click",this._handleFABClickBound)}},{key:"_removeEventHandlers",value:function(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this._handleOpenBound),this.el.removeEventListener("mouseleave",this._handleCloseBound)):this.el.removeEventListener("click",this._handleFABClickBound)}},{key:"_handleFABClick",value:function(){this.isOpen?this.close():this.open()}},{key:"_handleDocumentClick",value:function(t){r(t.target).closest(this.$menu).length||this.close()}},{key:"open",value:function(){this.isOpen||(this.options.toolbarEnabled?this._animateInToolbar():this._animateInFAB(),this.isOpen=!0)}},{key:"close",value:function(){this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this._handleCloseBound,!0),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),this._animateOutToolbar()):this._animateOutFAB(),this.isOpen=!1)}},{key:"_animateInFAB",value:function(){var e=this;this.$el.addClass("active");var i=0;this.$floatingBtnsReverse.each(function(t){s({targets:t,opacity:1,scale:[.4,1],translateY:[e.offsetY,0],translateX:[e.offsetX,0],duration:275,delay:i,easing:"easeInOutQuad"}),i+=40})}},{key:"_animateOutFAB",value:function(){var e=this;this.$floatingBtnsReverse.each(function(t){s.remove(t),s({targets:t,opacity:0,scale:.4,translateY:e.offsetY,translateX:e.offsetX,duration:175,easing:"easeOutQuad",complete:function(){e.$el.removeClass("active")}})})}},{key:"_animateInToolbar",value:function(){var t,e=this,i=window.innerWidth,n=window.innerHeight,s=this.el.getBoundingClientRect(),o=r('
    '),a=this.$anchor.css("background-color");this.$anchor.append(o),this.offsetX=s.left-i/2+s.width/2,this.offsetY=n-s.bottom,t=i/o[0].clientWidth,this.btnBottom=s.bottom,this.btnLeft=s.left,this.btnWidth=s.width,this.$el.addClass("active"),this.$el.css({"text-align":"center",width:"100%",bottom:0,left:0,transform:"translateX("+this.offsetX+"px)",transition:"none"}),this.$anchor.css({transform:"translateY("+-this.offsetY+"px)",transition:"none"}),o.css({"background-color":a}),setTimeout(function(){e.$el.css({transform:"",transition:"transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s"}),e.$anchor.css({overflow:"visible",transform:"",transition:"transform .2s"}),setTimeout(function(){e.$el.css({overflow:"hidden","background-color":a}),o.css({transform:"scale("+t+")",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"}),e.$menu.children("li").children("a").css({opacity:1}),e._handleDocumentClickBound=e._handleDocumentClick.bind(e),window.addEventListener("scroll",e._handleCloseBound,!0),document.body.addEventListener("click",e._handleDocumentClickBound,!0)},100)},0)}},{key:"_animateOutToolbar",value:function(){var t=this,e=window.innerWidth,i=window.innerHeight,n=this.$el.find(".fab-backdrop"),s=this.$anchor.css("background-color");this.offsetX=this.btnLeft-e/2+this.btnWidth/2,this.offsetY=i-this.btnBottom,this.$el.removeClass("active"),this.$el.css({"background-color":"transparent",transition:"none"}),this.$anchor.css({transition:"none"}),n.css({transform:"scale(0)","background-color":s}),this.$menu.children("li").children("a").css({opacity:""}),setTimeout(function(){n.remove(),t.$el.css({"text-align":"",width:"",bottom:"",left:"",overflow:"","background-color":"",transform:"translate3d("+-t.offsetX+"px,0,0)"}),t.$anchor.css({overflow:"",transform:"translate3d(0,"+t.offsetY+"px,0)"}),setTimeout(function(){t.$el.css({transform:"translate3d(0,0,0)",transition:"transform .2s"}),t.$anchor.css({transform:"translate3d(0,0,0)",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"})},20)},200)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FloatingActionButton}},{key:"defaults",get:function(){return e}}]),n}();M.FloatingActionButton=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"floatingActionButton","M_FloatingActionButton")}(cash,M.anime),function(g){"use strict";var e={autoClose:!1,format:"mmm dd, yyyy",parse:null,defaultDate:null,setDefaultDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,container:null,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onOpen:null,onClose:null,onDraw:null},t=function(t){function B(t,e){_classCallCheck(this,B);var i=_possibleConstructorReturn(this,(B.__proto__||Object.getPrototypeOf(B)).call(this,B,t,e));(i.el.M_Datepicker=i).options=g.extend({},B.defaults,e),e&&e.hasOwnProperty("i18n")&&"object"==typeof e.i18n&&(i.options.i18n=g.extend({},B.defaults.i18n,e.i18n)),i.options.minDate&&i.options.minDate.setHours(0,0,0,0),i.options.maxDate&&i.options.maxDate.setHours(0,0,0,0),i.id=M.guid(),i._setupVariables(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupEventHandlers(),i.options.defaultDate||(i.options.defaultDate=new Date(Date.parse(i.el.value)));var n=i.options.defaultDate;return B._isDate(n)?i.options.setDefaultDate?(i.setDate(n,!0),i.setInputValue()):i.gotoDate(n):i.gotoDate(new Date),i.isOpen=!1,i}return _inherits(B,Component),_createClass(B,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),g(this.modalEl).remove(),this.destroySelects(),this.el.M_Datepicker=void 0}},{key:"destroySelects",value:function(){var t=this.calendarEl.querySelector(".orig-select-year");t&&M.FormSelect.getInstance(t).destroy();var e=this.calendarEl.querySelector(".orig-select-month");e&&M.FormSelect.getInstance(e).destroy()}},{key:"_insertHTMLIntoDOM",value:function(){this.options.showClearBtn&&(g(this.clearBtn).css({visibility:""}),this.clearBtn.innerHTML=this.options.i18n.clear),this.doneBtn.innerHTML=this.options.i18n.done,this.cancelBtn.innerHTML=this.options.i18n.cancel,this.options.container?this.$modalEl.appendTo(this.options.container):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modalEl.id="modal-"+this.id,this.modal=M.Modal.init(this.modalEl,{onCloseEnd:function(){t.isOpen=!1}})}},{key:"toString",value:function(t){var e=this;return t=t||this.options.format,B._isDate(this.date)?t.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g).map(function(t){return e.formats[t]?e.formats[t]():t}).join(""):""}},{key:"setDate",value:function(t,e){if(!t)return this.date=null,this._renderDateDisplay(),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),B._isDate(t)){var i=this.options.minDate,n=this.options.maxDate;B._isDate(i)&&tn.maxDate||n.disableWeekends&&B._isWeekend(y)||n.disableDayFn&&n.disableDayFn(y),isEmpty:C,isStartRange:x,isEndRange:L,isInRange:T,showDaysInNextAndPreviousMonths:n.showDaysInNextAndPreviousMonths};l.push(this.renderDay($)),7==++_&&(r.push(this.renderRow(l,n.isRTL,m)),_=0,m=!(l=[]))}return this.renderTable(n,r,i)}},{key:"renderDay",value:function(t){var e=[],i="false";if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'';e.push("is-outside-current-month"),e.push("is-selection-disabled")}return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&(e.push("is-selected"),i="true"),t.hasEvent&&e.push("has-event"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"}},{key:"renderRow",value:function(t,e,i){return''+(e?t.reverse():t).join("")+""}},{key:"renderTable",value:function(t,e,i){return'
    '+this.renderHead(t)+this.renderBody(e)+"
    "}},{key:"renderHead",value:function(t){var e=void 0,i=[];for(e=0;e<7;e++)i.push(''+this.renderDayName(t,e,!0)+"");return""+(t.isRTL?i.reverse():i).join("")+""}},{key:"renderBody",value:function(t){return""+t.join("")+""}},{key:"renderTitle",value:function(t,e,i,n,s,o){var a,r,l=void 0,h=void 0,d=void 0,u=this.options,c=i===u.minYear,p=i===u.maxYear,v='
    ',f=!0,m=!0;for(d=[],l=0;l<12;l++)d.push('");for(a='",g.isArray(u.yearRange)?(l=u.yearRange[0],h=u.yearRange[1]+1):(l=i-u.yearRange,h=1+i+u.yearRange),d=[];l=u.minYear&&d.push('");r='";v+='',v+='
    ',u.showMonthAfterYear?v+=r+a:v+=a+r,v+="
    ",c&&(0===n||u.minMonth>=n)&&(f=!1),p&&(11===n||u.maxMonth<=n)&&(m=!1);return(v+='')+"
    "}},{key:"draw",value:function(t){if(this.isOpen||t){var e,i=this.options,n=i.minYear,s=i.maxYear,o=i.minMonth,a=i.maxMonth,r="";this._y<=n&&(this._y=n,!isNaN(o)&&this._m=s&&(this._y=s,!isNaN(a)&&this._m>a&&(this._m=a)),e="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(var l=0;l<1;l++)this._renderDateDisplay(),r+=this.renderTitle(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year,e)+this.render(this.calendars[l].year,this.calendars[l].month,e);this.destroySelects(),this.calendarEl.innerHTML=r;var h=this.calendarEl.querySelector(".orig-select-year"),d=this.calendarEl.querySelector(".orig-select-month");M.FormSelect.init(h,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),M.FormSelect.init(d,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),h.addEventListener("change",this._handleYearChange.bind(this)),d.addEventListener("change",this._handleMonthChange.bind(this)),"function"==typeof this.options.onDraw&&this.options.onDraw(this)}}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleInputChangeBound=this._handleInputChange.bind(this),this._handleCalendarClickBound=this._handleCalendarClick.bind(this),this._finishSelectionBound=this._finishSelection.bind(this),this._handleMonthChange=this._handleMonthChange.bind(this),this._closeBound=this.close.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("change",this._handleInputChangeBound),this.calendarEl.addEventListener("click",this._handleCalendarClickBound),this.doneBtn.addEventListener("click",this._finishSelectionBound),this.cancelBtn.addEventListener("click",this._closeBound),this.options.showClearBtn&&(this._handleClearClickBound=this._handleClearClick.bind(this),this.clearBtn.addEventListener("click",this._handleClearClickBound))}},{key:"_setupVariables",value:function(){var e=this;this.$modalEl=g(B._template),this.modalEl=this.$modalEl[0],this.calendarEl=this.modalEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.modalEl.querySelector(".year-text"),this.dateTextEl=this.modalEl.querySelector(".date-text"),this.options.showClearBtn&&(this.clearBtn=this.modalEl.querySelector(".datepicker-clear")),this.doneBtn=this.modalEl.querySelector(".datepicker-done"),this.cancelBtn=this.modalEl.querySelector(".datepicker-cancel"),this.formats={d:function(){return e.date.getDate()},dd:function(){var t=e.date.getDate();return(t<10?"0":"")+t},ddd:function(){return e.options.i18n.weekdaysShort[e.date.getDay()]},dddd:function(){return e.options.i18n.weekdays[e.date.getDay()]},m:function(){return e.date.getMonth()+1},mm:function(){var t=e.date.getMonth()+1;return(t<10?"0":"")+t},mmm:function(){return e.options.i18n.monthsShort[e.date.getMonth()]},mmmm:function(){return e.options.i18n.months[e.date.getMonth()]},yy:function(){return(""+e.date.getFullYear()).slice(2)},yyyy:function(){return e.date.getFullYear()}}}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("change",this._handleInputChangeBound),this.calendarEl.removeEventListener("click",this._handleCalendarClickBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleCalendarClick",value:function(t){if(this.isOpen){var e=g(t.target);e.hasClass("is-disabled")||(!e.hasClass("datepicker-day-button")||e.hasClass("is-empty")||e.parent().hasClass("is-disabled")?e.closest(".month-prev").length?this.prevMonth():e.closest(".month-next").length&&this.nextMonth():(this.setDate(new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"))),this.options.autoClose&&this._finishSelection()))}}},{key:"_handleClearClick",value:function(){this.date=null,this.setInputValue(),this.close()}},{key:"_handleMonthChange",value:function(t){this.gotoMonth(t.target.value)}},{key:"_handleYearChange",value:function(t){this.gotoYear(t.target.value)}},{key:"gotoMonth",value:function(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}},{key:"gotoYear",value:function(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}},{key:"_handleInputChange",value:function(t){var e=void 0;t.firedBy!==this&&(e=this.options.parse?this.options.parse(this.el.value,this.options.format):new Date(Date.parse(this.el.value)),B._isDate(e)&&this.setDate(e))}},{key:"renderDayName",value:function(t,e,i){for(e+=t.firstDay;7<=e;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}},{key:"_finishSelection",value:function(){this.setInputValue(),this.close()}},{key:"open",value:function(){if(!this.isOpen)return this.isOpen=!0,"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.draw(),this.modal.open(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.modal.close(),this}}],[{key:"init",value:function(t,e){return _get(B.__proto__||Object.getPrototypeOf(B),"init",this).call(this,this,t,e)}},{key:"_isDate",value:function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}},{key:"_isWeekend",value:function(t){var e=t.getDay();return 0===e||6===e}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"_getDaysInMonth",value:function(t,e){return[31,B._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}},{key:"_isLeapYear",value:function(t){return t%4==0&&t%100!=0||t%400==0}},{key:"_compareDates",value:function(t,e){return t.getTime()===e.getTime()}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Datepicker}},{key:"defaults",get:function(){return e}}]),B}();t._template=['"].join(""),M.Datepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"datepicker","M_Datepicker")}(cash),function(h){"use strict";var e={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},autoClose:!1,twelveHour:!0,vibrate:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onSelect:null},t=function(t){function f(t,e){_classCallCheck(this,f);var i=_possibleConstructorReturn(this,(f.__proto__||Object.getPrototypeOf(f)).call(this,f,t,e));return(i.el.M_Timepicker=i).options=h.extend({},f.defaults,e),i.id=M.guid(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupVariables(),i._setupEventHandlers(),i._clockSetup(),i._pickerSetup(),i}return _inherits(f,Component),_createClass(f,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),h(this.modalEl).remove(),this.el.M_Timepicker=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleClockClickStartBound=this._handleClockClickStart.bind(this),this._handleDocumentClickMoveBound=this._handleDocumentClickMove.bind(this),this._handleDocumentClickEndBound=this._handleDocumentClickEnd.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.plate.addEventListener("mousedown",this._handleClockClickStartBound),this.plate.addEventListener("touchstart",this._handleClockClickStartBound),h(this.spanHours).on("click",this.showView.bind(this,"hours")),h(this.spanMinutes).on("click",this.showView.bind(this,"minutes"))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleClockClickStart",value:function(t){t.preventDefault();var e=this.plate.getBoundingClientRect(),i=e.left,n=e.top;this.x0=i+this.options.dialRadius,this.y0=n+this.options.dialRadius,this.moved=!1;var s=f._Pos(t);this.dx=s.x-this.x0,this.dy=s.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMoveBound),document.addEventListener("touchmove",this._handleDocumentClickMoveBound),document.addEventListener("mouseup",this._handleDocumentClickEndBound),document.addEventListener("touchend",this._handleDocumentClickEndBound)}},{key:"_handleDocumentClickMove",value:function(t){t.preventDefault();var e=f._Pos(t),i=e.x-this.x0,n=e.y-this.y0;this.moved=!0,this.setHand(i,n,!1,!0)}},{key:"_handleDocumentClickEnd",value:function(t){var e=this;t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEndBound),document.removeEventListener("touchend",this._handleDocumentClickEndBound);var i=f._Pos(t),n=i.x-this.x0,s=i.y-this.y0;this.moved&&n===this.dx&&s===this.dy&&this.setHand(n,s),"hours"===this.currentView?this.showView("minutes",this.options.duration/2):this.options.autoClose&&(h(this.minutesView).addClass("timepicker-dial-out"),setTimeout(function(){e.done()},this.options.duration/2)),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMoveBound),document.removeEventListener("touchmove",this._handleDocumentClickMoveBound)}},{key:"_insertHTMLIntoDOM",value:function(){this.$modalEl=h(f._template),this.modalEl=this.$modalEl[0],this.modalEl.id="modal-"+this.id;var t=document.querySelector(this.options.container);this.options.container&&t?this.$modalEl.appendTo(t):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modal=M.Modal.init(this.modalEl,{onOpenStart:this.options.onOpenStart,onOpenEnd:this.options.onOpenEnd,onCloseStart:this.options.onCloseStart,onCloseEnd:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t),t.isOpen=!1}})}},{key:"_setupVariables",value:function(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.modalEl.querySelector(".timepicker-canvas"),this.plate=this.modalEl.querySelector(".timepicker-plate"),this.hoursView=this.modalEl.querySelector(".timepicker-hours"),this.minutesView=this.modalEl.querySelector(".timepicker-minutes"),this.spanHours=this.modalEl.querySelector(".timepicker-span-hours"),this.spanMinutes=this.modalEl.querySelector(".timepicker-span-minutes"),this.spanAmPm=this.modalEl.querySelector(".timepicker-span-am-pm"),this.footer=this.modalEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}},{key:"_pickerSetup",value:function(){var t=h('").appendTo(this.footer).on("click",this.clear.bind(this));this.options.showClearBtn&&t.css({visibility:""});var e=h('
    ');h('").appendTo(e).on("click",this.close.bind(this)),h('").appendTo(e).on("click",this.done.bind(this)),e.appendTo(this.footer)}},{key:"_clockSetup",value:function(){this.options.twelveHour&&(this.$amBtn=h('
    AM
    '),this.$pmBtn=h('
    PM
    '),this.$amBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm),this.$pmBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}},{key:"_buildSVGClock",value:function(){var t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,n=f._createSVGEl("svg");n.setAttribute("class","timepicker-svg"),n.setAttribute("width",i),n.setAttribute("height",i);var s=f._createSVGEl("g");s.setAttribute("transform","translate("+t+","+t+")");var o=f._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx",0),o.setAttribute("cy",0),o.setAttribute("r",4);var a=f._createSVGEl("line");a.setAttribute("x1",0),a.setAttribute("y1",0);var r=f._createSVGEl("circle");r.setAttribute("class","timepicker-canvas-bg"),r.setAttribute("r",e),s.appendChild(a),s.appendChild(r),s.appendChild(o),n.appendChild(s),this._canvas.appendChild(n),this.hand=a,this.bg=r,this.bearing=o,this.g=s}},{key:"_buildHoursView",value:function(){var t=h('
    ');if(this.options.twelveHour)for(var e=1;e<13;e+=1){var i=t.clone(),n=e/6*Math.PI,s=this.options.outerRadius;i.css({left:this.options.dialRadius+Math.sin(n)*s-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*s-this.options.tickRadius+"px"}),i.html(0===e?"00":e),this.hoursView.appendChild(i[0])}else for(var o=0;o<24;o+=1){var a=t.clone(),r=o/6*Math.PI,l=0
    '),e=0;e<60;e+=5){var i=t.clone(),n=e/30*Math.PI;i.css({left:this.options.dialRadius+Math.sin(n)*this.options.outerRadius-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*this.options.outerRadius-this.options.tickRadius+"px"}),i.html(f._addLeadingZero(e)),this.minutesView.appendChild(i[0])}}},{key:"_handleAmPmClick",value:function(t){var e=h(t.target);this.amOrPm=e.hasClass("am-btn")?"AM":"PM",this._updateAmPmView()}},{key:"_updateAmPmView",value:function(){this.options.twelveHour&&(this.$amBtn.toggleClass("text-primary","AM"===this.amOrPm),this.$pmBtn.toggleClass("text-primary","PM"===this.amOrPm))}},{key:"_updateTimeFromInput",value:function(){var t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(0','",""].join(""),M.Timepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"timepicker","M_Timepicker")}(cash),function(s){"use strict";var e={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_CharacterCounter=i).options=s.extend({},n.defaults,e),i.isInvalid=!1,i.isValidLength=!1,i._setupCounter(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}},{key:"_setupEventHandlers",value:function(){this._handleUpdateCounterBound=this.updateCounter.bind(this),this.el.addEventListener("focus",this._handleUpdateCounterBound,!0),this.el.addEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("focus",this._handleUpdateCounterBound,!0),this.el.removeEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_setupCounter",value:function(){this.counterEl=document.createElement("span"),s(this.counterEl).addClass("character-counter").css({float:"right","font-size":"12px",height:1}),this.$el.parent().append(this.counterEl)}},{key:"_removeCounter",value:function(){s(this.counterEl).remove()}},{key:"updateCounter",value:function(){var t=+this.$el.attr("data-length"),e=this.el.value.length;this.isValidLength=e<=t;var i=e;t&&(i+="/"+t,this._validateInput()),s(this.counterEl).html(i)}},{key:"_validateInput",value:function(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.$el.removeClass("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.$el.removeClass("valid"),this.$el.addClass("invalid"))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_CharacterCounter}},{key:"defaults",get:function(){return e}}]),n}();M.CharacterCounter=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"characterCounter","M_CharacterCounter")}(cash),function(b){"use strict";var e={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null},t=function(t){function i(t,e){_classCallCheck(this,i);var n=_possibleConstructorReturn(this,(i.__proto__||Object.getPrototypeOf(i)).call(this,i,t,e));return(n.el.M_Carousel=n).options=b.extend({},i.defaults,e),n.hasMultipleSlides=1'),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('
  • ');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i'+t.getAttribute("label")+"")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'":e.innerHTML,a=d("
  • "),r=d("");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime); diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html deleted file mode 100644 index 1e546b037f..0000000000 --- a/esphome/dashboard/templates/index.html +++ /dev/null @@ -1,676 +0,0 @@ - - - - - - - Dashboard - ESPHome - - - - - - - - - - - - - - - {% if streamer_mode %} - - {% end %} - - - - - - -
    -
    - -
    - - {% for i, entry in enumerate(entries) %} -
    -
    - - {{ escape(entry.name) }} - - more_vert - - {% if 'web_server' in entry.loaded_integrations %} - launch - {% end %} - - {% if entry.update_available %} - system_update - {% end %} - - -
    - Filename: - - {{ escape(entry.filename) }} - -
    - - {% if entry.comment %} -
    - {{ escape(entry.comment) }} -
    - {% end %} - -
    -
    - Edit - Validate - Upload - Logs -
    - -
    - {% end %} - -
    - -
    - - {% if len(entries) == 0 %} -
    -
    Welcome to ESPHome
    -

    It looks like you don't yet have any Nodes configured.

    -

    Click on the pulsating button at the bottom right of the page to add a Node.

    -
    - - - {% end %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - {% if begin and len(entries) == 1 %} - - {% end %} - - - - diff --git a/esphome/dashboard/templates/login.html b/esphome/dashboard/templates/login.html deleted file mode 100644 index f8d1116dff..0000000000 --- a/esphome/dashboard/templates/login.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - Login - ESPHome - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    - - v{{ version }} - Dashboard Login -

    - {% if hassio %} - Login by entering your Home Assistant login credentials. - {% else %} - Login by entering your ESPHome login credentials. - {% end %} -

    - - {% if error is not None %} -
    - Error! - {{ escape(error) }} -
    - - - {% end %} - -
    - {% if has_username or hassio %} -
    -
    - person - - -
    -
    - {% end %} - -
    -
    - lock - - -
    -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - - - -
    -
    - - - diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py new file mode 100644 index 0000000000..3e3864aa17 --- /dev/null +++ b/esphome/dashboard/util.py @@ -0,0 +1,9 @@ +import hashlib + + +def password_hash(password: str) -> bytes: + """Create a hash of a password to transform it to a fixed-length digest. + + Note this is not meant for secure storage, but for securely comparing passwords. + """ + return hashlib.sha256(password.encode()).digest() diff --git a/esphome/espota2.py b/esphome/espota2.py index edfa4e63e6..8f299395dd 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -4,6 +4,7 @@ import random import socket import sys import time +import gzip from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address @@ -17,6 +18,7 @@ RESPONSE_UPDATE_PREPARE_OK = 66 RESPONSE_BIN_MD5_OK = 67 RESPONSE_RECEIVE_OK = 68 RESPONSE_UPDATE_END_OK = 69 +RESPONSE_SUPPORTS_COMPRESSION = 70 RESPONSE_ERROR_MAGIC = 128 RESPONSE_ERROR_UPDATE_PREPARE = 129 @@ -34,6 +36,8 @@ OTA_VERSION_1_0 = 1 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] +FEATURE_SUPPORTS_COMPRESSION = 0x01 + _LOGGER = logging.getLogger(__name__) @@ -52,14 +56,13 @@ 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() # pylint: disable=no-self-use def done(self): - sys.stderr.write('\n') + sys.stderr.write("\n") sys.stderr.flush() @@ -78,24 +81,24 @@ def receive_exactly(sock, amount, msg, expect, decode=True): if decode: data = [] else: - data = b'' + data = b"" try: data += recv_decode(sock, 1, decode=decode) except OSError as err: - raise OTAError(f"Error receiving acknowledge {msg}: {err}") + raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err try: check_error(data, expect) except OTAError as err: sock.close() - raise OTAError(f"Error {msg}: {err}") + raise OTAError(f"Error {msg}: {err}") from err while len(data) < amount: try: data += recv_decode(sock, amount - len(data), decode=decode) except OSError as err: - raise OTAError(f"Error receiving {msg}: {err}") + raise OTAError(f"Error receiving {msg}: {err}") from err return data @@ -106,38 +109,54 @@ def check_error(data, expect): if dat == RESPONSE_ERROR_MAGIC: raise OTAError("Error: Invalid magic byte") if dat == RESPONSE_ERROR_UPDATE_PREPARE: - raise OTAError("Error: Couldn't prepare flash memory for update. Is the binary too big? " - "Please try restarting the ESP.") + raise OTAError( + "Error: Couldn't prepare flash memory for update. Is the binary too big? " + "Please try restarting the ESP." + ) if dat == RESPONSE_ERROR_AUTH_INVALID: raise OTAError("Error: Authentication invalid. Is the password correct?") if dat == RESPONSE_ERROR_WRITING_FLASH: - raise OTAError("Error: Wring OTA data to flash memory failed. See USB logs for more " - "information.") + raise OTAError( + "Error: Wring OTA data to flash memory failed. See USB logs for more " + "information." + ) if dat == RESPONSE_ERROR_UPDATE_END: - raise OTAError("Error: Finishing update failed. See the MQTT/USB logs for more " - "information.") + raise OTAError( + "Error: Finishing update failed. See the MQTT/USB logs for more " + "information." + ) if dat == RESPONSE_ERROR_INVALID_BOOTSTRAPPING: - raise OTAError("Error: Please press the reset button on the ESP. A manual reset is " - "required on the first OTA-Update after flashing via USB.") + raise OTAError( + "Error: Please press the reset button on the ESP. A manual reset is " + "required on the first OTA-Update after flashing via USB." + ) if dat == RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG: - raise OTAError("Error: ESP has been flashed with wrong flash size. Please choose the " - "correct 'board' option (esp01_1m always works) and then flash over USB.") + raise OTAError( + "Error: ESP has been flashed with wrong flash size. Please choose the " + "correct 'board' option (esp01_1m always works) and then flash over USB." + ) if dat == RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: - raise OTAError("Error: ESP does not have the requested flash size (wrong board). Please " - "choose the correct 'board' option (esp01_1m always works) and try " - "uploading again.") + raise OTAError( + "Error: ESP does not have the requested flash size (wrong board). Please " + "choose the correct 'board' option (esp01_1m always works) and try " + "uploading again." + ) if dat == RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: - raise OTAError("Error: ESP does not have enough space to store OTA file. Please try " - "flashing a minimal firmware (remove everything except ota)") + raise OTAError( + "Error: ESP does not have enough space to store OTA file. Please try " + "flashing a minimal firmware (remove everything except ota)" + ) if dat == RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: - raise OTAError("Error: The OTA partition on the ESP is too small. ESPHome needs to resize " - "this partition, please flash over USB.") + raise OTAError( + "Error: The OTA partition on the ESP is too small. ESPHome needs to resize " + "this partition, please flash over USB." + ) if dat == RESPONSE_ERROR_UNKNOWN: raise OTAError("Unknown error from ESP") 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): @@ -147,64 +166,78 @@ def send_check(sock, data, msg): elif isinstance(data, int): data = bytes([data]) elif isinstance(data, str): - data = data.encode('utf8') + data = data.encode("utf8") sock.sendall(data) except OSError as err: - raise OTAError(f"Error sending {msg}: {err}") + raise OTAError(f"Error sending {msg}: {err}") from err def perform_ota(sock, password, file_handle, filename): - file_md5 = hashlib.md5(file_handle.read()).hexdigest() - file_size = file_handle.tell() - _LOGGER.info('Uploading %s (%s bytes)', filename, file_size) - file_handle.seek(0) - _LOGGER.debug("MD5 of binary is %s", file_md5) + file_contents = file_handle.read() + file_size = len(file_contents) + _LOGGER.info("Uploading %s (%s bytes)", filename, file_size) # Enable nodelay, we need it for phase 1 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - send_check(sock, MAGIC_BYTES, 'magic bytes') + send_check(sock, MAGIC_BYTES, "magic bytes") - _, version = receive_exactly(sock, 2, 'version', RESPONSE_OK) + _, version = receive_exactly(sock, 2, "version", RESPONSE_OK) if version != OTA_VERSION_1_0: raise OTAError(f"Unsupported OTA version {version}") # Features - send_check(sock, 0x00, 'features') - receive_exactly(sock, 1, 'features', RESPONSE_HEADER_OK) + send_check(sock, FEATURE_SUPPORTS_COMPRESSION, "features") + features = receive_exactly( + sock, 1, "features", [RESPONSE_HEADER_OK, RESPONSE_SUPPORTS_COMPRESSION] + )[0] - auth, = receive_exactly(sock, 1, 'auth', [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK]) + if features == RESPONSE_SUPPORTS_COMPRESSION: + upload_contents = gzip.compress(file_contents, compresslevel=9) + _LOGGER.info("Compressed to %s bytes", len(upload_contents)) + else: + upload_contents = file_contents + + (auth,) = receive_exactly( + sock, 1, "auth", [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK] + ) if auth == RESPONSE_REQUEST_AUTH: if not password: raise OTAError("ESP requests password, but no password given!") - nonce = receive_exactly(sock, 32, 'authentication nonce', [], decode=False).decode() + nonce = receive_exactly( + sock, 32, "authentication nonce", [], decode=False + ).decode() _LOGGER.debug("Auth: Nonce is %s", nonce) cnonce = hashlib.md5(str(random.random()).encode()).hexdigest() _LOGGER.debug("Auth: CNonce is %s", cnonce) - send_check(sock, cnonce, 'auth cnonce') + send_check(sock, cnonce, "auth cnonce") result_md5 = hashlib.md5() - result_md5.update(password.encode('utf-8')) + result_md5.update(password.encode("utf-8")) result_md5.update(nonce.encode()) result_md5.update(cnonce.encode()) result = result_md5.hexdigest() _LOGGER.debug("Auth: Result is %s", result) - send_check(sock, result, 'auth result') - receive_exactly(sock, 1, 'auth result', RESPONSE_AUTH_OK) + send_check(sock, result, "auth result") + receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK) - file_size_encoded = [ - (file_size >> 24) & 0xFF, - (file_size >> 16) & 0xFF, - (file_size >> 8) & 0xFF, - (file_size >> 0) & 0xFF, + upload_size = len(upload_contents) + upload_size_encoded = [ + (upload_size >> 24) & 0xFF, + (upload_size >> 16) & 0xFF, + (upload_size >> 8) & 0xFF, + (upload_size >> 0) & 0xFF, ] - send_check(sock, file_size_encoded, 'binary size') - receive_exactly(sock, 1, 'binary size', RESPONSE_UPDATE_PREPARE_OK) + send_check(sock, upload_size_encoded, "binary size") + receive_exactly(sock, 1, "binary size", RESPONSE_UPDATE_PREPARE_OK) - send_check(sock, file_md5, 'file checksum') - receive_exactly(sock, 1, 'file checksum', RESPONSE_BIN_MD5_OK) + upload_md5 = hashlib.md5(upload_contents).hexdigest() + _LOGGER.debug("MD5 of upload is %s", upload_md5) + + send_check(sock, upload_md5, "file checksum") + receive_exactly(sock, 1, "file checksum", RESPONSE_BIN_MD5_OK) # Disable nodelay for transfer sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) @@ -217,7 +250,7 @@ def perform_ota(sock, password, file_handle, filename): offset = 0 progress = ProgressBar() while True: - chunk = file_handle.read(1024) + chunk = upload_contents[offset : offset + 1024] if not chunk: break offset += len(chunk) @@ -225,10 +258,10 @@ def perform_ota(sock, password, file_handle, filename): try: sock.sendall(chunk) except OSError as err: - sys.stderr.write('\n') - raise OTAError(f"Error sending data: {err}") + sys.stderr.write("\n") + raise OTAError(f"Error sending data: {err}") from err - progress.update(offset / float(file_size)) + progress.update(offset / upload_size) progress.done() # Enable nodelay for last checks @@ -236,9 +269,9 @@ def perform_ota(sock, password, file_handle, filename): _LOGGER.info("Waiting for result...") - receive_exactly(sock, 1, 'receive OK', RESPONSE_RECEIVE_OK) - receive_exactly(sock, 1, 'Update end', RESPONSE_UPDATE_END_OK) - send_check(sock, RESPONSE_OK, 'end acknowledgement') + receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK) + receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK) + send_check(sock, RESPONSE_OK, "end acknowledgement") _LOGGER.info("OTA successful") @@ -255,11 +288,15 @@ def run_ota_impl_(remote_host, remote_port, password, filename): try: ip = resolve_ip_address(remote_host) except EsphomeError as err: - _LOGGER.error("Error resolving IP address of %s. Is it connected to WiFi?", - remote_host) - _LOGGER.error("(If this error persists, please set a static IP address: " - "https://esphome.io/components/wifi.html#manual-ips)") - raise OTAError(err) + _LOGGER.error( + "Error resolving IP address of %s. Is it connected to WiFi?", + remote_host, + ) + _LOGGER.error( + "(If this error persists, please set a static IP address: " + "https://esphome.io/components/wifi.html#manual-ips)" + ) + raise OTAError(err) from err _LOGGER.info(" -> %s", ip) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -271,15 +308,14 @@ def run_ota_impl_(remote_host, remote_port, password, filename): _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err) return 1 - file_handle = open(filename, 'rb') - try: - perform_ota(sock, password, file_handle, filename) - except OTAError as err: - _LOGGER.error(str(err)) - return 1 - finally: - sock.close() - file_handle.close() + with open(filename, "rb") as file_handle: + try: + perform_ota(sock, password, file_handle, filename) + except OTAError as err: + _LOGGER.error(str(err)) + return 1 + finally: + sock.close() return 0 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..64c8d6a6b7 --- /dev/null +++ b/esphome/git.py @@ -0,0 +1,95 @@ +from pathlib import Path +import subprocess +import hashlib +import logging +import urllib.parse + +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, + username: str = None, + password: str = None, +) -> Path: + key = f"{url}@{ref}" + + if username is not None and password is not None: + url = url.replace( + "://", f"://{urllib.parse.quote(username)}:{urllib.parse.quote(password)}@" + ) + + repo_dir = _compute_destination_path(key, domain) + fetch_pr_branch = ref is not None and ref.startswith("pull/") + if not repo_dir.is_dir(): + _LOGGER.info("Cloning %s", key) + _LOGGER.debug("Location: %s", repo_dir) + cmd = ["git", "clone", "--depth=1"] + if ref is not None and not fetch_pr_branch: + cmd += ["--branch", ref] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) + + if fetch_pr_branch: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], str(repo_dir)) + run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + + 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 6413a25e01..1193d61eaa 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -2,6 +2,9 @@ import codecs import logging import os +from pathlib import Path +from typing import Union +import tempfile _LOGGER = logging.getLogger(__name__) @@ -19,59 +22,48 @@ def ensure_unique_string(preferred_string, current_strings): return test_string -def indent_all_but_first_and_last(text, padding=' '): +def indent_all_but_first_and_last(text, padding=" "): lines = text.splitlines(True) if len(lines) <= 2: return text - return lines[0] + ''.join(padding + line for line in lines[1:-1]) + lines[-1] + return lines[0] + "".join(padding + line for line in lines[1:-1]) + lines[-1] -def indent_list(text, padding=' '): +def indent_list(text, padding=" "): return [padding + line for line in text.splitlines()] -def indent(text, padding=' '): - return '\n'.join(indent_list(text, padding)) +def indent(text, padding=" "): + return "\n".join(indent_list(text, padding)) # From https://stackoverflow.com/a/14945195/8924614 -def cpp_string_escape(string, encoding='utf-8'): +def cpp_string_escape(string, encoding="utf-8"): def _should_escape(byte): # type: (int) -> bool if not 32 <= byte < 127: return True - if byte in (ord('\\'), ord('"')): + if byte in (ord("\\"), ord('"')): return True return False if isinstance(string, str): string = string.encode(encoding) - result = '' + result = "" for character in string: if _should_escape(character): - result += f'\\{character:03o}' + result += f"\\{character:03o}" else: result += chr(character) - return '"' + result + '"' - - -def color(the_color, message=''): - from colorlog.escape_codes import escape_codes, parse_colors - - if not message: - res = parse_colors(the_color) - else: - res = parse_colors(the_color) + message + escape_codes['reset'] - - return res + return f'"{result}"' def run_system_command(*args): import subprocess - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() - rc = p.returncode - return rc, stdout, stderr + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: + stdout, stderr = p.communicate() + rc = p.returncode + return rc, stdout, stderr def mkdir_p(path): @@ -82,15 +74,17 @@ def mkdir_p(path): os.makedirs(path) except OSError as err: import errno + if err.errno == errno.EEXIST and os.path.isdir(path): pass else: from esphome.core import EsphomeError - raise EsphomeError(f"Error creating directories {path}: {err}") + + raise EsphomeError(f"Error creating directories {path}: {err}") from err def is_ip_address(host): - parts = host.split('.') + parts = host.split(".") if len(parts) != 4: return False try: @@ -103,22 +97,26 @@ 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() - except Exception: - raise EsphomeError("Cannot start mDNS sockets, is this a docker container without " - "host network mode?") - try: - info = zc.resolve_host(host + '.') + zc = EsphomeZeroconf() except Exception as err: - raise EsphomeError(f"Error resolving mDNS hostname: {err}") + raise EsphomeError( + "Cannot start mDNS sockets, is this a docker container without " + "host network mode?" + ) from err + try: + info = zc.resolve_host(f"{host}.") + except Exception as err: + raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err finally: zc.close() if info is None: - raise EsphomeError("Error resolving address with mDNS: Did not respond. " - "Maybe the device is offline.") + raise EsphomeError( + "Error resolving address with mDNS: Did not respond. " + "Maybe the device is offline." + ) return info @@ -128,7 +126,7 @@ def resolve_ip_address(host): errs = [] - if host.endswith('.local'): + if host.endswith(".local"): try: return _resolve_with_zeroconf(host) except EsphomeError as err: @@ -138,8 +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))) + raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err def get_bool_env(var, default=False): @@ -147,7 +144,7 @@ def get_bool_env(var, default=False): def is_hassio(): - return get_bool_env('ESPHOME_IS_HASSIO') + return get_bool_env("ESPHOME_IS_HASSIO") def walk_files(path): @@ -158,27 +155,37 @@ def walk_files(path): def read_file(path): try: - with codecs.open(path, 'r', encoding='utf-8') as f_handle: + with codecs.open(path, "r", encoding="utf-8") as f_handle: return f_handle.read() except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Error reading file {path}: {err}") + + raise EsphomeError(f"Error reading file {path}: {err}") from err except UnicodeDecodeError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Error reading file {path}: {err}") + + raise EsphomeError(f"Error reading file {path}: {err}") from err -def _write_file(path, text): - import tempfile - directory = os.path.dirname(path) - mkdir_p(directory) +def _write_file(path: Union[Path, str], text: Union[str, bytes]): + """Atomically writes `text` to the given path. - tmp_path = None + Automatically creates all parent directories. + """ + if not isinstance(path, Path): + path = Path(path) data = text if isinstance(text, str): data = text.encode() + + directory = path.parent + directory.mkdir(exist_ok=True, parents=True) + + tmp_path = None try: - with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: + with tempfile.NamedTemporaryFile( + mode="wb", dir=directory, delete=False + ) as f_handle: tmp_path = f_handle.name f_handle.write(data) # Newer tempfile implementations create the file with mode 0o600 @@ -193,24 +200,35 @@ def _write_file(path, text): _LOGGER.error("Write file cleanup failed: %s", err) -def write_file(path, text): +def write_file(path: Union[Path, str], text: str): try: _write_file(path, text) - except OSError: + except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Could not write file at {path}") + + raise EsphomeError(f"Could not write file at {path}") from err -def write_file_if_changed(path, text): +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 os.path.isfile(path): + 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, dst): +def copy_file_if_changed(src: os.PathLike, dst: os.PathLike) -> None: import shutil + if file_compare(src, dst): return mkdir_p(os.path.dirname(dst)) @@ -218,14 +236,15 @@ def copy_file_if_changed(src, dst): shutil.copy(src, dst) except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Error copying file {src} to {dst}: {err}") + + raise EsphomeError(f"Error copying file {src} to {dst}: {err}") from err def list_starts_with(list_, sub): return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub)) -def file_compare(path1, path2): +def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: """Return True if the files path1 and path2 have the same contents.""" import stat @@ -235,16 +254,19 @@ def file_compare(path1, path2): # File doesn't exist or another error -> not equal return False - if stat.S_IFMT(stat1.st_mode) != stat.S_IFREG or stat.S_IFMT(stat2.st_mode) != stat.S_IFREG: + if ( + stat.S_IFMT(stat1.st_mode) != stat.S_IFREG + or stat.S_IFMT(stat2.st_mode) != stat.S_IFREG + ): # At least one of them is not a regular file (or does not exist) return False if stat1.st_size != stat2.st_size: # Different sizes return False - bufsize = 8*1024 + bufsize = 8 * 1024 # Read files in blocks until a mismatch is found - with open(path1, 'rb') as fh1, open(path2, 'rb') as fh2: + with open(path1, "rb") as fh1, open(path2, "rb") as fh2: while True: blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize) if blob1 != blob2: @@ -258,11 +280,11 @@ def file_compare(path1, path2): # 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/jsonschema.py b/esphome/jsonschema.py new file mode 100644 index 0000000000..12929dc602 --- /dev/null +++ b/esphome/jsonschema.py @@ -0,0 +1,91 @@ +"""Helpers to retrieve schema from voluptuous validators. + +These are a helper decorators to help get schema from some +components which uses volutuous in a way where validation +is hidden in local functions +These decorators should not modify at all what the functions +originally do. +However there is a property to further disable decorator +impact.""" + + +# This is set to true by script/build_jsonschema.py +# only, so data is collected (again functionality is not modified) +EnableJsonSchemaCollect = False + +extended_schemas = {} +list_schemas = {} +registry_schemas = {} +hidden_schemas = {} +typed_schemas = {} + + +def jschema_extractor(validator_name): + if EnableJsonSchemaCollect: + + def decorator(func): + hidden_schemas[str(func)] = validator_name + return func + + return decorator + + def dummy(f): + return f + + return dummy + + +def jschema_extended(func): + if EnableJsonSchemaCollect: + + def decorate(*args, **kwargs): + ret = func(*args, **kwargs) + assert len(args) == 2 + extended_schemas[str(ret)] = args + return ret + + return decorate + + return func + + +def jschema_composite(func): + if EnableJsonSchemaCollect: + + def decorate(*args, **kwargs): + ret = func(*args, **kwargs) + # args length might be 2, but 2nd is always validator + list_schemas[str(ret)] = args + return ret + + return decorate + + return func + + +def jschema_registry(registry): + if EnableJsonSchemaCollect: + + def decorator(func): + registry_schemas[str(func)] = registry + return func + + return decorator + + def dummy(f): + return f + + return dummy + + +def jschema_typed(func): + if EnableJsonSchemaCollect: + + def decorate(*args, **kwargs): + ret = func(*args, **kwargs) + typed_schemas[str(ret)] = (args, kwargs) + return ret + + return decorate + + return func diff --git a/esphome/legacy.py b/esphome/legacy.py deleted file mode 100644 index 27373ee1a3..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 new file mode 100644 index 0000000000..05d2e5a213 --- /dev/null +++ b/esphome/loader.py @@ -0,0 +1,188 @@ +import logging +from typing import Callable, List, Optional, Any, ContextManager +from types import ModuleType +import importlib +import importlib.util +import importlib.resources +import importlib.abc +import sys +from pathlib import Path +from dataclasses import dataclass + +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__) + + +@dataclass(frozen=True, order=True) +class FileResource: + package: str + resource: str + + def path(self) -> ContextManager[Path]: + return importlib.resources.path(self.package, self.resource) + + +class ComponentManifest: + def __init__(self, module: ModuleType): + self.module = module + + @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 + def is_platform(self) -> bool: + return len(self.module.__name__.split(".")) == 4 + + @property + def is_platform_component(self) -> bool: + return getattr(self.module, "IS_PLATFORM_COMPONENT", False) + + @property + def config_schema(self) -> Optional[Any]: + return getattr(self.module, "CONFIG_SCHEMA", None) + + @property + def multi_conf(self) -> bool: + return getattr(self.module, "MULTI_CONF", False) + + @property + def to_code(self) -> Optional[Callable[[Any], None]]: + return getattr(self.module, "to_code", None) + + @property + def dependencies(self) -> List[str]: + return getattr(self.module, "DEPENDENCIES", []) + + @property + def conflicts_with(self) -> List[str]: + return getattr(self.module, "CONFLICTS_WITH", []) + + @property + def auto_load(self) -> List[str]: + return getattr(self.module, "AUTO_LOAD", []) + + @property + def codeowners(self) -> List[str]: + return getattr(self.module, "CODEOWNERS", []) + + @property + 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 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 + ret.append(FileResource(self.package, resource)) + return ret + + +class ComponentMetaFinder(importlib.abc.MetaPathFinder): + def __init__( + self, components_path: Path, allowed_components: Optional[List[str]] = None + ) -> None: + self._allowed_components = allowed_components + self._finders = [] + for hook in sys.path_hooks: + try: + finder = hook(str(components_path)) + except ImportError: + continue + self._finders.append(finder) + + def find_spec(self, fullname: str, path: Optional[List[str]], target=None): + if not fullname.startswith("esphome.components."): + return None + parts = fullname.split(".") + if len(parts) != 3: + # only handle direct components, not platforms + # platforms are handled automatically when parent is imported + return None + component = parts[2] + if ( + self._allowed_components is not None + and component not in self._allowed_components + ): + return None + + for finder in self._finders: + spec = finder.find_spec(fullname, target=target) + if spec is not None: + return spec + return None + + +def clear_component_meta_finders(): + sys.meta_path = [x for x in sys.meta_path if not isinstance(x, ComponentMetaFinder)] + + +def install_meta_finder( + components_path: Path, allowed_components: Optional[List[str]] = None +): + sys.meta_path.insert(0, ComponentMetaFinder(components_path, allowed_components)) + + +def install_custom_components_meta_finder(): + custom_components_dir = (Path(CORE.config_dir) / "custom_components").resolve() + install_meta_finder(custom_components_dir) + + +def _lookup_module(domain): + if domain in _COMPONENT_CACHE: + return _COMPONENT_CACHE[domain] + + try: + module = importlib.import_module(f"esphome.components.{domain}") + except ImportError as e: + if "No module named" not in str(e): + _LOGGER.error("Unable to import component %s:", domain, exc_info=True) + return None + except Exception: # pylint: disable=broad-except + _LOGGER.error("Unable to load component %s:", domain, exc_info=True) + return None + else: + manif = ComponentManifest(module) + _COMPONENT_CACHE[domain] = manif + return manif + + +def get_component(domain): + assert "." not in domain + return _lookup_module(domain) + + +def get_platform(domain, platform): + full = f"{platform}.{domain}" + return _lookup_module(full) + + +_COMPONENT_CACHE = {} +CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() +_COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) diff --git a/esphome/log.py b/esphome/log.py new file mode 100644 index 0000000000..e7ba0fdd82 --- /dev/null +++ b/esphome/log.py @@ -0,0 +1,88 @@ +import logging + +from esphome.core import CORE + + +class AnsiFore: + KEEP = "" + BLACK = "\033[30m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + MAGENTA = "\033[35m" + CYAN = "\033[36m" + WHITE = "\033[37m" + RESET = "\033[39m" + + BOLD_BLACK = "\033[1;30m" + BOLD_RED = "\033[1;31m" + BOLD_GREEN = "\033[1;32m" + BOLD_YELLOW = "\033[1;33m" + BOLD_BLUE = "\033[1;34m" + BOLD_MAGENTA = "\033[1;35m" + BOLD_CYAN = "\033[1;36m" + BOLD_WHITE = "\033[1;37m" + BOLD_RESET = "\033[1;39m" + + +class AnsiStyle: + BRIGHT = "\033[1m" + BOLD = "\033[1m" + DIM = "\033[2m" + THIN = "\033[2m" + NORMAL = "\033[22m" + RESET_ALL = "\033[0m" + + +Fore = AnsiFore() +Style = AnsiStyle() + + +def color(col: str, msg: str, reset: bool = True) -> bool: + if col and not col.startswith("\033["): + raise ValueError("Color must be value from esphome.log.Fore") + s = str(col) + msg + if reset and col: + s += str(Style.RESET_ALL) + return s + + +class ESPHomeLogFormatter(logging.Formatter): + def __init__(self, *, include_timestamp: bool): + fmt = "%(asctime)s " if include_timestamp else "" + fmt += "%(levelname)s %(message)s" + super().__init__(fmt=fmt, style="%") + + def format(self, record): + formatted = super().format(record) + prefix = { + "DEBUG": Fore.CYAN, + "INFO": Fore.GREEN, + "WARNING": Fore.YELLOW, + "ERROR": Fore.RED, + "CRITICAL": Fore.RED, + }.get(record.levelname, "") + return f"{prefix}{formatted}{Style.RESET_ALL}" + + +def setup_log( + debug: bool = False, quiet: bool = False, include_timestamp: bool = False +) -> None: + import colorama + + if debug: + log_level = logging.DEBUG + CORE.verbose = True + elif quiet: + log_level = logging.CRITICAL + else: + log_level = logging.INFO + logging.basicConfig(level=log_level) + + logging.getLogger("urllib3").setLevel(logging.WARNING) + + colorama.init() + logging.getLogger().handlers[0].setFormatter( + ESPHomeLogFormatter(include_timestamp=include_timestamp) + ) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index cbcf067c44..07602e8ced 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -7,11 +7,22 @@ import time import paho.mqtt.client as mqtt -from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \ - CONF_LOG_TOPIC, CONF_MQTT, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL_FINGERPRINTS, \ - CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME +from esphome.const import ( + CONF_BROKER, + CONF_DISCOVERY_PREFIX, + CONF_ESPHOME, + CONF_LOG_TOPIC, + CONF_MQTT, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL_FINGERPRINTS, + CONF_TOPIC, + CONF_TOPIC_PREFIX, + CONF_USERNAME, +) from esphome.core import CORE, EsphomeError -from esphome.helpers import color +from esphome.log import color, Fore from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -36,21 +47,24 @@ def initialize(config, subscriptions, on_message, username, password, client_id) except OSError: pass - wait_time = min(2**tries, 300) + wait_time = min(2 ** tries, 300) _LOGGER.warning( "Disconnected from MQTT (%s). Trying to reconnect in %s s", - result_code, wait_time) + result_code, + wait_time, + ) time.sleep(wait_time) tries += 1 - client = mqtt.Client(client_id or '') + client = mqtt.Client(client_id or "") client.on_connect = on_connect client.on_message = on_message client.on_disconnect = on_disconnect if username is None: if config[CONF_MQTT].get(CONF_USERNAME): - client.username_pw_set(config[CONF_MQTT][CONF_USERNAME], - config[CONF_MQTT][CONF_PASSWORD]) + client.username_pw_set( + config[CONF_MQTT][CONF_USERNAME], config[CONF_MQTT][CONF_PASSWORD] + ) elif username: client.username_pw_set(username, password) @@ -59,15 +73,21 @@ def initialize(config, subscriptions, on_message, username, password, client_id) tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member else: tls_version = ssl.PROTOCOL_SSLv23 - client.tls_set(ca_certs=None, certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED, - tls_version=tls_version, ciphers=None) + client.tls_set( + ca_certs=None, + certfile=None, + keyfile=None, + cert_reqs=ssl.CERT_REQUIRED, + tls_version=tls_version, + ciphers=None, + ) try: host = str(config[CONF_MQTT][CONF_BROKER]) port = int(config[CONF_MQTT][CONF_PORT]) client.connect(host, port) except OSError as err: - raise EsphomeError(f"Cannot connect to MQTT broker: {err}") + raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err try: client.loop_forever() @@ -84,17 +104,17 @@ 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 _LOGGER.info("Starting log output from %s", topic) def on_message(client, userdata, msg): - time_ = datetime.now().time().strftime('[%H:%M:%S]') - payload = msg.payload.decode(errors='backslashreplace') + time_ = datetime.now().time().strftime("[%H:%M:%S]") + payload = msg.payload.decode(errors="backslashreplace") message = time_ + payload safe_print(message) @@ -103,12 +123,14 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): def clear_topic(config, topic, username=None, password=None, client_id=None): if topic is None: - discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, 'homeassistant') + discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, "homeassistant") name = config[CONF_ESPHOME][CONF_NAME] - topic = f'{discovery_prefix}/+/{name}/#' + topic = f"{discovery_prefix}/+/{name}/#" _LOGGER.info("Clearing messages from '%s'", topic) - _LOGGER.info("Please close this window when no more messages appear and the " - "MQTT topic has been cleared of retained messages.") + _LOGGER.info( + "Please close this window when no more messages appear and the " + "MQTT topic has been cleared of retained messages." + ) def on_message(client, userdata, msg): if not msg.payload or not msg.retain: @@ -136,7 +158,8 @@ def get_fingerprint(config): sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print("SHA1 Fingerprint: " + color('cyan', sha1)) - safe_print("Copy the string above into mqtt.ssl_fingerprints section of {}" - "".format(CORE.config_path)) + safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}") + safe_print( + 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 22192b599e..2b3adce86d 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,499 +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}, - '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}, - '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}, - '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 = ESP8266_BOARD_PINS.get(CORE.board, {}) - base_pins = ESP8266_BASE_PINS - elif CORE.is_esp32: - board_pins = ESP32_BOARD_PINS.get(CORE.board, {}) - base_pins = ESP32_BASE_PINS - else: - raise NotImplementedError - - while isinstance(board_pins, str): - board_pins = ESP8266_BOARD_PINS.get(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 4866119902..2072e25ec5 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__) @@ -22,13 +25,14 @@ def patch_structhash(): from os import makedirs def patched_clean_build_dir(build_dir, *args): - from platformio import util + from platformio import fs from platformio.project.helpers import get_project_dir + platformio_ini = join(get_project_dir(), "platformio.ini") # if project's config is modified if isdir(build_dir) and getmtime(platformio_ini) > getmtime(build_dir): - util.rmtree_(build_dir) + fs.rmtree(build_dir) if not isdir(build_dir): makedirs(build_dir) @@ -38,54 +42,62 @@ 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/.*', - r'PLATFORM: .*', - r'DEBUG: Current.*', - 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.*", - r'Scanning dependencies...', + r"Verbose mode can be enabled via `-v, --verbose` option.*", + r"CONFIGURATION: https://docs.platformio.org/.*", + r"DEBUG: Current.*", + r"LDF Modes:.*", + r"LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*", + 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', - r'esptool.py v.*', + r"Memory Usage -> http://bit.ly/pio-memory-usage", r"Found: https://platformio.org/lib/show/.*", r"Using cache: .*", - r'Installing dependencies', - r'.* @ .* is already installed', - r'Building in .* mode', - r'Advanced Memory Usage is available via .*', + r"Installing dependencies", + r"Library Manager: Already installed, built-in library", + r"Building in .* mode", + r"Advanced Memory Usage is available via .*", + r"Merged .* ELF section", + r"esptool.py v.*", + r"Checking size .*", + r"Retrieving maximum program size .*", + r"PLATFORM: .*", + r"PACKAGES:.*", + r" - framework-arduinoespressif.* \(.*\)", + r" - tool-esptool.* \(.*\)", + r" - toolchain-.* \(.*\)", + r"Creating BIN file .*", ] 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()) - cmd = ['platformio'] + list(args) + os.environ.setdefault( + "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) + ) + cmd = ["platformio"] + list(args) if not CORE.verbose: - kwargs['filter_lines'] = FILTER_PLATFORMIO_LINES + kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES - if os.environ.get('ESPHOME_USE_SUBPROCESS') is not None: + if os.environ.get("ESPHOME_USE_SUBPROCESS") is not None: return run_external_process(*cmd, **kwargs) import platformio.__main__ + patch_structhash() - return run_external_command(platformio.__main__.main, - *cmd, **kwargs) + return run_external_command(platformio.__main__.main, *cmd, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> Union[str, int]: - command = ['run', '-d', CORE.build_path] + command = ["run", "-d", CORE.build_path] if verbose: - command += ['-v'] + command += ["-v"] command += list(args) return run_platformio_cli(*command, **kwargs) @@ -94,34 +106,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): - args = ['-t', 'idedata'] +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("idedata", f"{CORE.name}.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 @@ -129,10 +163,10 @@ ESP8266_EXCEPTION_CODES = { 0: "Illegal instruction (Is the flash damaged?)", 1: "SYSCALL instruction", 2: "InstructionFetchError: Processor internal physical address or data error during " - "instruction fetch", + "instruction fetch", 3: "LoadStoreError: Processor internal physical address or data error during load or store", 4: "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT " - "register", + "register", 5: "Alloca: MOVSP instruction, if caller's registers are not in the register file", 6: "Integer Divide By Zero", 7: "reserved", @@ -147,17 +181,17 @@ ESP8266_EXCEPTION_CODES = { 16: "InstTLBMiss: Error during Instruction TLB refill", 17: "InstTLBMultiHit: Multiple instruction TLB entries matched", 18: "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level " - "less than CRING", + "less than CRING", 19: "reserved", 20: "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute " - "that does not permit instruction fetch", + "that does not permit instruction fetch", 21: "reserved", 22: "reserved", 23: "reserved", 24: "LoadStoreTLBMiss: Error during TLB refill for a load or store", 25: "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", 26: "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less " - "than ", + "than ", 27: "reserved", 28: "Access to invalid address: LOAD (wild pointer?)", 29: "Access to invalid address: STORE (wild pointer?)", @@ -169,7 +203,7 @@ def _decode_pc(config, addr): if not idedata.addr2line_path or not idedata.firmware_elf_path: _LOGGER.debug("decode_pc no addr2line") return - command = [idedata.addr2line_path, '-pfiaC', '-e', idedata.firmware_elf_path, addr] + command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] try: translation = subprocess.check_output(command).decode().strip() except Exception: # pylint: disable=broad-except @@ -179,7 +213,7 @@ def _decode_pc(config, addr): if "?? ??:0" in translation: # Nothing useful return - translation = translation.replace(' at ??:?', '').replace(':?', '') + translation = translation.replace(" at ??:?", "").replace(":?", "") _LOGGER.warning("Decoded %s", translation) @@ -189,15 +223,21 @@ def _parse_register(config, regex, line): _decode_pc(config, match.group(1)) -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_EXCVADDR_RE = re.compile(r'EXCVADDR\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})+') -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}') +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_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})+" +) +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}") def process_stacktrace(config, line, backtrace_state): @@ -205,8 +245,10 @@ def process_stacktrace(config, line, backtrace_state): # ESP8266 Exception type match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line) if match is not None: - code = match.group(1) - _LOGGER.warning("Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, 'unknown')) + code = int(match.group(1)) + _LOGGER.warning( + "Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, "unknown") + ) # ESP8266 PC/EXCVADDR _parse_register(config, STACKTRACE_ESP8266_PC_RE, line) @@ -214,12 +256,16 @@ 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) if match is not None: - _LOGGER.warning("Memory allocation of %s bytes failed at %s", - match.group(2), match.group(1)) + _LOGGER.warning( + "Memory allocation of %s bytes failed at %s", match.group(2), match.group(1) + ) _decode_pc(config, match.group(1)) # ESP32 single-line backtrace @@ -230,11 +276,11 @@ def process_stacktrace(config, line, backtrace_state): _decode_pc(config, addr.group()) # ESP8266 multi-line backtrace - if '>>>stack>>>' in line: + if ">>>stack>>>" in line: # Start of backtrace backtrace_state = True _LOGGER.warning("Found stack trace! Trying to decode it") - elif '<< 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 - return cc_path[:-3] + 'addr2line' + + # Windows + if self.cc_path.endswith(".exe"): + return f"{self.cc_path[:-7]}addr2line.exe" + + return f"{self.cc_path[:-3]}addr2line" diff --git a/esphome/storage_json.py b/esphome/storage_json.py index b4dc29c9cd..207a3edf57 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -4,39 +4,49 @@ 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 - return os.path.join(base_path, '.esphome', f'{config_filename}.json') + return os.path.join(base_path, ".esphome", f"{config_filename}.json") def esphome_storage_path(base_path): # type: (str) -> str - return os.path.join(base_path, '.esphome', 'esphome.json') + return os.path.join(base_path, ".esphome", "esphome.json") def trash_storage_path(base_path): # type: (str) -> str - return os.path.join(base_path, '.esphome', 'trash') + return os.path.join(base_path, ".esphome", "trash") # pylint: disable=too-many-instance-attributes class StorageJSON: - def __init__(self, storage_version, name, comment, esphome_version, - src_version, arduino_version, address, esp_platform, board, build_path, - firmware_bin_path, loaded_integrations): + def __init__( + self, + storage_version, + name, + comment, + esphome_version, + src_version, + address, + web_port, + target_platform, + build_path, + firmware_bin_path, + loaded_integrations, + ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) self.storage_version = storage_version # type: int @@ -49,75 +59,72 @@ 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 + # Web server port of the ESP, for example 80 + assert web_port is None or isinstance(web_port, int) + self.web_port = web_port # type: int # 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 self.firmware_bin_path = firmware_bin_path # type: str # A list of strings of names of loaded integrations - self.loaded_integrations = loaded_integrations # type: List[str] + self.loaded_integrations = loaded_integrations # type: List[str] self.loaded_integrations.sort() def as_dict(self): return { - 'storage_version': self.storage_version, - 'name': self.name, - '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, - 'build_path': self.build_path, - 'firmware_bin_path': self.firmware_bin_path, - 'loaded_integrations': self.loaded_integrations, + "storage_version": self.storage_version, + "name": self.name, + "comment": self.comment, + "esphome_version": self.esphome_version, + "src_version": self.src_version, + "address": self.address, + "web_port": self.web_port, + "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()) @staticmethod - def from_esphome_core(esph, old): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON + def from_esphome_core( + esph, old + ): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON return StorageJSON( storage_version=1, name=esph.name, 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, + web_port=esph.web_port, + 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, + web_port=None, + target_platform=esp_platform, build_path=None, firmware_bin_path=None, loaded_integrations=[], @@ -125,23 +132,34 @@ class StorageJSON: @staticmethod def _load_impl(path): # type: (str) -> Optional[StorageJSON] - with codecs.open(path, 'r', encoding='utf-8') as f_handle: + with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) - storage_version = storage['storage_version'] - name = storage.get('name') - comment = storage.get('comment') - esphome_version = storage.get('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', []) - return StorageJSON(storage_version, name, comment, esphome_version, - src_version, arduino_version, address, esp_platform, board, build_path, - firmware_bin_path, loaded_integrations) + storage_version = storage["storage_version"] + name = storage.get("name") + comment = storage.get("comment") + esphome_version = storage.get( + "esphome_version", storage.get("esphomeyaml_version") + ) + src_version = storage.get("src_version") + address = storage.get("address") + web_port = storage.get("web_port") + esp_platform = storage.get("esp_platform") + build_path = storage.get("build_path") + firmware_bin_path = storage.get("firmware_bin_path") + loaded_integrations = storage.get("loaded_integrations", []) + return StorageJSON( + storage_version, + name, + comment, + esphome_version, + src_version, + address, + web_port, + esp_platform, + build_path, + firmware_bin_path, + loaded_integrations, + ) @staticmethod def load(path): # type: (str) -> Optional[StorageJSON] @@ -155,8 +173,9 @@ class StorageJSON: class EsphomeStorageJSON: - def __init__(self, storage_version, cookie_secret, last_update_check, - remote_version): + def __init__( + self, storage_version, cookie_secret, last_update_check, remote_version + ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) self.storage_version = storage_version # type: int @@ -169,10 +188,10 @@ class EsphomeStorageJSON: def as_dict(self): # type: () -> dict return { - 'storage_version': self.storage_version, - 'cookie_secret': self.cookie_secret, - 'last_update_check': self.last_update_check_str, - 'remote_version': self.remote_version, + "storage_version": self.storage_version, + "cookie_secret": self.cookie_secret, + "last_update_check": self.last_update_check_str, + "remote_version": self.remote_version, } @property @@ -187,21 +206,22 @@ 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()) @staticmethod def _load_impl(path): # type: (str) -> Optional[EsphomeStorageJSON] - with codecs.open(path, 'r', encoding='utf-8') as f_handle: + with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) - storage_version = storage['storage_version'] - cookie_secret = storage.get('cookie_secret') - last_update_check = storage.get('last_update_check') - remote_version = storage.get('remote_version') - return EsphomeStorageJSON(storage_version, cookie_secret, last_update_check, - remote_version) + storage_version = storage["storage_version"] + cookie_secret = storage.get("cookie_secret") + last_update_check = storage.get("last_update_check") + remote_version = storage.get("remote_version") + return EsphomeStorageJSON( + storage_version, cookie_secret, last_update_check, remote_version + ) @staticmethod def load(path): # type: (str) -> Optional[EsphomeStorageJSON] diff --git a/esphome/symlink_ops.py b/esphome/symlink_ops.py deleted file mode 100644 index accfbe5ff7..0000000000 --- a/esphome/symlink_ops.py +++ /dev/null @@ -1,164 +0,0 @@ -import os - -if hasattr(os, 'symlink'): - def symlink(src, dst): - return os.symlink(src, dst) - - def islink(path): - return os.path.islink(path) - - def readlink(path): - return os.readlink(path) - - def unlink(path): - return os.unlink(path) -else: - import ctypes - from ctypes import wintypes - # Code taken from - # https://stackoverflow.com/questions/27972776/having-trouble-implementing-a-readlink-function - - kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) - - FILE_READ_ATTRIBUTES = 0x0080 - OPEN_EXISTING = 3 - FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 - FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 - FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 - - IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 - IO_REPARSE_TAG_SYMLINK = 0xA000000C - FSCTL_GET_REPARSE_POINT = 0x000900A8 - MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000 - - LPDWORD = ctypes.POINTER(wintypes.DWORD) - LPWIN32_FIND_DATA = ctypes.POINTER(wintypes.WIN32_FIND_DATAW) - INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value - - def IsReparseTagNameSurrogate(tag): - return bool(tag & 0x20000000) - - def _check_invalid_handle(result, func, args): - if result == INVALID_HANDLE_VALUE: - raise ctypes.WinError(ctypes.get_last_error()) - return args - - def _check_bool(result, func, args): - if not result: - raise ctypes.WinError(ctypes.get_last_error()) - return args - - kernel32.FindFirstFileW.errcheck = _check_invalid_handle - kernel32.FindFirstFileW.restype = wintypes.HANDLE - kernel32.FindFirstFileW.argtypes = ( - wintypes.LPCWSTR, # _In_ lpFileName - LPWIN32_FIND_DATA) # _Out_ lpFindFileData - - kernel32.FindClose.argtypes = ( - wintypes.HANDLE,) # _Inout_ hFindFile - - kernel32.CreateFileW.errcheck = _check_invalid_handle - kernel32.CreateFileW.restype = wintypes.HANDLE - kernel32.CreateFileW.argtypes = ( - wintypes.LPCWSTR, # _In_ lpFileName - wintypes.DWORD, # _In_ dwDesiredAccess - wintypes.DWORD, # _In_ dwShareMode - wintypes.LPVOID, # _In_opt_ lpSecurityAttributes - wintypes.DWORD, # _In_ dwCreationDisposition - wintypes.DWORD, # _In_ dwFlagsAndAttributes - wintypes.HANDLE) # _In_opt_ hTemplateFile - - kernel32.CloseHandle.argtypes = ( - wintypes.HANDLE,) # _In_ hObject - - kernel32.DeviceIoControl.errcheck = _check_bool - kernel32.DeviceIoControl.argtypes = ( - wintypes.HANDLE, # _In_ hDevice - wintypes.DWORD, # _In_ dwIoControlCode - wintypes.LPVOID, # _In_opt_ lpInBuffer - wintypes.DWORD, # _In_ nInBufferSize - wintypes.LPVOID, # _Out_opt_ lpOutBuffer - wintypes.DWORD, # _In_ nOutBufferSize - LPDWORD, # _Out_opt_ lpBytesReturned - wintypes.LPVOID) # _Inout_opt_ lpOverlapped - - class REPARSE_DATA_BUFFER(ctypes.Structure): - class ReparseData(ctypes.Union): - class LinkData(ctypes.Structure): - _fields_ = (('SubstituteNameOffset', wintypes.USHORT), - ('SubstituteNameLength', wintypes.USHORT), - ('PrintNameOffset', wintypes.USHORT), - ('PrintNameLength', wintypes.USHORT)) - - @property - def PrintName(self): - dt = wintypes.WCHAR * (self.PrintNameLength // ctypes.sizeof(wintypes.WCHAR)) - name = dt.from_address(ctypes.addressof(self.PathBuffer) + - self.PrintNameOffset).value - if name.startswith(r'\??'): - name = r'\\?' + name[3:] # NT => Windows - return name - - class SymbolicLinkData(LinkData): - _fields_ = (('Flags', wintypes.ULONG), ('PathBuffer', wintypes.BYTE * 0)) - - class MountPointData(LinkData): - _fields_ = (('PathBuffer', wintypes.BYTE * 0),) - - class GenericData(ctypes.Structure): - _fields_ = (('DataBuffer', wintypes.BYTE * 0),) - _fields_ = (('SymbolicLinkReparseBuffer', SymbolicLinkData), - ('MountPointReparseBuffer', MountPointData), - ('GenericReparseBuffer', GenericData)) - _fields_ = (('ReparseTag', wintypes.ULONG), - ('ReparseDataLength', wintypes.USHORT), - ('Reserved', wintypes.USHORT), - ('ReparseData', ReparseData)) - _anonymous_ = ('ReparseData',) - - def symlink(src, dst): - csl = ctypes.windll.kernel32.CreateSymbolicLinkW - csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) - csl.restype = ctypes.c_ubyte - flags = 1 if os.path.isdir(src) else 0 - if csl(dst, src, flags) == 0: - error = ctypes.WinError() - # pylint: disable=no-member - if error.winerror == 1314 and error.errno == 22: - from esphome.core import EsphomeError - raise EsphomeError("Cannot create symlink from '%s' to '%s'. Try running tool \ -with elevated privileges" % (src, dst)) - raise error - - def islink(path): - if not os.path.isdir(path): - return False - data = wintypes.WIN32_FIND_DATAW() - kernel32.FindClose(kernel32.FindFirstFileW(path, ctypes.byref(data))) - if not data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT: - return False - return IsReparseTagNameSurrogate(data.dwReserved0) - - def readlink(path): - n = wintypes.DWORD() - buf = (wintypes.BYTE * MAXIMUM_REPARSE_DATA_BUFFER_SIZE)() - flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS - handle = kernel32.CreateFileW(path, FILE_READ_ATTRIBUTES, 0, None, - OPEN_EXISTING, flags, None) - try: - kernel32.DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, None, 0, - buf, ctypes.sizeof(buf), ctypes.byref(n), None) - finally: - kernel32.CloseHandle(handle) - rb = REPARSE_DATA_BUFFER.from_buffer(buf) - tag = rb.ReparseTag - if tag == IO_REPARSE_TAG_SYMLINK: - return rb.SymbolicLinkReparseBuffer.PrintName - if tag == IO_REPARSE_TAG_MOUNT_POINT: - return rb.MountPointReparseBuffer.PrintName - if not IsReparseTagNameSurrogate(tag): - raise ValueError("not a link") - raise ValueError("unsupported reparse tag: %d" % tag) - - def unlink(path): - return os.rmdir(path) 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 de6736096c..b2ba0c22c3 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, List import collections import io @@ -7,6 +7,7 @@ import os import re import subprocess import sys +from pathlib import Path from esphome import const @@ -23,11 +24,13 @@ class RegistryEntry: @property def coroutine_fun(self): from esphome.core import coroutine + return coroutine(self.fun) @property def schema(self): from esphome.config_validation import Schema + return Schema(self.raw_schema) @@ -59,7 +62,7 @@ def safe_print(message=""): if CORE.dashboard: try: - message = message.replace('\033', '\\033') + message = message.replace("\033", "\\033") except UnicodeEncodeError: pass @@ -70,10 +73,10 @@ def safe_print(message=""): pass try: - print(message.encode('utf-8', 'backslashreplace')) + print(message.encode("utf-8", "backslashreplace")) except UnicodeEncodeError: try: - print(message.encode('ascii', 'backslashreplace')) + print(message.encode("ascii", "backslashreplace")) except UnicodeEncodeError: print("Cannot print line because of invalid locale!") @@ -81,13 +84,13 @@ def safe_print(message=""): def shlex_quote(s): if not s: return "''" - if re.search(r'[^\w@%+=:,./-]', s) is None: + if re.search(r"[^\w@%+=:,./-]", s) is None: return s return "'" + s.replace("'", "'\"'\"'") + "'" -ANSI_ESCAPE = re.compile(r'\033[@-_][0-?]*[ -/]*[@-~]') +ANSI_ESCAPE = re.compile(r"\033[@-_][0-?]*[ -/]*[@-~]") class RedirectText: @@ -96,9 +99,9 @@ class RedirectText: if filter_lines is None: self._filter_pattern = None else: - pattern = r'|'.join(r'(?:' + pattern + r')' for pattern in filter_lines) + pattern = r"|".join(r"(?:" + pattern + r")" for pattern in filter_lines) self._filter_pattern = re.compile(pattern) - self._line_buffer = '' + self._line_buffer = "" def __getattr__(self, item): return getattr(self._out, item) @@ -111,7 +114,7 @@ class RedirectText: # work. The shell we create in the dashboard is not a tty, so python removes # all color codes from the resulting stream. We just convert them to something # we can easily recognize later here. - s = s.replace('\033', '\\033') + s = s.replace("\033", "\\033") self._out.write(s) def write(self, s): @@ -127,13 +130,13 @@ class RedirectText: self._line_buffer += s lines = self._line_buffer.splitlines(True) for line in lines: - if '\n' not in line and '\r' not in line: + if "\n" not in line and "\r" not in line: # Not a complete line, set line buffer self._line_buffer = line break - self._line_buffer = '' + self._line_buffer = "" - line_without_ansi = ANSI_ESCAPE.sub('', line) + line_without_ansi = ANSI_ESCAPE.sub("", line) line_without_end = line_without_ansi.rstrip() if self._filter_pattern.match(line_without_end) is not None: # Filter pattern matched, ignore the line @@ -153,9 +156,9 @@ class RedirectText: return True -def run_external_command(func, *cmd, - capture_stdout: bool = False, - filter_lines: str = None) -> Union[int, str]: +def run_external_command( + func, *cmd, capture_stdout: bool = False, filter_lines: str = None +) -> Union[int, str]: """ Run a function from an external package that acts like a main method. @@ -168,13 +171,14 @@ def run_external_command(func, *cmd, :return: str if `capture_stdout` is set else int exit code. """ + def mock_exit(return_code): raise SystemExit(return_code) orig_argv = sys.argv orig_exit = sys.exit # mock sys.exit - full_cmd = ' '.join(shlex_quote(x) for x in cmd) - _LOGGER.info("Running: %s", full_cmd) + full_cmd = " ".join(shlex_quote(x) for x in cmd) + _LOGGER.debug("Running: %s", full_cmd) orig_stdout = sys.stdout sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) @@ -188,8 +192,8 @@ def run_external_command(func, *cmd, sys.argv = list(cmd) sys.exit = mock_exit return func() or 0 - except KeyboardInterrupt: - return 1 + except KeyboardInterrupt: # pylint: disable=try-except-raise + raise except SystemExit as err: return err.args[0] except Exception as err: # pylint: disable=broad-except @@ -209,34 +213,33 @@ def run_external_command(func, *cmd, def run_external_process(*cmd, **kwargs): - full_cmd = ' '.join(shlex_quote(x) for x in cmd) - _LOGGER.info("Running: %s", full_cmd) - filter_lines = kwargs.get('filter_lines') + full_cmd = " ".join(shlex_quote(x) for x in cmd) + _LOGGER.debug("Running: %s", full_cmd) + filter_lines = kwargs.get("filter_lines") - capture_stdout = kwargs.get('capture_stdout', False) + capture_stdout = kwargs.get("capture_stdout", False) if capture_stdout: - sub_stdout = io.BytesIO() + sub_stdout = subprocess.PIPE else: sub_stdout = RedirectText(sys.stdout, filter_lines=filter_lines) sub_stderr = RedirectText(sys.stderr, filter_lines=filter_lines) try: - return subprocess.call(cmd, - stdout=sub_stdout, - stderr=sub_stderr) + proc = subprocess.run( + cmd, stdout=sub_stdout, stderr=sub_stderr, encoding="utf-8", check=False + ) + return proc.stdout if capture_stdout else proc.returncode + except KeyboardInterrupt: # pylint: disable=try-except-raise + raise except Exception as err: # pylint: disable=broad-except _LOGGER.error("Running command failed: %s", err) _LOGGER.error("Please try running %s locally.", full_cmd) return 1 - finally: - if capture_stdout: - # pylint: disable=lost-exception - return sub_stdout.getvalue() def is_dev_esphome_version(): - return 'dev' in const.__version__ + return "dev" in const.__version__ # Custom OrderedDict with nicer repr method for debugging @@ -245,14 +248,53 @@ 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: + def __init__(self, path: str, description: str): + self.path = path + self.description = description + + +# from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py +def get_serial_ports() -> List[SerialPort]: + from serial.tools.list_ports import comports + + result = [] + for port, desc, info in comports(include_links=True): + if not port: + continue + if "VID:PID" in info: + result.append(SerialPort(path=port, description=desc)) + # Also add objects in /dev/serial/by-id/ + # ref: https://github.com/esphome/issues/issues/1346 + + by_id_path = Path("/dev/serial/by-id") + if sys.platform.lower().startswith("linux") and by_id_path.exists(): + from serial.tools.list_ports_linux import SysFS + + for path in by_id_path.glob("*"): + device = SysFS(path) + if device.subsystem == "platform": + result.append(SerialPort(path=str(path), description=info[1])) + + result.sort(key=lambda x: x.path) + return result diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 8193c1317c..0fdae423cf 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -2,11 +2,12 @@ import difflib import itertools import voluptuous as vol +from esphome.jsonschema import jschema_extended class ExtraKeysInvalid(vol.Invalid): def __init__(self, *arg, **kwargs): - self.candidates = kwargs.pop('candidates') + self.candidates = kwargs.pop("candidates") vol.Invalid.__init__(self, *arg, **kwargs) @@ -19,7 +20,10 @@ def ensure_multiple_invalid(err): # pylint: disable=protected-access, unidiomatic-typecheck class _Schema(vol.Schema): """Custom cv.Schema that prints similar keys on error.""" - def __init__(self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None): + + def __init__( + self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None + ): super().__init__(schema, required=required, extra=extra) # List of extra schemas to apply after validation # Should be used sparingly, as it's not a very voluptuous-way/clean way of @@ -32,11 +36,12 @@ class _Schema(vol.Schema): try: res = extra(res) except vol.Invalid as err: + # pylint: disable=raise-missing-from raise ensure_multiple_invalid(err) return res def _compile_mapping(self, schema, invalid_msg=None): - invalid_msg = invalid_msg or 'mapping value' + invalid_msg = invalid_msg or "mapping value" # Check some things that ESPHome's schemas do not allow # mostly to keep the logic in this method sane (so these may be re-added if needed). @@ -46,13 +51,16 @@ class _Schema(vol.Schema): if isinstance(key, vol.Remove): raise ValueError("ESPHome does not allow vol.Remove") if isinstance(key, vol.primitive_types): - raise ValueError("All schema keys must be wrapped in cv.Required or cv.Optional") + raise ValueError( + "All schema keys must be wrapped in cv.Required or cv.Optional" + ) # Keys that may be required all_required_keys = {key for key in schema if isinstance(key, vol.Required)} # Keys that may have defaults - all_default_keys = {key for key in schema if isinstance(key, vol.Optional)} + # This is a list because sets do not guarantee insertion order + all_default_keys = [key for key in schema if isinstance(key, vol.Optional)] # Recursively compile schema _compiled_schema = {} @@ -62,7 +70,9 @@ class _Schema(vol.Schema): _compiled_schema[skey] = (new_key, new_value) # Sort compiled schema (probably not necessary for esphome, but leave it here just in case) - candidates = list(vol.schema_builder._iterate_mapping_candidates(_compiled_schema)) + candidates = list( + vol.schema_builder._iterate_mapping_candidates(_compiled_schema) + ) # After we have the list of candidates in the correct order, we want to apply some # optimization so that each @@ -73,8 +83,13 @@ class _Schema(vol.Schema): for skey, (ckey, cvalue) in candidates: if type(skey) in vol.primitive_types: candidates_by_key.setdefault(skey, []).append((skey, (ckey, cvalue))) - elif isinstance(skey, vol.Marker) and type(skey.schema) in vol.primitive_types: - candidates_by_key.setdefault(skey.schema, []).append((skey, (ckey, cvalue))) + elif ( + isinstance(skey, vol.Marker) + and type(skey.schema) in vol.primitive_types + ): + candidates_by_key.setdefault(skey.schema, []).append( + (skey, (ckey, cvalue)) + ) else: # These are wildcards such as 'int', 'str', 'Remove' and others which should be # applied to all keys @@ -99,7 +114,10 @@ class _Schema(vol.Schema): # Insert default values for non-existing keys. for key in all_default_keys: - if not isinstance(key.default, vol.Undefined) and key.schema not in key_value_map: + if ( + not isinstance(key.default, vol.Undefined) + and key.schema not in key_value_map + ): # A default value has been specified for this missing key, insert it. key_value_map[key.schema] = key.default() @@ -108,8 +126,9 @@ class _Schema(vol.Schema): for key, value in key_value_map.items(): key_path = path + [key] # Optimization. Validate against the matching key first, then fallback to the rest - relevant_candidates = itertools.chain(candidates_by_key.get(key, []), - additional_candidates) + relevant_candidates = itertools.chain( + candidates_by_key.get(key, []), additional_candidates + ) # compare each given key/value against all compiled key/values # schema key, (compiled key, compiled value) @@ -156,14 +175,21 @@ class _Schema(vol.Schema): elif self.extra != vol.REMOVE_EXTRA: if isinstance(key, str) and key_names: matches = difflib.get_close_matches(key, key_names) - errors.append(ExtraKeysInvalid('extra keys not allowed', key_path, - candidates=matches)) + errors.append( + ExtraKeysInvalid( + "extra keys not allowed", + key_path, + candidates=matches, + ) + ) else: - errors.append(vol.Invalid('extra keys not allowed', key_path)) + errors.append( + vol.Invalid("extra keys not allowed", key_path) + ) # for any required keys left that weren't found and don't have defaults: for key in required_keys: - msg = getattr(key, 'msg', None) or 'required key not provided' + msg = getattr(key, "msg", None) or "required key not provided" errors.append(vol.RequiredFieldInvalid(msg, path + [key])) if errors: raise vol.MultipleInvalid(errors) @@ -177,9 +203,10 @@ class _Schema(vol.Schema): self._extra_schemas.append(validator) return self - # pylint: disable=arguments-differ + @jschema_extended + # pylint: disable=signature-differs def extend(self, *schemas, **kwargs): - extra = kwargs.pop('extra', None) + extra = kwargs.pop("extra", None) if kwargs: raise ValueError if not schemas: diff --git a/esphome/vscode.py b/esphome/vscode.py index e8c0b106f7..68d59abd02 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -20,11 +20,11 @@ def _dump_range(range): if range is None: return None return { - 'document': range.start_mark.document, - 'start_line': range.start_mark.line, - 'start_col': range.start_mark.column, - 'end_line': range.end_mark.line, - 'end_col': range.end_mark.column, + "document": range.start_mark.document, + "start_line": range.start_mark.line, + "start_col": range.start_mark.column, + "end_line": range.end_mark.line, + "end_col": range.end_mark.column, } @@ -34,39 +34,45 @@ class VSCodeResult: self.validation_errors = [] def dump(self): - return json.dumps({ - 'type': 'result', - 'yaml_errors': self.yaml_errors, - 'validation_errors': self.validation_errors, - }) + return json.dumps( + { + "type": "result", + "yaml_errors": self.yaml_errors, + "validation_errors": self.validation_errors, + } + ) def add_yaml_error(self, message): - self.yaml_errors.append({ - 'message': message, - }) + self.yaml_errors.append( + { + "message": message, + } + ) def add_validation_error(self, range_, message): - self.validation_errors.append({ - 'range': _dump_range(range_), - 'message': message, - }) + self.validation_errors.append( + { + "range": _dump_range(range_), + "message": message, + } + ) def read_config(args): while True: CORE.reset() data = json.loads(input()) - assert data['type'] == 'validate' + assert data["type"] == "validate" CORE.vscode = True CORE.ace = args.ace - f = data['file'] + 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'] + CORE.config_path = data["file"] vs = VSCodeResult() try: - res = load_config() + res = load_config(dict(args.substitution) if args.substitution else {}) except Exception as err: # pylint: disable=broad-except vs.add_yaml_error(str(err)) else: diff --git a/esphome/wizard.py b/esphome/wizard.py index b00f6d2b01..c64ad3a583 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -6,11 +6,13 @@ import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.helpers import color, get_bool_env, write_file +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 CORE_BIG = r""" _____ ____ _____ ______ / ____/ __ \| __ \| ____| @@ -43,20 +45,9 @@ OTA_BIG = r""" ____ _______ BASE_CONFIG = """esphome: name: {name} - platform: {platform} - board: {board} - -wifi: - ssid: "{ssid}" - password: "{psk}" - - # Enable fallback hotspot (captive portal) in case wifi connection fails - ap: - ssid: "{fallback_name}" - password: "{fallback_psk}" - -captive_portal: +""" +LOGGER_API_CONFIG = """ # Enable logging logger: @@ -64,51 +55,111 @@ logger: api: """ +ESP8266_CONFIG = """ +esp8266: + board: {board} +""" + +ESP32_CONFIG = """ +esp32: + board: {board} + framework: + type: arduino +""" + def sanitize_double_quotes(value): - return value.replace('\\', '\\\\').replace('"', '\\"') + return value.replace("\\", "\\\\").replace('"', '\\"') def wizard_file(**kwargs): letters = string.ascii_letters + string.digits - ap_name_base = kwargs['name'].replace('_', ' ').title() + ap_name_base = kwargs["name"].replace("_", " ").title() ap_name = f"{ap_name_base} Fallback Hotspot" if len(ap_name) > 32: ap_name = ap_name_base - kwargs['fallback_name'] = ap_name - kwargs['fallback_psk'] = ''.join(random.choice(letters) for _ in range(12)) + kwargs["fallback_name"] = ap_name + kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) config = BASE_CONFIG.format(**kwargs) - if kwargs['password']: - config += ' password: "{0}"\n\nota:\n password: "{0}"\n'.format(kwargs['password']) + config += ( + ESP8266_CONFIG.format(**kwargs) + if kwargs["platform"] == "ESP8266" + else ESP32_CONFIG.format(**kwargs) + ) + + config += LOGGER_API_CONFIG + + # Configure API + if "password" in kwargs: + config += f" password: \"{kwargs['password']}\"\n" + + # Configure OTA + config += "\nota:\n" + if "ota_password" in kwargs: + config += f" password: \"{kwargs['ota_password']}\"" + elif "password" in kwargs: + config += f" password: \"{kwargs['password']}\"" + + # Configuring wifi + config += "\n\nwifi:\n" + + if "ssid" in kwargs: + if kwargs["ssid"].startswith("!secret"): + template = " ssid: {ssid}\n password: {psk}\n" + else: + template = """ ssid: "{ssid}"\n password: "{psk}"\n""" + config += template.format(**kwargs) else: - config += "\nota:\n" + config += """ # ssid: "My SSID" + # password: "mypassword" + + networks: +""" + + # pylint: disable=consider-using-f-string + config += """ + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "{fallback_name}" + password: "{fallback_psk}" + +captive_portal: +""".format( + **kwargs + ) return config def wizard_write(path, **kwargs): - name = kwargs['name'] - board = kwargs['board'] + from esphome.components.esp8266 import boards as esp8266_boards - kwargs['ssid'] = sanitize_double_quotes(kwargs['ssid']) - kwargs['psk'] = sanitize_double_quotes(kwargs['psk']) - kwargs['password'] = sanitize_double_quotes(kwargs['password']) + name = kwargs["name"] + board = kwargs["board"] - if 'platform' not in kwargs: - kwargs['platform'] = 'ESP8266' if board in ESP8266_BOARD_PINS else 'ESP32' - platform = kwargs['platform'] + for key in ("ssid", "psk", "password", "ota_password"): + if key in kwargs: + kwargs[key] = sanitize_double_quotes(kwargs[key]) + + if "platform" not in kwargs: + 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) -if get_bool_env('ESPHOME_QUICKWIZARD'): +if get_bool_env(ENV_QUICKWIZARD): + def sleep(time): pass + else: from time import sleep @@ -130,162 +181,213 @@ def default_input(text, default): # From https://stackoverflow.com/a/518232/8924614 def strip_accents(value): - return ''.join(c for c in unicodedata.normalize('NFD', str(value)) - if unicodedata.category(c) != 'Mn') + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) def wizard(path): - 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('cyan', 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( + 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('cyan', path))) - return 1 + safe_print( + 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!") sleep(1.5) safe_print("I'm the wizard of ESPHome :)") sleep(1.25) safe_print("And I'm here to help you get started with ESPHome.") sleep(2.0) - safe_print("In 4 steps I'm going to guide you through creating a basic " - "configuration file for your custom ESP8266/ESP32 firmware. Yay!") + safe_print( + "In 4 steps I'm going to guide you through creating a basic " + "configuration file for your custom ESP8266/ESP32 firmware. Yay!" + ) sleep(3.0) safe_print() safe_print_step(1, CORE_BIG) - safe_print("First up, please choose a " + color('green', 'name') + " for your node.") - safe_print("It should be a unique name that can be used to identify the device later.") + 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('bold_white', "livingroom"))) + safe_print( + f"For example, I like calling the node in my living room {color(Fore.BOLD_WHITE, 'livingroom')}." + ) safe_print() sleep(1) - name = input(color("bold_white", "(name): ")) + name = input(color(Fore.BOLD_WHITE, "(name): ")) + while True: try: name = cv.valid_name(name) break except vol.Invalid: - safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include " - "numbers, letters and underscores.".format(name))) - name = strip_accents(name).replace(' ', '_') - name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) - safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name))) + safe_print( + color( + Fore.RED, + f'Oh noes, "{name}" isn\'t a valid name. Names can only ' + f"include numbers, lower-case letters and hyphens. ", + ) + ) + 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(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('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("Now I'd like to know what microcontroller you're using so that I can compile " - "firmwares for it.") - safe_print("Are you using an " + color('green', 'ESP32') + " or " + - color('green', 'ESP8266') + " platform? (Choose ESP8266 for Sonoff devices)") + safe_print( + "Now I'd like to know what microcontroller you're using so that I can compile " + "firmwares for it." + ) + safe_print( + 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) safe_print() safe_print("Please enter either ESP32 or ESP8266.") - platform = input(color("bold_white", "(ESP32/ESP8266): ")) + platform = input(color(Fore.BOLD_WHITE, "(ESP32/ESP8266): ")) try: - platform = vol.All(vol.Upper, vol.Any('ESP32', 'ESP8266'))(platform) + platform = vol.All(vol.Upper, vol.Any("ESP32", "ESP8266"))(platform) break except vol.Invalid: - safe_print("Unfortunately, I can't find an espressif microcontroller called " - "\"{}\". Please try again.".format(platform)) - safe_print("Thanks! You've chosen {} as your platform.".format(color('cyan', platform))) + safe_print( + f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.' + ) + safe_print(f"Thanks! You've chosen {color(Fore.CYAN, platform)} as your platform.") safe_print() sleep(1) - if platform == 'ESP32': - board_link = 'http://docs.platformio.org/en/latest/platforms/espressif32.html#boards' + if platform == "ESP32": + board_link = ( + "http://docs.platformio.org/en/latest/platforms/espressif32.html#boards" + ) else: - board_link = 'http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards' + board_link = ( + "http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" + ) - safe_print("Next, I need to know what " + color('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('green', board_link))) - if platform == 'ESP32': - safe_print("(Type " + color('green', 'esp01_1m') + " for Sonoff devices)") + safe_print(f"Please go to {color(Fore.GREEN, board_link)} and choose a board.") + if platform == "ESP32": + 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("bold_white", 'nodemcu-32s'))) - boards = list(ESP32_BOARD_PINS.keys()) + if platform == "ESP32": + 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("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("bold_white", "(board): ")) + board = input(color(Fore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: - safe_print(color('red', f"Sorry, I don't think the board \"{board}\" exists.")) + safe_print( + color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.') + ) safe_print() sleep(0.25) safe_print() - safe_print("Way to go! You've chosen {} as your board.".format(color('cyan', board))) + safe_print(f"Way to go! You've chosen {color(Fore.CYAN, board)} as your board.") safe_print() sleep(1) safe_print_step(3, WIFI_BIG) - safe_print("In this step, I'm going to create the configuration for " - "WiFi.") + safe_print("In this step, I'm going to create the configuration for " "WiFi.") safe_print() sleep(1) - safe_print("First, what's the " + color('green', 'SSID') + - f" (the name) of the WiFi network {name} I should connect to?") + safe_print( + f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" + ) sleep(1.5) - safe_print("For example \"{}\".".format(color('bold_white', "Abraham Linksys"))) + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".") while True: - ssid = input(color('bold_white', "(ssid): ")) + ssid = input(color(Fore.BOLD_WHITE, "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: - safe_print(color('red', "Unfortunately, \"{}\" doesn't seem to be a valid SSID. " - "Please try again.".format(ssid))) + safe_print( + color( + Fore.RED, + f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.', + ) + ) safe_print() sleep(1) - safe_print("Thank you very much! You've just chosen \"{}\" as your SSID." - "".format(color('cyan', ssid))) + safe_print( + f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.' + ) safe_print() sleep(0.75) - safe_print("Now please state the " + color('green', 'password') + - " of the WiFi network so that I can connect to it (Leave empty for no password)") + safe_print( + f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" + ) safe_print() - safe_print("For example \"{}\"".format(color('bold_white', 'PASSWORD42'))) + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"") sleep(0.5) - psk = input(color('bold_white', '(PSK): ')) - safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).") + psk = input(color(Fore.BOLD_WHITE, "(PSK): ")) + safe_print( + "Perfect! WiFi is now set up (you can create static IPs and so on later)." + ) sleep(1.5) safe_print_step(4, OTA_BIG) - safe_print("Almost there! ESPHome can automatically upload custom firmwares over WiFi " - "(over the air) and integrates into Home Assistant with a native API.") - safe_print("This can be insecure if you do not trust the WiFi network. Do you want to set " - "a " + color('green', 'password') + " for connecting to this ESP?") + safe_print( + "Almost there! ESPHome can automatically upload custom firmwares over WiFi " + "(over the air) and integrates into Home Assistant with a native API." + ) + safe_print( + f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?" + ) safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = input(color('bold_white', '(password): ')) + password = input(color(Fore.BOLD_WHITE, "(password): ")) - wizard_write(path=path, name=name, platform=platform, board=board, - ssid=ssid, psk=psk, password=password) + wizard_write( + path=path, + name=name, + platform=platform, + board=board, + ssid=ssid, + psk=psk, + password=password, + ) safe_print() - safe_print(color('cyan', "DONE! I've now written a new configuration file to ") + - color('bold_cyan', path)) + safe_print( + color(Fore.CYAN, "DONE! I've now written a new configuration file to ") + + color(Fore.BOLD_CYAN, path) + ) safe_print() safe_print("Next steps:") - safe_print(" > Check your Home Assistant \"integrations\" screen. If all goes well, you " - "should see your ESP being discovered automatically.") - safe_print(" > Then follow the rest of the getting started guide:") - safe_print(" > https://esphome.io/guides/getting_started_command_line.html") + safe_print(" > Follow the rest of the getting started guide:") + safe_print( + " > https://esphome.io/guides/getting_started_command_line.html#adding-some-features" + ) + safe_print(" > to learn how to customize ESPHome and install it to your device.") return 0 diff --git a/esphome/writer.py b/esphome/writer.py index 67b1332e8f..8963572752 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,54 +1,70 @@ import logging import os import re +from pathlib import Path +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__ +from esphome.const import ( + HEADER_FILE_EXTENSIONS, + SOURCE_FILE_EXTENSIONS, + __version__, + ENV_NOGITIGNORE, +) from esphome.core import CORE, EsphomeError -from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files, \ - copy_file_if_changed +from esphome.helpers import ( + mkdir_p, + read_file, + write_file_if_changed, + walk_files, + copy_file_if_changed, + get_bool_env, +) from esphome.storage_json import StorageJSON, storage_path +from esphome import loader _LOGGER = logging.getLogger(__name__) -CPP_AUTO_GENERATE_BEGIN = '// ========== AUTO GENERATED CODE BEGIN ===========' -CPP_AUTO_GENERATE_END = '// =========== AUTO GENERATED CODE END ============' -CPP_INCLUDE_BEGIN = '// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========' -CPP_INCLUDE_END = '// ========== AUTO GENERATED INCLUDE BLOCK END ===========' -INI_AUTO_GENERATE_BEGIN = '; ========== AUTO GENERATED CODE BEGIN ===========' -INI_AUTO_GENERATE_END = '; =========== AUTO GENERATED CODE END ============' +CPP_AUTO_GENERATE_BEGIN = "// ========== AUTO GENERATED CODE BEGIN ===========" +CPP_AUTO_GENERATE_END = "// =========== AUTO GENERATED CODE END ============" +CPP_INCLUDE_BEGIN = "// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========" +CPP_INCLUDE_END = "// ========== AUTO GENERATED INCLUDE BLOCK END ===========" +INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ===========" +INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============" -CPP_BASE_FORMAT = ("""// Auto generated code by esphome -""", """" +CPP_BASE_FORMAT = ( + """// Auto generated code by esphome +""", + """" void setup() { - // ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== - """, """ - // ========= YOU CAN EDIT AFTER THIS LINE ========= + """, + """ App.setup(); } void loop() { App.loop(); } -""") +""", +) -INI_BASE_FORMAT = ("""; Auto generated code by esphome +INI_BASE_FORMAT = ( + """; Auto generated code by esphome [common] lib_deps = build_flags = upload_flags = -; ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== -""", """ -; ========= YOU CAN EDIT AFTER THIS LINE ========= +""", + """ -""") +""", +) UPLOAD_SPEED_OVERRIDE = { - 'esp210': 57600, + "esp210": 57600, } @@ -60,10 +76,9 @@ def get_flags(key): def get_include_text(): - include_text = '#include "esphome.h"\n' \ - 'using namespace esphome;\n' + include_text = '#include "esphome.h"\nusing namespace esphome;\n' for _, component, conf in iter_components(CORE.config): - if not hasattr(component, 'includes'): + if not hasattr(component, "includes"): continue includes = component.includes if callable(includes): @@ -71,10 +86,10 @@ def get_include_text(): if includes is None: continue if isinstance(includes, list): - includes = '\n'.join(includes) + includes = "\n".join(includes) if not includes: continue - include_text += includes + '\n' + include_text += f"{includes}\n" return include_text @@ -83,58 +98,12 @@ def replace_file_content(text, pattern, repl): return content_new, count -def migrate_src_version_0_to_1(): - main_cpp = CORE.relative_build_path('src', 'main.cpp') - if not os.path.isfile(main_cpp): - return - - content = read_file(main_cpp) - - if CPP_INCLUDE_BEGIN in content: - return - - content, count = replace_file_content(content, r'\s*delay\((?:16|20)\);', '') - if count != 0: - _LOGGER.info("Migration: Removed %s occurrence of 'delay(16);' in %s", count, main_cpp) - - content, count = replace_file_content(content, r'using namespace esphomelib;', '') - if count != 0: - _LOGGER.info("Migration: Removed %s occurrence of 'using namespace esphomelib;' " - "in %s", count, main_cpp) - - if CPP_INCLUDE_BEGIN not in content: - content, count = replace_file_content(content, r'#include "esphomelib/application.h"', - CPP_INCLUDE_BEGIN + '\n' + CPP_INCLUDE_END) - if count == 0: - _LOGGER.error("Migration failed. ESPHome 1.10.0 needs to have a new auto-generated " - "include section in the %s file. Please remove %s and let it be " - "auto-generated again.", main_cpp, main_cpp) - _LOGGER.info("Migration: Added include section to %s", main_cpp) - - write_file_if_changed(main_cpp, content) - - -def migrate_src_version(old, new): - if old == new: - return - if old > new: - _LOGGER.warning("The source version rolled backwards! Ignoring.") - return - - if old == 0: - migrate_src_version_0_to_1() - - def storage_should_clean(old, new): # type: (StorageJSON, StorageJSON) -> bool if old is None: return True 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 @@ -147,9 +116,6 @@ def update_storage_json(): if old == new: return - old_src_version = old.src_version if old is not None else 0 - migrate_src_version(old_src_version, new.src_version) - if storage_should_clean(old, new): _LOGGER.info("Core config or version changed, cleaning build files...") clean_build() @@ -157,65 +123,27 @@ def update_storage_json(): new.save(path) -def format_ini(data): - content = '' +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)): - content += f'{key} =\n' + if isinstance(value, list): + content += f"{key} =\n" for x in value: - content += f' {x}\n' + content += f" {x}\n" else: - content += f'{key} = {value}\n' + content += f"{key} = {value}\n" 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))) - - def get_ini_content(): - lib_deps = gather_lib_deps() - build_flags = gather_build_flags() + 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)) - 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') - if not os.path.isfile(partitions_csv): - with open(partitions_csv, "w") as f: - f.write("nvs, data, nvs, 0x009000, 0x005000,\n") - f.write("otadata, data, ota, 0x00e000, 0x002000,\n") - f.write("app0, app, ota_0, 0x010000, 0x190000,\n") - f.write("app1, app, ota_1, 0x200000, 0x190000,\n") - f.write("eeprom, data, 0x99, 0x390000, 0x001000,\n") - f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n") - - 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 - - # 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, {})) - - content = f'[env:{CORE.name}]\n' - content += format_ini(data) + content = f"[env:{CORE.name}]\n" + content += format_ini(CORE.platformio_options) return content @@ -223,32 +151,42 @@ def get_ini_content(): def find_begin_end(text, begin_s, end_s): begin_index = text.find(begin_s) if begin_index == -1: - raise EsphomeError("Could not find auto generated code begin in file, either " - "delete the main sketch file or insert the comment again.") + raise EsphomeError( + "Could not find auto generated code begin in file, either " + "delete the main sketch file or insert the comment again." + ) if text.find(begin_s, begin_index + 1) != -1: - raise EsphomeError("Found multiple auto generate code begins, don't know " - "which to chose, please remove one of them.") + raise EsphomeError( + "Found multiple auto generate code begins, don't know " + "which to chose, please remove one of them." + ) end_index = text.find(end_s) if end_index == -1: - raise EsphomeError("Could not find auto generated code end in file, either " - "delete the main sketch file or insert the comment again.") + raise EsphomeError( + "Could not find auto generated code end in file, either " + "delete the main sketch file or insert the comment again." + ) if text.find(end_s, end_index + 1) != -1: - raise EsphomeError("Found multiple auto generate code endings, don't know " - "which to chose, please remove one of them.") + raise EsphomeError( + "Found multiple auto generate code endings, don't know " + "which to chose, please remove one of them." + ) - return text[:begin_index], text[(end_index + len(end_s)):] + return text[:begin_index], text[(end_index + len(end_s)) :] def write_platformio_ini(content): update_storage_json() - path = CORE.relative_build_path('platformio.ini') + path = CORE.relative_build_path("platformio.ini") if os.path.isfile(path): text = read_file(path) - content_format = find_begin_end(text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END) + content_format = find_begin_end( + text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END + ) 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) @@ -257,7 +195,8 @@ def write_platformio_project(): mkdir_p(CORE.build_path) content = get_ini_content() - write_gitignore() + if not get_bool_env(ENV_NOGITIGNORE): + write_gitignore() write_platformio_ini(content) @@ -267,86 +206,116 @@ 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' +DEFINES_H_TARGET = "esphome/core/defines.h" +VERSION_H_TARGET = "esphome/core/version.h" ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY -ESPHome automatically populates the esphome/ directory, and any +ESPHome automatically populates the build directory, and any changes to this directory will be removed the next time esphome is run. -For modifying esphome's core files, please use a development esphome install -or use the custom_components folder. +For modifying esphome's core files, please use a development esphome install, +the custom_components folder or the external_components feature. """ def copy_src_tree(): - source_files = {} + 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 include_l = [] - for target, path in source_files_l: - if os.path.splitext(path)[1] in HEADER_FILE_EXTENSIONS: + for target, _ in source_files_l: + if target.suffix in HEADER_FILE_EXTENSIONS: include_l.append(f'#include "{target}"') - include_l.append('') - include_s = '\n'.join(include_l) + include_l.append("") + include_s = "\n".join(include_l) - source_files_copy = source_files.copy() - source_files_copy.pop(DEFINES_H_TARGET) + 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) - for path in walk_files(CORE.relative_src_path('esphome')): - if os.path.splitext(path)[1] not in SOURCE_FILE_EXTENSIONS: + for fname in walk_files(CORE.relative_src_path("esphome")): + p = Path(fname) + if p.suffix not in SOURCE_FILE_EXTENSIONS: # Not a source file, ignore continue # Transform path to target path name - target = os.path.relpath(path, CORE.relative_src_path()).replace(os.path.sep, '/') - if target in (DEFINES_H_TARGET, VERSION_H_TARGET): + target = p.relative_to(CORE.relative_src_path()) + if target in ignore_targets: # Ignore defines.h, will be dealt with later continue if target not in source_files_copy: # Source file removed, delete target - os.remove(path) + p.unlink() else: - src_path = source_files_copy.pop(target) - copy_file_if_changed(src_path, path) + src_file = source_files_copy.pop(target) + with src_file.path() as src_path: + copy_file_if_changed(src_path, p) # Now copy new files - for target, src_path in source_files_copy.items(): - dst_path = CORE.relative_src_path(*target.split('/')) - copy_file_if_changed(src_path, dst_path) + for target, src_file in source_files_copy.items(): + dst_path = CORE.relative_src_path(*target.parts) + with src_file.path() as src_path: + copy_file_if_changed(src_path, dst_path) # Finally copy defines - write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'defines.h'), - generate_defines_h()) - write_file_if_changed(CORE.relative_src_path('esphome', 'README.txt'), - ESPHOME_README_TXT) - write_file_if_changed(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__)) + write_file_if_changed( + CORE.relative_src_path("esphome", "core", "defines.h"), generate_defines_h() + ) + write_file_if_changed(CORE.relative_build_path("README.txt"), ESPHOME_README_TXT) + write_file_if_changed( + CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s) + ) + write_file_if_changed( + 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] define_content_l.sort() - return DEFINES_H_FORMAT.format('\n'.join(define_content_l)) + 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') + path = CORE.relative_src_path("main.cpp") if os.path.isfile(path): text = read_file(path) - code_format = find_begin_end(text, CPP_AUTO_GENERATE_BEGIN, CPP_AUTO_GENERATE_END) - code_format_ = find_begin_end(code_format[0], CPP_INCLUDE_BEGIN, CPP_INCLUDE_END) + code_format = find_begin_end( + text, CPP_AUTO_GENERATE_BEGIN, CPP_AUTO_GENERATE_END + ) + code_format_ = find_begin_end( + code_format[0], CPP_INCLUDE_BEGIN, CPP_INCLUDE_END + ) code_format = (code_format_[0], code_format_[1], code_format[1]) else: code_format = CPP_BASE_FORMAT @@ -355,8 +324,10 @@ 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 += code_format[1] + CPP_AUTO_GENERATE_BEGIN + '\n' + code_s + CPP_AUTO_GENERATE_END + full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}" + full_file += ( + 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) @@ -378,17 +349,12 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome # This is an example and may include too much for your use-case. # You can modify this file to suit your needs. /.esphome/ -**/.pioenvs/ -**/.piolibdeps/ -**/lib/ -**/src/ -**/platformio.ini /secrets.yaml """ def write_gitignore(): - path = CORE.relative_config_path('.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 053fba6274..57009be57e 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -11,7 +11,14 @@ import yaml.constructor from esphome import core from esphome.config_helpers import read_config_file -from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange +from esphome.core import ( + EsphomeError, + IPAddress, + Lambda, + MACAddress, + TimePeriod, + DocumentRange, +) from esphome.helpers import add_class_to_obj from esphome.util import OrderedDict, filter_yaml_files @@ -20,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) # Mostly copied from Home Assistant because that code works fine and # let's not reinvent the wheel here -SECRET_YAML = 'secrets.yaml' +SECRET_YAML = "secrets.yaml" _SECRET_CACHE = {} _SECRET_VALUES = {} @@ -28,20 +35,35 @@ _SECRET_VALUES = {} class ESPHomeDataBase: @property def esp_range(self): - return getattr(self, '_esp_range', None) + return getattr(self, "_esp_range", None) + + @property + def content_offset(self): + return getattr(self, "_content_offset", 0) def from_node(self, node): # pylint: disable=attribute-defined-outside-init self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) + if isinstance(node, yaml.ScalarNode): + if node.style is not None and node.style in "|>": + self._content_offset = 1 + + def from_database(self, database): + # pylint: disable=attribute-defined-outside-init + self._esp_range = database.esp_range + self._content_offset = database.content_offset class ESPForceValue: pass -def make_data_base(value): +def make_data_base(value, from_database: ESPHomeDataBase = None): try: - return add_class_to_obj(value, ESPHomeDataBase) + value = add_class_to_obj(value, ESPHomeDataBase) + if from_database is not None: + value.from_database(from_database) + return value except TypeError: # Adding class failed, ignore error return value @@ -109,13 +131,13 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors for key_node, value_node in node.value: # merge key is '<<' - is_merge_key = key_node.tag == 'tag:yaml.org,2002:merge' + is_merge_key = key_node.tag == "tag:yaml.org,2002:merge" # key has no explicit tag set - is_default_tag = key_node.tag == 'tag:yaml.org,2002:value' + is_default_tag = key_node.tag == "tag:yaml.org,2002:value" if is_default_tag: # Default tag for mapping keys is string - key_node.tag = 'tag:yaml.org,2002:str' + key_node.tag = "tag:yaml.org,2002:str" if not is_merge_key: # base case, this is a simple key-value pair @@ -126,14 +148,21 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors try: hash(key) except TypeError: + # pylint: disable=raise-missing-from raise yaml.constructor.ConstructorError( - f'Invalid key "{key}" (not hashable)', key_node.start_mark) + f'Invalid key "{key}" (not hashable)', key_node.start_mark + ) + + key = make_data_base(str(key)) + key.from_node(key_node) # Check if it is a duplicate key if key in seen_keys: raise yaml.constructor.ConstructorError( - f'Duplicate key "{key}"', key_node.start_mark, - 'NOTE: Previous declaration here:', seen_keys[key], + f'Duplicate key "{key}"', + key_node.start_mark, + "NOTE: Previous declaration here:", + seen_keys[key], ) seen_keys[key] = key_node.start_mark @@ -152,15 +181,19 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors for item in value: if not isinstance(item, dict): raise yaml.constructor.ConstructorError( - "While constructing a mapping", node.start_mark, - "Expected a mapping for merging, but found {}".format(type(item)), - value_node.start_mark) + "While constructing a mapping", + node.start_mark, + f"Expected a mapping for merging, but found {type(item)}", + value_node.start_mark, + ) merge_pairs.extend(item.items()) else: 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)), value_node.start_mark) + "While constructing a mapping", + node.start_mark, + f"Expected a mapping or list of mappings for merging, but found {type(value)}", + value_node.start_mark, + ) if merge_pairs: # We found some merge keys along the way, merge them into base pairs @@ -191,7 +224,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors args = node.value.split() # Check for a default value if len(args) > 1: - return os.getenv(args[0], ' '.join(args[1:])) + return os.getenv(args[0], " ".join(args[1:])) if args[0] in os.environ: return os.environ[args[0]] raise yaml.MarkedYAMLError( @@ -222,12 +255,12 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_list(self, node): - files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) return [_load_yaml_internal(f) for f in files] @_add_data_ref def construct_include_dir_merge_list(self, node): - files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) merged_list = [] for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -237,7 +270,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_named(self, node): - files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) mapping = OrderedDict() for fname in files: filename = os.path.splitext(os.path.basename(fname))[0] @@ -246,7 +279,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_merge_named(self, node): - files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) mapping = OrderedDict() for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -264,29 +297,42 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return add_class_to_obj(obj, ESPForceValue) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:omap', ESPHomeLoader.construct_yaml_omap) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:str', ESPHomeLoader.construct_yaml_str) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:seq', ESPHomeLoader.construct_yaml_seq) -ESPHomeLoader.add_constructor('tag:yaml.org,2002:map', ESPHomeLoader.construct_yaml_map) -ESPHomeLoader.add_constructor('!env_var', ESPHomeLoader.construct_env_var) -ESPHomeLoader.add_constructor('!secret', ESPHomeLoader.construct_secret) -ESPHomeLoader.add_constructor('!include', ESPHomeLoader.construct_include) -ESPHomeLoader.add_constructor('!include_dir_list', ESPHomeLoader.construct_include_dir_list) -ESPHomeLoader.add_constructor('!include_dir_merge_list', - ESPHomeLoader.construct_include_dir_merge_list) -ESPHomeLoader.add_constructor('!include_dir_named', ESPHomeLoader.construct_include_dir_named) -ESPHomeLoader.add_constructor('!include_dir_merge_named', - ESPHomeLoader.construct_include_dir_merge_named) -ESPHomeLoader.add_constructor('!lambda', ESPHomeLoader.construct_lambda) -ESPHomeLoader.add_constructor('!force', ESPHomeLoader.construct_force) +ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) +ESPHomeLoader.add_constructor( + "tag:yaml.org,2002:float", ESPHomeLoader.construct_yaml_float +) +ESPHomeLoader.add_constructor( + "tag:yaml.org,2002:binary", ESPHomeLoader.construct_yaml_binary +) +ESPHomeLoader.add_constructor( + "tag:yaml.org,2002:omap", ESPHomeLoader.construct_yaml_omap +) +ESPHomeLoader.add_constructor("tag:yaml.org,2002:str", ESPHomeLoader.construct_yaml_str) +ESPHomeLoader.add_constructor("tag:yaml.org,2002:seq", ESPHomeLoader.construct_yaml_seq) +ESPHomeLoader.add_constructor("tag:yaml.org,2002:map", ESPHomeLoader.construct_yaml_map) +ESPHomeLoader.add_constructor("!env_var", ESPHomeLoader.construct_env_var) +ESPHomeLoader.add_constructor("!secret", ESPHomeLoader.construct_secret) +ESPHomeLoader.add_constructor("!include", ESPHomeLoader.construct_include) +ESPHomeLoader.add_constructor( + "!include_dir_list", ESPHomeLoader.construct_include_dir_list +) +ESPHomeLoader.add_constructor( + "!include_dir_merge_list", ESPHomeLoader.construct_include_dir_merge_list +) +ESPHomeLoader.add_constructor( + "!include_dir_named", ESPHomeLoader.construct_include_dir_named +) +ESPHomeLoader.add_constructor( + "!include_dir_merge_named", ESPHomeLoader.construct_include_dir_merge_named +) +ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) +ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) -def load_yaml(fname): - _SECRET_VALUES.clear() - _SECRET_CACHE.clear() +def load_yaml(fname, clear_secrets=True): + if clear_secrets: + _SECRET_VALUES.clear() + _SECRET_CACHE.clear() return _load_yaml_internal(fname) @@ -297,20 +343,21 @@ def _load_yaml_internal(fname): try: return loader.get_single_data() or OrderedDict() except yaml.YAMLError as exc: - raise EsphomeError(exc) + raise EsphomeError(exc) from exc finally: loader.dispose() def dump(dict_): """Dump YAML to a string and remove null.""" - return yaml.dump(dict_, default_flow_style=False, allow_unicode=True, - Dumper=ESPHomeDumper) + return yaml.dump( + dict_, default_flow_style=False, allow_unicode=True, Dumper=ESPHomeDumper + ) def _is_file_valid(name): """Decide if a file is valid.""" - return not name.startswith('.') + return not name.startswith(".") def _find_files(directory, pattern): @@ -337,7 +384,7 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors if self.alias_key is not None: self.represented_objects[self.alias_key] = node best_style = True - if hasattr(mapping, 'items'): + if hasattr(mapping, "items"): mapping = list(mapping.items()) for item_key, item_value in mapping: node_key = self.represent_data(item_key) @@ -355,29 +402,33 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors return node def represent_secret(self, value): - return self.represent_scalar(tag='!secret', value=_SECRET_VALUES[str(value)]) + return self.represent_scalar(tag="!secret", value=_SECRET_VALUES[str(value)]) def represent_stringify(self, value): if is_secret(value): return self.represent_secret(value) - return self.represent_scalar(tag='tag:yaml.org,2002:str', value=str(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') + 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)) + 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) if math.isnan(value): - value = '.nan' + value = ".nan" elif math.isinf(value): - value = '.inf' if value > 0 else '-.inf' + value = ".inf" if value > 0 else "-.inf" else: value = str(repr(value)).lower() # Note that in some cases `repr(data)` represents a float number @@ -387,14 +438,14 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors # Unfortunately, this is not a valid float representation according # to the definition of the `!!float` tag. We fix this by adding # '.0' before the 'e' symbol. - if '.' not in value and 'e' in value: - value = value.replace('e', '.0e', 1) - return self.represent_scalar(tag='tag:yaml.org,2002:float', value=value) + if "." not in value and "e" in value: + value = value.replace("e", ".0e", 1) + return self.represent_scalar(tag="tag:yaml.org,2002:float", value=value) def represent_lambda(self, value): if is_secret(value.value): return self.represent_secret(value.value) - return self.represent_scalar(tag='!lambda', value=value.value, style='|') + return self.represent_scalar(tag="!lambda", value=value.value, style="|") def represent_id(self, value): if is_secret(value.id): @@ -403,12 +454,11 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors ESPHomeDumper.add_multi_representer( - dict, - lambda dumper, value: dumper.represent_mapping('tag:yaml.org,2002:map', value) + dict, lambda dumper, value: dumper.represent_mapping("tag:yaml.org,2002:map", value) ) ESPHomeDumper.add_multi_representer( list, - lambda dumper, value: dumper.represent_sequence('tag:yaml.org,2002:seq', value) + lambda dumper, value: dumper.represent_sequence("tag:yaml.org,2002:seq", value), ) ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool) ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index a8ca5b3c53..1fbdf7e93f 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,514 +1,34 @@ -# 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, + ServiceStateChange, + current_time_millis, +) _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: @@ -516,27 +36,26 @@ 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 return False if next_ <= now: out = DNSOutgoing(_FLAGS_QR_QUERY) - out.add_question( - DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) + out.add_question(DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) zc.send(out) 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) @@ -544,235 +63,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 = current_time_millis() - 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 = current_time_millis() 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)) + 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 a6e9926de6..3c0b725d65 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,52 +1,156 @@ -; 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-but-set-variable + -Wno-sign-compare + +[clangtidy] +; This are the flags for clang-tidy. +build_flags = + -Wall + -Wextra + -Wunreachable-code + -Wfor-loop-analysis + -Wshadow-field + -Wshadow-field-in-constructor + -Wshadow-uncaptured-local [common] lib_deps = - AsyncTCP-esphome@1.1.1 - AsyncMqttClient-esphome@0.8.4 - ArduinoJson-esphomelib@5.13.3 - ESPAsyncWebServer-esphome@1.2.6 - FastLED@3.2.9 - NeoPixelBus-esphome@2.5.2 - ESPAsyncTCP-esphome@1.2.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.4 ; api + makuna/NeoPixelBus@2.6.9 ; neopixelbus + esphome/Improv@1.0.0 ; improv_serial / esp32_improv build_flags = - -Wno-reorder - -DUSE_WEB_SERVER - -DUSE_FAST_LED_LIGHT - -DUSE_NEO_PIXEL_BUS_LIGHT - -DCLANG_TIDY -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE -; Don't use FlashStringHelper for debug builds because CLion freaks out for all -; log messages -src_filter = + +src_filter = + +<./> + +<../tests/dummy_main.cpp> + +<../.temp/all-include.cpp> -[env:livingroom8266] -; use Arduino framework v2.3.0 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760) -platform = espressif8266@1.8.0 -board = nodemcuv2 -framework = arduino +[common:arduino] +extends = common lib_deps = ${common.lib_deps} - ESP8266WiFi - Hash -build_flags = ${common.build_flags} -src_filter = ${common.src_filter} + + ottowinter/AsyncMqttClient-esphome@0.8.6 ; mqtt + ottowinter/ArduinoJson-esphomelib@5.13.3 ; json + esphome/ESPAsyncWebServer-esphome@2.1.0 ; web_server_base + fastled/FastLED@3.3.2 ; fastled_base + mikalhart/TinyGPSPlus@1.0.2 ; gps + freekode/TM1651@1.0.1 ; tm1651 + glmnet/Dsmr@0.5 ; dsmr + rweather/Crypto@0.2.0 ; dsmr + dudanov/MideaUART@1.1.8 ; midea + ; PIO isn't update releases correctly, see: + ; https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd + https://github.com/ToniA/arduino-heatpumpir.git#1.0.18 ; 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:livingroom32] -platform = espressif32@1.11.0 -board = nodemcu-32s framework = arduino -lib_deps = ${common.lib_deps} -build_flags = ${common.build_flags} -DUSE_ETHERNET -src_filter = ${common.src_filter} + +board = nodemcuv2 +lib_deps = + ${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 + +framework = arduino +board = nodemcu-32s +lib_deps = + ${common:arduino.lib_deps} + esphome/AsyncTCP-esphome@1.2.2 ; async_tcp +build_flags = + ${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/pylintrc b/pylintrc index c65a9a7cd9..8f2e9a7359 100644 --- a/pylintrc +++ b/pylintrc @@ -3,6 +3,7 @@ reports=no ignore=api_pb2.py disable= + format, missing-docstring, fixme, unused-argument, @@ -25,3 +26,6 @@ disable= stop-iteration-return, no-self-use, import-outside-toplevel, + # Broken + unsupported-membership-test, + unsubscriptable-object, diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..7a75060c8e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +target-version = ["py36", "py37", "py38"] +exclude = 'generated' diff --git a/requirements.txt b/requirements.txt index 8c763ef9a6..c45797a71f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,18 @@ -voluptuous==0.11.7 -PyYAML==5.2 -paho-mqtt==1.5.0 -colorlog==4.0.2 -tornado==5.1.1 -protobuf==3.11.1 -tzlocal==2.0.0 -pytz==2019.3 -pyserial==3.4 -ifaddr==0.1.6 -platformio==4.1.0 -esptool==2.7 +voluptuous==0.12.2 +PyYAML==6.0 +paho-mqtt==1.6.1 +colorama==0.4.4 +tornado==6.1 +tzlocal==4.1 # from time +tzdata>=2021.1 # from time +pyserial==3.5 +platformio==5.2.2 # When updating platformio, also update Dockerfile +esptool==3.2 +click==8.0.3 +esphome-dashboard==20211211.0 +aioesphomeapi==10.6.0 +zeroconf==0.36.13 + +# 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_optional.txt b/requirements_optional.txt new file mode 100644 index 0000000000..2c73430109 --- /dev/null +++ b/requirements_optional.txt @@ -0,0 +1,2 @@ +pillow>4.0.0 +cryptography>=2.0.0,<4 diff --git a/requirements_test.txt b/requirements_test.txt index 268f78fcdf..4d5c40296f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,25 +1,12 @@ -voluptuous==0.11.7 -PyYAML==5.2 -paho-mqtt==1.5.0 -colorlog==4.0.2 -tornado==5.1.1 -protobuf==3.11.1 -tzlocal==2.0.0 -pytz==2019.3 -pyserial==3.4 -ifaddr==0.1.6 -platformio==4.1.0 -esptool==2.7 - -pylint==1.9.4 ; python_version<"3" -pylint==2.4.4 ; python_version>"3" -flake8==3.7.9 -pillow -pexpect +pylint==2.12.2 +flake8==4.0.1 +black==21.12b0 +pre-commit # Unit tests -pytest==5.3.2 -pytest-cov==2.8.1 -pytest-mock==1.13.0 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-mock==3.6.1 +pytest-asyncio==0.16.0 asyncmock==0.4.2 -hypothesis==4.57.0 +hypothesis==5.49.0 diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py index e690a2c5d7..f5297c062c 100644 --- a/script/api_protobuf/api_options_pb2.py +++ b/script/api_protobuf/api_options_pb2.py @@ -2,12 +2,14 @@ # source: api_options.proto import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) + +_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() @@ -17,37 +19,38 @@ from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor DESCRIPTOR = _descriptor.FileDescriptor( - name='api_options.proto', - package='', - syntax='proto2', - serialized_options=None, - serialized_pb=_b('\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto\"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse') - , - dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) + name="api_options.proto", + package="", + syntax="proto2", + serialized_options=None, + serialized_pb=_b( + '\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse' + ), + dependencies=[ + google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR, + ], +) _APISOURCETYPE = _descriptor.EnumDescriptor( - name='APISourceType', - full_name='APISourceType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='SOURCE_BOTH', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SOURCE_SERVER', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SOURCE_CLIENT', index=2, number=2, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=63, - serialized_end=133, + name="APISourceType", + full_name="APISourceType", + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name="SOURCE_BOTH", index=0, number=0, serialized_options=None, type=None + ), + _descriptor.EnumValueDescriptor( + name="SOURCE_SERVER", index=1, number=1, serialized_options=None, type=None + ), + _descriptor.EnumValueDescriptor( + name="SOURCE_CLIENT", index=2, number=2, serialized_options=None, type=None + ), + ], + containing_type=None, + serialized_options=None, + serialized_start=63, + serialized_end=133, ) _sym_db.RegisterEnumDescriptor(_APISOURCETYPE) @@ -58,105 +61,186 @@ SOURCE_CLIENT = 2 NEEDS_SETUP_CONNECTION_FIELD_NUMBER = 1038 needs_setup_connection = _descriptor.FieldDescriptor( - name='needs_setup_connection', full_name='needs_setup_connection', index=0, - number=1038, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=True, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="needs_setup_connection", + full_name="needs_setup_connection", + index=0, + number=1038, + type=8, + cpp_type=7, + label=1, + has_default_value=True, + default_value=True, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) NEEDS_AUTHENTICATION_FIELD_NUMBER = 1039 needs_authentication = _descriptor.FieldDescriptor( - name='needs_authentication', full_name='needs_authentication', index=1, - number=1039, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=True, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="needs_authentication", + full_name="needs_authentication", + index=1, + number=1039, + type=8, + cpp_type=7, + label=1, + has_default_value=True, + default_value=True, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) ID_FIELD_NUMBER = 1036 id = _descriptor.FieldDescriptor( - name='id', full_name='id', index=2, - number=1036, type=13, cpp_type=3, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="id", + full_name="id", + index=2, + number=1036, + type=13, + cpp_type=3, + label=1, + has_default_value=True, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) SOURCE_FIELD_NUMBER = 1037 source = _descriptor.FieldDescriptor( - name='source', full_name='source', index=3, - number=1037, type=14, cpp_type=8, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="source", + full_name="source", + index=3, + number=1037, + type=14, + cpp_type=8, + label=1, + has_default_value=True, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) IFDEF_FIELD_NUMBER = 1038 ifdef = _descriptor.FieldDescriptor( - name='ifdef', full_name='ifdef', index=4, - number=1038, 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=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="ifdef", + full_name="ifdef", + index=4, + number=1038, + 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=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) LOG_FIELD_NUMBER = 1039 log = _descriptor.FieldDescriptor( - name='log', full_name='log', index=5, - number=1039, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=True, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="log", + full_name="log", + index=5, + number=1039, + type=8, + cpp_type=7, + label=1, + has_default_value=True, + default_value=True, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) NO_DELAY_FIELD_NUMBER = 1040 no_delay = _descriptor.FieldDescriptor( - name='no_delay', full_name='no_delay', index=6, - number=1040, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) + name="no_delay", + full_name="no_delay", + index=6, + number=1040, + type=8, + cpp_type=7, + label=1, + has_default_value=True, + default_value=False, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=True, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, +) _VOID = _descriptor.Descriptor( - name='void', - full_name='void', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=55, - serialized_end=61, + name="void", + full_name="void", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[], + extensions=[], + nested_types=[], + enum_types=[], + serialized_options=None, + is_extendable=False, + syntax="proto2", + extension_ranges=[], + oneofs=[], + serialized_start=55, + serialized_end=61, ) -DESCRIPTOR.message_types_by_name['void'] = _VOID -DESCRIPTOR.enum_types_by_name['APISourceType'] = _APISOURCETYPE -DESCRIPTOR.extensions_by_name['needs_setup_connection'] = needs_setup_connection -DESCRIPTOR.extensions_by_name['needs_authentication'] = needs_authentication -DESCRIPTOR.extensions_by_name['id'] = id -DESCRIPTOR.extensions_by_name['source'] = source -DESCRIPTOR.extensions_by_name['ifdef'] = ifdef -DESCRIPTOR.extensions_by_name['log'] = log -DESCRIPTOR.extensions_by_name['no_delay'] = no_delay +DESCRIPTOR.message_types_by_name["void"] = _VOID +DESCRIPTOR.enum_types_by_name["APISourceType"] = _APISOURCETYPE +DESCRIPTOR.extensions_by_name["needs_setup_connection"] = needs_setup_connection +DESCRIPTOR.extensions_by_name["needs_authentication"] = needs_authentication +DESCRIPTOR.extensions_by_name["id"] = id +DESCRIPTOR.extensions_by_name["source"] = source +DESCRIPTOR.extensions_by_name["ifdef"] = ifdef +DESCRIPTOR.extensions_by_name["log"] = log +DESCRIPTOR.extensions_by_name["no_delay"] = no_delay _sym_db.RegisterFileDescriptor(DESCRIPTOR) -void = _reflection.GeneratedProtocolMessageType('void', (_message.Message,), dict( - DESCRIPTOR = _VOID, - __module__ = 'api_options_pb2' - # @@protoc_insertion_point(class_scope:void) - )) +void = _reflection.GeneratedProtocolMessageType( + "void", + (_message.Message,), + dict( + DESCRIPTOR=_VOID, + __module__="api_options_pb2" + # @@protoc_insertion_point(class_scope:void) + ), +) _sym_db.RegisterMessage(void) -google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_setup_connection) -google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_authentication) +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension( + needs_setup_connection +) +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension( + needs_authentication +) google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(id) source.enum_type = _APISOURCETYPE google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(source) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py old mode 100644 new mode 100755 index e78eff76c7..016a0995b9 --- 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. @@ -27,39 +28,48 @@ from subprocess import call import api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor -file_header = '// This file was automatically generated with a tool.\n' -file_header += '// See scripts/api_protobuf/api_protobuf.py\n' +file_header = "// This file was automatically generated with a tool.\n" +file_header += "// See scripts/api_protobuf/api_protobuf.py\n" cwd = Path(__file__).resolve().parent -root = cwd.parent.parent / 'esphome' / 'components' / 'api' -prot = root / 'api.protoc' -call(['protoc', '-o', str(prot), '-I', str(root), 'api.proto']) +root = cwd.parent.parent / "esphome" / "components" / "api" +prot = root / "api.protoc" +call(["protoc", "-o", str(prot), "-I", str(root), "api.proto"]) content = prot.read_bytes() d = descriptor.FileDescriptorSet.FromString(content) -def indent_list(text, padding=' '): - return [padding + line for line in text.splitlines()] +def indent_list(text, padding=" "): + lines = [] + for line in text.splitlines(): + if line == "": + p = "" + elif line.startswith("#ifdef") or line.startswith("#endif"): + p = "" + else: + p = padding + lines.append(p + line) + return lines -def indent(text, padding=' '): - return '\n'.join(indent_list(text, padding)) +def indent(text, padding=" "): + return "\n".join(indent_list(text, padding)) def camel_to_snake(name): # https://stackoverflow.com/a/1176023 - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() -class TypeInfo(): +class TypeInfo: def __init__(self, field): self._field = field @property def default_value(self): - return '' + return "" @property def name(self): @@ -87,11 +97,11 @@ class TypeInfo(): @property def reference_type(self): - return f'{self.cpp_type} ' + return f"{self.cpp_type} " @property def const_reference_type(self): - return f'{self.cpp_type} ' + return f"{self.cpp_type} " @property def public_content(self) -> str: @@ -103,18 +113,20 @@ class TypeInfo(): @property def class_member(self) -> str: - return f'{self.cpp_type} {self.field_name}{{{self.default_value}}}; // NOLINT' + return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};" @property def decode_varint_content(self) -> str: content = self.decode_varint if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name} = {content}; return true; - }}''') + }}""" + ) decode_varint = None @@ -123,11 +135,13 @@ class TypeInfo(): content = self.decode_length if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name} = {content}; return true; - }}''') + }}""" + ) decode_length = None @@ -136,11 +150,13 @@ class TypeInfo(): content = self.decode_32bit if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name} = {content}; return true; - }}''') + }}""" + ) decode_32bit = None @@ -149,24 +165,26 @@ class TypeInfo(): content = self.decode_64bit if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name} = {content}; return true; - }}''') + }}""" + ) decode_64bit = None @property def encode_content(self): - return f'buffer.{self.encode_func}({self.number}, this->{self.field_name});' + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" encode_func = None @property def dump_content(self): o = f'out.append(" {self.name}: ");\n' - o += self.dump(f'this->{self.field_name}') + '\n' + o += self.dump(f"this->{self.field_name}") + "\n" o += f'out.append("\\n");\n' return o @@ -186,115 +204,115 @@ def register_type(name): @register_type(1) class DoubleType(TypeInfo): - cpp_type = 'double' - default_value = '0.0' - decode_64bit = 'value.as_double()' - encode_func = 'encode_double' + cpp_type = "double" + default_value = "0.0" + decode_64bit = "value.as_double()" + encode_func = "encode_double" def dump(self, name): o = f'sprintf(buffer, "%g", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(2) class FloatType(TypeInfo): - cpp_type = 'float' - default_value = '0.0f' - decode_32bit = 'value.as_float()' - encode_func = 'encode_float' + cpp_type = "float" + default_value = "0.0f" + decode_32bit = "value.as_float()" + encode_func = "encode_float" def dump(self, name): o = f'sprintf(buffer, "%g", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(3) class Int64Type(TypeInfo): - cpp_type = 'int64_t' - default_value = '0' - decode_varint = 'value.as_int64()' - encode_func = 'encode_int64' + cpp_type = "int64_t" + default_value = "0" + decode_varint = "value.as_int64()" + encode_func = "encode_int64" def dump(self, name): o = f'sprintf(buffer, "%ll", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(4) class UInt64Type(TypeInfo): - cpp_type = 'uint64_t' - default_value = '0' - decode_varint = 'value.as_uint64()' - encode_func = 'encode_uint64' + cpp_type = "uint64_t" + default_value = "0" + decode_varint = "value.as_uint64()" + encode_func = "encode_uint64" def dump(self, name): o = f'sprintf(buffer, "%ull", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(5) class Int32Type(TypeInfo): - cpp_type = 'int32_t' - default_value = '0' - decode_varint = 'value.as_int32()' - encode_func = 'encode_int32' + cpp_type = "int32_t" + default_value = "0" + decode_varint = "value.as_int32()" + encode_func = "encode_int32" def dump(self, name): o = f'sprintf(buffer, "%d", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(6) class Fixed64Type(TypeInfo): - cpp_type = 'uint64_t' - default_value = '0' - decode_64bit = 'value.as_fixed64()' - encode_func = 'encode_fixed64' + cpp_type = "uint64_t" + default_value = "0" + decode_64bit = "value.as_fixed64()" + encode_func = "encode_fixed64" def dump(self, name): o = f'sprintf(buffer, "%ull", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(7) class Fixed32Type(TypeInfo): - cpp_type = 'uint32_t' - default_value = '0' - decode_32bit = 'value.as_fixed32()' - encode_func = 'encode_fixed32' + cpp_type = "uint32_t" + default_value = "0" + decode_32bit = "value.as_fixed32()" + encode_func = "encode_fixed32" def dump(self, name): o = f'sprintf(buffer, "%u", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(8) class BoolType(TypeInfo): - cpp_type = 'bool' - default_value = 'false' - decode_varint = 'value.as_bool()' - encode_func = 'encode_bool' + cpp_type = "bool" + default_value = "false" + decode_varint = "value.as_bool()" + encode_func = "encode_bool" def dump(self, name): - o = f'out.append(YESNO({name}));' + o = f"out.append(YESNO({name}));" return o @register_type(9) class StringType(TypeInfo): - cpp_type = 'std::string' - default_value = '' - reference_type = 'std::string &' - const_reference_type = 'const std::string &' - decode_length = 'value.as_string()' - encode_func = 'encode_string' + cpp_type = "std::string" + default_value = "" + reference_type = "std::string &" + const_reference_type = "const std::string &" + decode_length = "value.as_string()" + encode_func = "encode_string" def dump(self, name): o = f'out.append("\'").append({name}).append("\'");' @@ -307,37 +325,37 @@ class MessageType(TypeInfo): def cpp_type(self): return self._field.type_name[1:] - default_value = '' + default_value = "" @property def reference_type(self): - return f'{self.cpp_type} &' + return f"{self.cpp_type} &" @property def const_reference_type(self): - return f'const {self.cpp_type} &' + return f"const {self.cpp_type} &" @property def encode_func(self): - return f'encode_message<{self.cpp_type}>' + return f"encode_message<{self.cpp_type}>" @property def decode_length(self): - return f'value.as_message<{self.cpp_type}>()' + return f"value.as_message<{self.cpp_type}>()" def dump(self, name): - o = f'{name}.dump_to(out);' + o = f"{name}.dump_to(out);" return o @register_type(12) class BytesType(TypeInfo): - cpp_type = 'std::string' - default_value = '' - reference_type = 'std::string &' - const_reference_type = 'const std::string &' - decode_length = 'value.as_string()' - encode_func = 'encode_string' + cpp_type = "std::string" + default_value = "" + reference_type = "std::string &" + const_reference_type = "const std::string &" + decode_length = "value.as_string()" + encode_func = "encode_string" def dump(self, name): o = f'out.append("\'").append({name}).append("\'");' @@ -346,14 +364,14 @@ class BytesType(TypeInfo): @register_type(13) class UInt32Type(TypeInfo): - cpp_type = 'uint32_t' - default_value = '0' - decode_varint = 'value.as_uint32()' - encode_func = 'encode_uint32' + cpp_type = "uint32_t" + default_value = "0" + decode_varint = "value.as_uint32()" + encode_func = "encode_uint32" def dump(self, name): o = f'sprintf(buffer, "%u", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @@ -361,72 +379,72 @@ class UInt32Type(TypeInfo): class EnumType(TypeInfo): @property def cpp_type(self): - return f'enums::{self._field.type_name[1:]}' + return f"enums::{self._field.type_name[1:]}" @property def decode_varint(self): - return f'value.as_enum<{self.cpp_type}>()' + return f"value.as_enum<{self.cpp_type}>()" - default_value = '' + default_value = "" @property def encode_func(self): - return f'encode_enum<{self.cpp_type}>' + return f"encode_enum<{self.cpp_type}>" def dump(self, name): - o = f'out.append(proto_enum_to_string<{self.cpp_type}>({name}));' + o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" return o @register_type(15) class SFixed32Type(TypeInfo): - cpp_type = 'int32_t' - default_value = '0' - decode_32bit = 'value.as_sfixed32()' - encode_func = 'encode_sfixed32' + cpp_type = "int32_t" + default_value = "0" + decode_32bit = "value.as_sfixed32()" + encode_func = "encode_sfixed32" def dump(self, name): o = f'sprintf(buffer, "%d", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(16) class SFixed64Type(TypeInfo): - cpp_type = 'int64_t' - default_value = '0' - decode_64bit = 'value.as_sfixed64()' - encode_func = 'encode_sfixed64' + cpp_type = "int64_t" + default_value = "0" + decode_64bit = "value.as_sfixed64()" + encode_func = "encode_sfixed64" def dump(self, name): o = f'sprintf(buffer, "%ll", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(17) class SInt32Type(TypeInfo): - cpp_type = 'int32_t' - default_value = '0' - decode_varint = 'value.as_sint32()' - encode_func = 'encode_sint32' + cpp_type = "int32_t" + default_value = "0" + decode_varint = "value.as_sint32()" + encode_func = "encode_sint32" def dump(self, name): o = f'sprintf(buffer, "%d", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @register_type(18) class SInt64Type(TypeInfo): - cpp_type = 'int64_t' - default_value = '0' - decode_varint = 'value.as_sint64()' - encode_func = 'encode_sin64' + cpp_type = "int64_t" + default_value = "0" + decode_varint = "value.as_sint64()" + encode_func = "encode_sin64" - def dump(self): + def dump(self, name): o = f'sprintf(buffer, "%ll", {name});\n' - o += f'out.append(buffer);' + o += f"out.append(buffer);" return o @@ -437,59 +455,67 @@ class RepeatedTypeInfo(TypeInfo): @property def cpp_type(self): - return f'std::vector<{self._ti.cpp_type}>' + return f"std::vector<{self._ti.cpp_type}>" @property def reference_type(self): - return f'{self.cpp_type} &' + return f"{self.cpp_type} &" @property def const_reference_type(self): - return f'const {self.cpp_type} &' + return f"const {self.cpp_type} &" @property def decode_varint_content(self) -> str: content = self._ti.decode_varint if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name}.push_back({content}); return true; - }}''') + }}""" + ) @property def decode_length_content(self) -> str: content = self._ti.decode_length if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name}.push_back({content}); return true; - }}''') + }}""" + ) @property def decode_32bit_content(self) -> str: content = self._ti.decode_32bit if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name}.push_back({content}); return true; - }}''') + }}""" + ) @property def decode_64bit_content(self) -> str: content = self._ti.decode_64bit if content is None: return None - return dedent(f'''\ + return dedent( + f"""\ case {self.number}: {{ this->{self.field_name}.push_back({content}); return true; - }}''') + }}""" + ) @property def _ti_is_bool(self): @@ -498,18 +524,18 @@ class RepeatedTypeInfo(TypeInfo): @property def encode_content(self): - return f"""\ - for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{ - buffer.{self._ti.encode_func}({self.number}, it, true); - }}""" + o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += f"}}" + return o @property def dump_content(self): o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n' o += f' out.append(" {self.name}: ");\n' - o += indent(self._ti.dump('it')) + '\n' + o += indent(self._ti.dump("it")) + "\n" o += f' out.append("\\n");\n' - o += f'}}\n' + o += f"}}\n" return o @@ -517,17 +543,18 @@ def build_enum_type(desc): name = desc.name out = f"enum {name} : uint32_t {{\n" for v in desc.value: - out += f' {v.name} = {v.number},\n' - out += '};\n' + out += f" {v.name} = {v.number},\n" + out += "};\n" - cpp = f"template<>\n" - cpp += f"const char *proto_enum_to_string(enums::{name} value) {{\n" + cpp = f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n" cpp += f" switch (value) {{\n" for v in desc.value: - cpp += f' case enums::{v.name}: return "{v.name}";\n' - cpp += f' default: return "UNKNOWN";\n' - cpp += f' }}\n' - cpp += f'}}\n' + cpp += f" case enums::{v.name}:\n" + cpp += f' return "{v.name}";\n' + cpp += f" default:\n" + cpp += f' return "UNKNOWN";\n' + cpp += f" }}\n" + cpp += f"}}\n" return out, cpp @@ -562,80 +589,101 @@ def build_message_type(desc): if ti.dump_content: dump.append(ti.dump_content) - cpp = '' + cpp = "" if decode_varint: - decode_varint.append('default:\n return false;') - o = f'bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n' - o += ' switch (field_id) {\n' - o += indent("\n".join(decode_varint), ' ') + '\n' - o += ' }\n' - o += '}\n' + decode_varint.append("default:\n return false;") + o = f"bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n" + o += " switch (field_id) {\n" + o += indent("\n".join(decode_varint), " ") + "\n" + o += " }\n" + o += "}\n" cpp += o - prot = 'bool decode_varint(uint32_t field_id, ProtoVarInt value) override;' + prot = "bool decode_varint(uint32_t field_id, ProtoVarInt value) override;" protected_content.insert(0, prot) if decode_length: - decode_length.append('default:\n return false;') - o = f'bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n' - o += ' switch (field_id) {\n' - o += indent("\n".join(decode_length), ' ') + '\n' - o += ' }\n' - o += '}\n' + decode_length.append("default:\n return false;") + o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n" + o += " switch (field_id) {\n" + o += indent("\n".join(decode_length), " ") + "\n" + o += " }\n" + o += "}\n" cpp += o - prot = 'bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;' + prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;" protected_content.insert(0, prot) if decode_32bit: - decode_32bit.append('default:\n return false;') - o = f'bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n' - o += ' switch (field_id) {\n' - o += indent("\n".join(decode_32bit), ' ') + '\n' - o += ' }\n' - o += '}\n' + decode_32bit.append("default:\n return false;") + o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n" + o += " switch (field_id) {\n" + o += indent("\n".join(decode_32bit), " ") + "\n" + o += " }\n" + o += "}\n" cpp += o - prot = 'bool decode_32bit(uint32_t field_id, Proto32Bit value) override;' + prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;" protected_content.insert(0, prot) if decode_64bit: - decode_64bit.append('default:\n return false;') - o = f'bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n' - o += ' switch (field_id) {\n' - o += indent("\n".join(decode_64bit), ' ') + '\n' - o += ' }\n' - o += '}\n' + decode_64bit.append("default:\n return false;") + o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n" + o += " switch (field_id) {\n" + o += indent("\n".join(decode_64bit), " ") + "\n" + o += " }\n" + o += "}\n" cpp += o - prot = 'bool decode_64bit(uint32_t field_id, Proto64bit value) override;' + prot = "bool decode_64bit(uint32_t field_id, Proto64bit value) override;" protected_content.insert(0, prot) - o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{\n" - o += indent('\n'.join(encode)) + '\n' - o += '}\n' + o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" + if encode: + if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: + o += f" {encode[0]} " + else: + o += "\n" + o += indent("\n".join(encode)) + "\n" + o += "}\n" cpp += o - prot = 'void encode(ProtoWriteBuffer buffer) const override;' + prot = "void encode(ProtoWriteBuffer buffer) const override;" public_content.append(prot) - o = f"void {desc.name}::dump_to(std::string &out) const {{\n" + o = f"void {desc.name}::dump_to(std::string &out) const {{" if dump: - o += f" char buffer[64];\n" - o += f' out.append("{desc.name} {{\\n");\n' - o += indent('\n'.join(dump)) + '\n' - o += f' out.append("}}");\n' + if len(dump) == 1 and len(dump[0]) + len(o) + 3 < 120: + o += f" {dump[0]} " + else: + o += "\n" + o += f" __attribute__((unused)) char buffer[64];\n" + o += f' out.append("{desc.name} {{\\n");\n' + o += indent("\n".join(dump)) + "\n" + o += f' out.append("}}");\n' else: - o += f' out.append("{desc.name} {{}}");\n' - o += '}\n' + o2 = f'out.append("{desc.name} {{}}");' + if len(o) + len(o2) + 3 < 120: + o += f" {o2} " + else: + 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" - out += ' public:\n' - out += indent('\n'.join(public_content)) + '\n' - out += ' protected:\n' - out += indent('\n'.join(protected_content)) + '\n' + out += " public:\n" + out += indent("\n".join(public_content)) + "\n" + out += "\n" + out += " protected:\n" + out += indent("\n".join(protected_content)) + if len(protected_content) > 0: + out += "\n" out += "};\n" return out, cpp file = d.file[0] content = file_header -content += '''\ +content += """\ #pragma once #include "proto.h" @@ -643,26 +691,26 @@ content += '''\ namespace esphome { namespace api { -''' +""" cpp = file_header -cpp += '''\ +cpp += """\ #include "api_pb2.h" #include "esphome/core/log.h" namespace esphome { namespace api { -''' +""" -content += 'namespace enums {\n\n' +content += "namespace enums {\n\n" for enum in file.enum_type: s, c = build_enum_type(enum) content += s cpp += c -content += '\n} // namespace enums\n\n' +content += "\n} // namespace enums\n\n" mt = file.message_type @@ -671,21 +719,21 @@ for m in mt: content += s cpp += c -content += '''\ +content += """\ } // namespace api } // namespace esphome -''' -cpp += '''\ +""" +cpp += """\ } // namespace api } // namespace esphome -''' +""" -with open(root / 'api_pb2.h', 'w') as f: +with open(root / "api_pb2.h", "w") as f: f.write(content) -with open(root / 'api_pb2.cpp', 'w') as f: +with open(root / "api_pb2.cpp", "w") as f: f.write(cpp) SOURCE_BOTH = 0 @@ -694,7 +742,7 @@ SOURCE_CLIENT = 2 RECEIVE_CASES = {} -class_name = 'APIServerConnectionBase' +class_name = "APIServerConnectionBase" ifdefs = {} @@ -716,50 +764,54 @@ def build_service_message_type(mt): ifdef = get_opt(mt, pb.ifdef) log = get_opt(mt, pb.log, True) nodelay = get_opt(mt, pb.no_delay, False) - hout = '' - cout = '' + hout = "" + cout = "" if ifdef is not None: ifdefs[str(mt.name)] = ifdef - hout += f'#ifdef {ifdef}\n' - cout += f'#ifdef {ifdef}\n' + hout += f"#ifdef {ifdef}\n" + cout += f"#ifdef {ifdef}\n" if source in (SOURCE_BOTH, SOURCE_SERVER): # Generate send - func = f'send_{snake}' - hout += f'bool {func}(const {mt.name} &msg);\n' - cout += f'bool {class_name}::{func}(const {mt.name} &msg) {{\n' + func = f"send_{snake}" + 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' + cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n" + cout += f"}}\n" if source in (SOURCE_BOTH, SOURCE_CLIENT): # Generate receive - func = f'on_{snake}' - hout += f'virtual void {func}(const {mt.name} &value){{}};\n' - case = '' + func = f"on_{snake}" + hout += f"virtual void {func}(const {mt.name} &value){{}};\n" + case = "" if ifdef is not None: - case += f'#ifdef {ifdef}\n' - case += f'{mt.name} msg;\n' - case += f'msg.decode(msg_data, msg_size);\n' + case += f"#ifdef {ifdef}\n" + 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'this->{func}(msg);\n' + case += f"#endif\n" + case += f"this->{func}(msg);\n" if ifdef is not None: - case += f'#endif\n' - case += 'break;' + case += f"#endif\n" + case += "break;" RECEIVE_CASES[id_] = case if ifdef is not None: - hout += f'#endif\n' - cout += f'#endif\n' + hout += f"#endif\n" + cout += f"#endif\n" return hout, cout hpp = file_header -hpp += '''\ +hpp += """\ #pragma once #include "api_pb2.h" @@ -768,125 +820,125 @@ hpp += '''\ namespace esphome { namespace api { -''' +""" cpp = file_header -cpp += '''\ +cpp += """\ #include "api_pb2_service.h" #include "esphome/core/log.h" namespace esphome { namespace api { -static const char *TAG = "api.service"; +static const char *const TAG = "api.service"; -''' +""" -hpp += f'class {class_name} : public ProtoService {{\n' -hpp += ' public:\n' +hpp += f"class {class_name} : public ProtoService {{\n" +hpp += " public:\n" for mt in file.message_type: obj = build_service_message_type(mt) if obj is None: continue hout, cout = obj - hpp += indent(hout) + '\n' + hpp += indent(hout) + "\n" cpp += cout cases = list(RECEIVE_CASES.items()) cases.sort() -hpp += ' protected:\n' -hpp += f' bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n' -out = f'bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n' -out += f' switch(msg_type) {{\n' +hpp += " protected:\n" +hpp += f" bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" +out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" +out += f" switch (msg_type) {{\n" for i, case in cases: - c = f'case {i}: {{\n' - c += indent(case) + '\n' - c += f'}}' - out += indent(c, ' ') + '\n' -out += ' default: \n' -out += ' return false;\n' -out += ' }\n' -out += ' return true;\n' -out += '}\n' + c = f"case {i}: {{\n" + c += indent(case) + "\n" + c += f"}}" + out += indent(c, " ") + "\n" +out += " default:\n" +out += " return false;\n" +out += " }\n" +out += " return true;\n" +out += "}\n" cpp += out -hpp += '};\n' +hpp += "};\n" serv = file.service[0] -class_name = 'APIServerConnection' -hpp += '\n' -hpp += f'class {class_name} : public {class_name}Base {{\n' -hpp += ' public:\n' -hpp_protected = '' -cpp += '\n' +class_name = "APIServerConnection" +hpp += "\n" +hpp += f"class {class_name} : public {class_name}Base {{\n" +hpp += " public:\n" +hpp_protected = "" +cpp += "\n" m = serv.method[0] for m in serv.method: func = m.name inp = m.input_type[1:] ret = m.output_type[1:] - is_void = ret == 'void' + is_void = ret == "void" snake = camel_to_snake(inp) - on_func = f'on_{snake}' + on_func = f"on_{snake}" needs_conn = get_opt(m, pb.needs_setup_connection, True) needs_auth = get_opt(m, pb.needs_authentication, True) ifdef = ifdefs.get(inp, None) if ifdef is not None: - hpp += f'#ifdef {ifdef}\n' - hpp_protected += f'#ifdef {ifdef}\n' - cpp += f'#ifdef {ifdef}\n' + hpp += f"#ifdef {ifdef}\n" + hpp_protected += f"#ifdef {ifdef}\n" + cpp += f"#ifdef {ifdef}\n" - hpp_protected += f' void {on_func}(const {inp} &msg) override;\n' - hpp += f' virtual {ret} {func}(const {inp} &msg) = 0;\n' - cpp += f'void {class_name}::{on_func}(const {inp} &msg) {{\n' - body = '' + hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" + hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" + cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" + body = "" if needs_conn: - body += 'if (!this->is_connection_setup()) {\n' - body += ' this->on_no_setup_connection();\n' - body += ' return;\n' - body += '}\n' + body += "if (!this->is_connection_setup()) {\n" + body += " this->on_no_setup_connection();\n" + body += " return;\n" + body += "}\n" if needs_auth: - body += 'if (!this->is_authenticated()) {\n' - body += ' this->on_unauthenticated_access();\n' - body += ' return;\n' - body += '}\n' + body += "if (!this->is_authenticated()) {\n" + body += " this->on_unauthenticated_access();\n" + body += " return;\n" + body += "}\n" if is_void: - body += f'this->{func}(msg);\n' + body += f"this->{func}(msg);\n" else: - body += f'{ret} ret = this->{func}(msg);\n' + body += f"{ret} ret = this->{func}(msg);\n" ret_snake = camel_to_snake(ret) - body += f'if (!this->send_{ret_snake}(ret)) {{\n' - body += f' this->on_fatal_error();\n' - body += '}\n' - cpp += indent(body) + '\n' + '}\n' + body += f"if (!this->send_{ret_snake}(ret)) {{\n" + body += f" this->on_fatal_error();\n" + body += "}\n" + cpp += indent(body) + "\n" + "}\n" if ifdef is not None: - hpp += f'#endif\n' - hpp_protected += f'#endif\n' - cpp += f'#endif\n' + hpp += f"#endif\n" + hpp_protected += f"#endif\n" + cpp += f"#endif\n" -hpp += ' protected:\n' +hpp += " protected:\n" hpp += hpp_protected -hpp += '};\n' +hpp += "};\n" -hpp += '''\ +hpp += """\ } // namespace api } // namespace esphome -''' -cpp += '''\ +""" +cpp += """\ } // namespace api } // namespace esphome -''' +""" -with open(root / 'api_pb2_service.h', 'w') as f: +with open(root / "api_pb2_service.h", "w") as f: f.write(hpp) -with open(root / 'api_pb2_service.cpp', 'w') as f: +with open(root / "api_pb2_service.cpp", "w") as f: f.write(cpp) prot.unlink() diff --git a/script/build_codeowners.py b/script/build_codeowners.py new file mode 100755 index 0000000000..2ee7521b91 --- /dev/null +++ b/script/build_codeowners.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys +import argparse +from collections import defaultdict + +from esphome.helpers import write_file_if_changed +from esphome.config import get_component, get_platform +from esphome.core import CORE + +parser = argparse.ArgumentParser() +parser.add_argument( + "--check", help="Check if the CODEOWNERS file is up to date.", action="store_true" +) +args = parser.parse_args() + +# The root directory of the repo +root = Path(__file__).parent.parent +components_dir = root / "esphome" / "components" + +BASE = """ +# This file is generated by script/build_codeowners.py +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# +# Every time an issue is created with a label corresponding to an integration, +# the integration's code owner is automatically notified. + +# Core Code +setup.py @esphome/core +esphome/*.py @esphome/core +esphome/core/* @esphome/core + +# Integrations +""".strip() + +parts = [BASE] + +# Fake some directory so that get_component works +CORE.config_path = str(root) + +codeowners = defaultdict(list) + +for path in components_dir.iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + + name = path.name + comp = get_component(name) + if comp is None: + print( + f"Cannot find component {name}. Make sure current path is pip installed ESPHome" + ) + sys.exit(1) + + codeowners[f"esphome/components/{name}/*"].extend(comp.codeowners) + + for platform_path in path.iterdir(): + platform_name = platform_path.stem + platform = get_platform(platform_name, name) + if platform is None: + continue + + if platform_path.is_dir(): + # Sub foldered platforms get their own line + if not (platform_path / "__init__.py").is_file(): + continue + codeowners[f"esphome/components/{name}/{platform_name}/*"].extend( + platform.codeowners + ) + continue + + # Non-subfoldered platforms add to codeowners at component level + if not platform_path.is_file() or platform_path.name == "__init__.py": + continue + codeowners[f"esphome/components/{name}/*"].extend(platform.codeowners) + + +for path, owners in sorted(codeowners.items()): + owners = sorted(set(owners)) + if not owners: + continue + for owner in owners: + if not owner.startswith("@"): + print( + f"Codeowner {owner} for integration {path} must start with an '@' symbol!" + ) + sys.exit(1) + parts.append(f"{path} {' '.join(owners)}") + + +# End newline +parts.append("") +content = "\n".join(parts) +codeowners_file = root / "CODEOWNERS" + +if args.check: + if codeowners_file.read_text() != content: + print("CODEOWNERS file is not up to date.") + print("Please run `script/build_codeowners.py`") + sys.exit(1) + print("CODEOWNERS file is up to date") +else: + write_file_if_changed(codeowners_file, content) + print("Wrote CODEOWNERS") diff --git a/script/build_compile_commands.py b/script/build_compile_commands.py deleted file mode 100755 index f0fc48ad98..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 new file mode 100644 index 0000000000..7a3257411c --- /dev/null +++ b/script/build_jsonschema.py @@ -0,0 +1,801 @@ +#!/usr/bin/env python3 + +from esphome.cpp_generator import MockObj +import json +import argparse +import os +import re +from pathlib import Path +import voluptuous as vol + +# NOTE: Cannot import other esphome components globally as a modification in jsonschema +# is needed before modules are loaded +import esphome.jsonschema as ejs + +ejs.EnableJsonSchemaCollect = True + +DUMP_COMMENTS = False + +JSC_ACTION = "automation.ACTION_REGISTRY" +JSC_ALLOF = "allOf" +JSC_ANYOF = "anyOf" +JSC_COMMENT = "$comment" +JSC_CONDITION = "automation.CONDITION_REGISTRY" +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 = {} +schema_registry = {} +components = {} +modules = {} +registries = [] +pending_refs = [] + +definitions = {} +base_props = {} + + +parser = argparse.ArgumentParser() +parser.add_argument( + "--output", default="esphome.json", help="Output filename", type=os.path.abspath +) + +args = parser.parse_args() + + +def get_ref(definition): + return {JSC_REF: "#/definitions/" + definition} + + +def is_ref(jschema): + return isinstance(jschema, dict) and JSC_REF in jschema + + +def unref(jschema): + return definitions.get(jschema[JSC_REF][len("#/definitions/") :]) + + +def add_definition_array_or_single_object(ref): + return {JSC_ANYOF: [{"type": "array", "items": ref}, ref]} + + +def add_core(): + from esphome.core.config import CONFIG_SCHEMA + + base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA.schema) + + +def add_buses(): + # uart + from esphome.components.uart import UART_DEVICE_SCHEMA + + get_jschema("uart_bus", UART_DEVICE_SCHEMA) + + # spi + from esphome.components.spi import spi_device_schema + + get_jschema("spi_bus", spi_device_schema(False)) + + # i2c + from esphome.components.i2c import i2c_device_schema + + get_jschema("i2c_bus", i2c_device_schema(None)) + + +def add_registries(): + for domain, module in modules.items(): + add_module_registries(domain, module) + + +def add_module_registries(domain, module): + from esphome.util import Registry + + for c in dir(module): + m = getattr(module, c) + if isinstance(m, Registry): + add_registry(domain + "." + c, m) + + +def add_registry(registry_name, registry): + validators = [] + registries.append((registry, registry_name)) + for name in registry.keys(): + schema = get_jschema(str(name), registry[name].schema, create_return_ref=False) + if not schema: + schema = {"type": "null"} + o_schema = {"type": "object", JSC_PROPERTIES: {name: schema}} + o_schema = create_ref( + registry_name + "-" + name, str(registry[name].schema) + "x", o_schema + ) + validators.append(o_schema) + definitions[registry_name] = {JSC_ANYOF: validators} + + +def get_registry_ref(registry): + # we don't know yet + ref = {JSC_REF: "pending"} + pending_refs.append((ref, registry)) + return ref + + +def solve_pending_refs(): + for ref, registry in pending_refs: + for registry_match, name in registries: + if registry == registry_match: + ref[JSC_REF] = "#/definitions/" + name + + +def add_module_schemas(name, module): + import esphome.config_validation as cv + + for c in dir(module): + v = getattr(module, c) + if isinstance(v, cv.Schema): + get_jschema(name + "." + c, v) + + +def get_dirs(): + from esphome.loader import CORE_COMPONENTS_PATH + + dir_names = [ + d + for d in os.listdir(CORE_COMPONENTS_PATH) + if not d.startswith("__") + and os.path.isdir(os.path.join(CORE_COMPONENTS_PATH, d)) + ] + return dir_names + + +def get_logger_tags(): + from esphome.loader import CORE_COMPONENTS_PATH + import glob + + pattern = re.compile(r'^static const char(\*\s|\s\*)TAG = "(\w.*)";', re.MULTILINE) + tags = [ + "app", + "component", + "esphal", + "helpers", + "preferences", + "scheduler", + "api.service", + ] + for x in os.walk(CORE_COMPONENTS_PATH): + for y in glob.glob(os.path.join(x[0], "*.cpp")): + with open(y, "r") as file: + data = file.read() + match = pattern.search(data) + if match: + tags.append(match.group(2)) + return tags + + +def load_components(): + import esphome.config_validation as cv + from esphome.config import get_component + + modules["cv"] = cv + from esphome import automation + + modules["automation"] = automation + + for domain in get_dirs(): + components[domain] = get_component(domain) + modules[domain] = components[domain].module + + +def add_components(): + from esphome.config import get_platform + + for domain, c in components.items(): + if c.is_platform_component: + # this is a platform_component, e.g. binary_sensor + platform_schema = [ + { + "type": "object", + "properties": {"platform": {"type": "string"}}, + } + ] + if domain not in ("output", "display"): + # output bases are either FLOAT or BINARY so don't add common base for this + # display bases are either simple or FULL so don't add common base for this + platform_schema = [ + {"$ref": f"#/definitions/{domain}.{domain.upper()}_SCHEMA"} + ] + platform_schema + + base_props[domain] = {"type": "array", "items": {"allOf": platform_schema}} + + add_module_registries(domain, c.module) + add_module_schemas(domain, c.module) + + # need first to iterate all platforms then iteate components + # a platform component can have other components as properties, + # e.g. climate components usually have a temperature sensor + + for domain, c in components.items(): + if (c.config_schema is not None) or c.is_platform_component: + if c.is_platform_component: + platform_schema = base_props[domain]["items"]["allOf"] + for platform in get_dirs(): + p = get_platform(domain, platform) + if p is not None: + # this is a platform element, e.g. + # - platform: gpio + schema = get_jschema( + domain + "-" + platform, + p.config_schema, + create_return_ref=False, + ) + if ( + schema + ): # for invalid schemas, None is returned thus is deprecated + platform_schema.append( + { + "if": { + JSC_PROPERTIES: { + "platform": {"const": platform} + } + }, + "then": schema, + } + ) + + elif c.config_schema is not None: + # adds root components which are not platforms, e.g. api: logger: + if c.multi_conf: + schema = get_jschema(domain, c.config_schema) + schema = add_definition_array_or_single_object(schema) + else: + schema = get_jschema(domain, c.config_schema, False) + base_props[domain] = schema + + +def get_automation_schema(name, vschema): + from esphome.automation import AUTOMATION_SCHEMA + + # ensure SIMPLE_AUTOMATION + if SIMPLE_AUTOMATION not in definitions: + simple_automation = add_definition_array_or_single_object(get_ref(JSC_ACTION)) + simple_automation[JSC_ANYOF].append( + get_jschema(AUTOMATION_SCHEMA.__module__, AUTOMATION_SCHEMA) + ) + + definitions[schema_names[str(AUTOMATION_SCHEMA)]][JSC_PROPERTIES][ + "then" + ] = add_definition_array_or_single_object(get_ref(JSC_ACTION)) + definitions[SIMPLE_AUTOMATION] = simple_automation + + extra_vschema = None + if AUTOMATION_SCHEMA == ejs.extended_schemas[str(vschema)][0]: + extra_vschema = ejs.extended_schemas[str(vschema)][1] + + if not extra_vschema: + return get_ref(SIMPLE_AUTOMATION) + + # add then property + extra_jschema = get_jschema(name, extra_vschema, False) + + if is_ref(extra_jschema): + return extra_jschema + + if not JSC_PROPERTIES in extra_jschema: + # these are interval: and exposure_notifications, featuring automations a component + extra_jschema[JSC_ALLOF][0][JSC_PROPERTIES][ + "then" + ] = add_definition_array_or_single_object(get_ref(JSC_ACTION)) + ref = create_ref(name, extra_vschema, extra_jschema) + return add_definition_array_or_single_object(ref) + + # automations can be either + # * a single action, + # * an array of action, + # * 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) + + return create_ref(name, extra_vschema, jschema) + + +def get_entry(parent_key, vschema): + from esphome.voluptuous_schema import _Schema as schema_type + + entry = {} + # annotate schema validator info + if DUMP_COMMENTS: + entry[JSC_COMMENT] = "entry: " + parent_key + "/" + str(vschema) + + if isinstance(vschema, list): + ref = get_jschema(parent_key + "[]", vschema[0]) + entry = {"type": "array", "items": ref} + elif isinstance(vschema, schema_type) and hasattr(vschema, "schema"): + entry = get_jschema(parent_key, vschema, False) + elif hasattr(vschema, "validators"): + entry = get_jschema(parent_key, vschema, False) + elif vschema in schema_registry: + entry = schema_registry[vschema].copy() + elif str(vschema) in ejs.registry_schemas: + entry = get_registry_ref(ejs.registry_schemas[str(vschema)]) + elif str(vschema) in ejs.list_schemas: + ref = get_jschema(parent_key, ejs.list_schemas[str(vschema)][0]) + entry = {JSC_ANYOF: [ref, {"type": "array", "items": ref}]} + elif str(vschema) in ejs.typed_schemas: + schema_types = [{"type": "object", "properties": {"type": {"type": "string"}}}] + entry = {"allOf": schema_types} + for schema_key, vschema_type in ejs.typed_schemas[str(vschema)][0][0].items(): + schema_types.append( + { + "if": {"properties": {"type": {"const": schema_key}}}, + "then": get_jschema(f"{parent_key}-{schema_key}", vschema_type), + } + ) + + elif str(vschema) in ejs.hidden_schemas: + # get the schema from the automation schema + type = ejs.hidden_schemas[str(vschema)] + inner_vschema = vschema(ejs.jschema_extractor) + if type == "automation": + entry = get_automation_schema(parent_key, inner_vschema) + elif type == "maybe": + entry = get_jschema(parent_key, inner_vschema) + elif type == "one_of": + entry = {"enum": list(inner_vschema)} + elif type == "enum": + entry = {"enum": list(inner_vschema.keys())} + elif type == "effects": + # Like list schema but subset from list. + subset_list = inner_vschema[0] + # get_jschema('strobex', registry['strobe'].schema) + registry_schemas = [] + for name in subset_list: + registry_schemas.append(get_ref("light.EFFECTS_REGISTRY-" + name)) + + entry = { + JSC_ANYOF: [{"type": "array", "items": {JSC_ANYOF: registry_schemas}}] + } + + else: + raise ValueError("Unknown extracted schema type") + elif str(vschema).startswith(" 0: + output[JSC_REQUIRED] = required + return output + + +def add_pin_schema(): + from esphome import pins + + add_module_schemas("PIN", pins) + + +def add_pin_registry(): + from esphome import pins + + pin_registry = pins.PIN_SCHEMA_REGISTRY + assert len(pin_registry) > 0 + # Here are schemas for pcf8574, mcp23xxx and other port expanders which add + # gpio registers + # ESPHome validates pins schemas if it founds a key in the pin configuration. + # This key is added to a required in jsonschema, and all options are part of a + # oneOf section, so only one is selected. Also internal schema adds number as required. + + for mode in ("INPUT", "OUTPUT"): + schema_name = f"PIN.GPIO_FULL_{mode}_PIN_SCHEMA" + 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: + definitions[schema_name] = {"oneOf": schemas, "type": ["string", "object"]} + + for k, v in pin_registry.items(): + pin_jschema = get_jschema( + f"PIN.{mode}_" + k, v[1][0 if mode == "OUTPUT" else 1] + ) + if unref(pin_jschema): + pin_jschema["required"] = [k] + schemas.append(pin_jschema) + + +def dump_schema(): + import esphome.config_validation as cv + + from esphome import automation + from esphome.automation import validate_potentially_and_condition + from esphome import pins + from esphome.core import CORE + from esphome.helpers import write_file_if_changed + from esphome.components import remote_base + + # The root directory of the repo + root = Path(__file__).parent.parent + + # Fake some directory so that get_component works + CORE.config_path = str(root) + + file_path = args.output + + schema_registry[cv.boolean] = {"type": "boolean"} + + for v in [ + cv.int_, + cv.int_range, + cv.positive_int, + cv.float_, + cv.positive_float, + cv.positive_float, + cv.positive_not_null_int, + cv.negative_one_to_one_float, + cv.port, + ]: + schema_registry[v] = {"type": "number"} + + for v in [ + cv.string, + cv.string_strict, + cv.valid_name, + cv.hex_int, + cv.hex_int_range, + pins.output_pin, + pins.input_pin, + pins.input_pullup_pin, + cv.float_with_unit, + cv.subscribe_topic, + cv.publish_topic, + cv.mqtt_payload, + cv.ssid, + cv.percentage_int, + cv.percentage, + cv.possibly_negative_percentage, + cv.positive_time_period, + cv.positive_time_period_microseconds, + cv.positive_time_period_milliseconds, + cv.positive_time_period_minutes, + cv.positive_time_period_seconds, + ]: + schema_registry[v] = {"type": "string"} + + schema_registry[validate_potentially_and_condition] = get_ref("condition_list") + + for v in [pins.gpio_input_pin_schema, pins.gpio_input_pullup_pin_schema]: + schema_registry[v] = get_ref("PIN.GPIO_FULL_INPUT_PIN_SCHEMA") + for v in [pins.internal_gpio_input_pin_schema, pins.input_pin]: + schema_registry[v] = get_ref("PIN.INPUT_INTERNAL") + + for v in [pins.gpio_output_pin_schema, pins.internal_gpio_output_pin_schema]: + schema_registry[v] = get_ref("PIN.GPIO_FULL_OUTPUT_PIN_SCHEMA") + for v in [pins.internal_gpio_output_pin_schema, pins.output_pin]: + schema_registry[v] = get_ref("PIN.OUTPUT_INTERNAL") + + add_module_schemas("CONFIG", cv) + get_jschema("POLLING_COMPONENT", cv.polling_component_schema("60s")) + + add_pin_schema() + + add_module_schemas("REMOTE_BASE", remote_base) + add_module_schemas("AUTOMATION", automation) + + load_components() + add_registries() + + definitions["condition_list"] = { + JSC_ONEOF: [ + {"type": "array", "items": get_ref(JSC_CONDITION)}, + get_ref(JSC_CONDITION), + ] + } + + output = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "definitions": definitions, + JSC_PROPERTIES: base_props, + } + + add_core() + add_buses() + add_components() + + add_registries() # need second pass, e.g. climate.pid.autotune + add_pin_registry() + solve_pending_refs() + + write_file_if_changed(file_path, json.dumps(output)) + print(f"Wrote {file_path}") + + +dump_schema() diff --git a/script/bump-version.py b/script/bump-version.py new file mode 100755 index 0000000000..1f034344f9 --- /dev/null +++ b/script/bump-version.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +import argparse +import re +import subprocess +from dataclasses import dataclass +import sys + + +@dataclass +class Version: + major: int + minor: int + patch: int + beta: int = 0 + dev: bool = False + + def __str__(self): + return f"{self.major}.{self.minor}.{self.full_patch}" + + @property + def full_patch(self): + res = f"{self.patch}" + if self.beta > 0: + res += f"b{self.beta}" + if self.dev: + res += "-dev" + return res + + @classmethod + def parse(cls, value): + match = re.match(r"(\d+).(\d+).(\d+)(b\d+)?(-dev)?", value) + assert match is not None + major = int(match[1]) + minor = int(match[2]) + patch = int(match[3]) + beta = int(match[4][1:]) if match[4] else 0 + dev = bool(match[5]) + return Version(major=major, minor=minor, patch=patch, beta=beta, dev=dev) + + +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: Version): + sub( + "esphome/const.py", + r"^__version__ = .*$", + f'__version__ = "{version}"', + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("new_version", type=str) + args = parser.parse_args() + + version = Version.parse(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/ci-custom.py b/script/ci-custom.py index b2b838cb5b..52ac4025ca 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,16 +1,26 @@ #!/usr/bin/env python3 +from helpers import styled, print_error_for_file, git_ls_files, filter_changed +import argparse import codecs import collections +import colorama import fnmatch +import functools import os.path import re -import subprocess import sys +import time + +sys.path.append(os.path.dirname(__file__)) def find_all(a_str, sub): - for i, line in enumerate(a_str.splitlines()): + if not a_str.find(sub): + # Optimization: If str is not in whole text, then do not try + # on each line + return + for i, line in enumerate(a_str.split('\n')): column = 0 while True: column = line.find(sub, column) @@ -20,22 +30,57 @@ def find_all(a_str, sub): column += len(sub) -command = ['git', 'ls-files', '-s'] -proc = subprocess.Popen(command, stdout=subprocess.PIPE) -output, err = proc.communicate() -lines = [x.split() for x in output.decode('utf-8').splitlines()] -EXECUTABLE_BIT = { - s[3].strip(): int(s[0]) for s in lines -} -files = [s[3].strip() for s in lines] -files = list(filter(os.path.exists, files)) +colorama.init() + +parser = argparse.ArgumentParser() +parser.add_argument( + "files", nargs="*", default=[], help="files to be processed (regex on path)" +) +parser.add_argument( + "-c", "--changed", action="store_true", help="Only run on changed files" +) +parser.add_argument( + "--print-slowest", action="store_true", help="Print the slowest checks" +) +args = parser.parse_args() + +EXECUTABLE_BIT = git_ls_files() +files = list(EXECUTABLE_BIT.keys()) +# Match against re +file_name_re = re.compile("|".join(args.files)) +files = [p for p in files if file_name_re.search(p)] + +if args.changed: + files = filter_changed(files) + files.sort() -file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg', - '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg', - '.woff', '.woff2', '') -cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc') -ignore_types = ('.ico', '.woff', '.woff2', '') +file_types = ( + ".h", + ".c", + ".cpp", + ".tcc", + ".yaml", + ".yml", + ".ini", + ".txt", + ".ico", + ".svg", + ".py", + ".html", + ".js", + ".md", + ".sh", + ".css", + ".proto", + ".conf", + ".cfg", + ".woff", + ".woff2", + "", +) +cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") +ignore_types = (".ico", ".woff", ".woff2", "") LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] @@ -43,9 +88,9 @@ LINT_POST_CHECKS = [] def run_check(lint_obj, fname, *args): - include = lint_obj['include'] - exclude = lint_obj['exclude'] - func = lint_obj['func'] + include = lint_obj["include"] + exclude = lint_obj["exclude"] + func = lint_obj["func"] if include is not None: for incl in include: if fnmatch.fnmatch(fname, incl): @@ -60,21 +105,31 @@ def run_check(lint_obj, fname, *args): def run_checks(lints, fname, *args): for lint in lints: - add_errors(fname, run_check(lint, fname, *args)) + start = time.process_time() + try: + add_errors(fname, run_check(lint, fname, *args)) + except Exception: + print(f"Check {lint['func'].__name__} on file {fname} failed:") + raise + duration = time.process_time() - start + lint.setdefault("durations", []).append(duration) def _add_check(checks, func, include=None, exclude=None): - checks.append({ - 'include': include, - 'exclude': exclude or [], - 'func': func, - }) + checks.append( + { + "include": include, + "exclude": exclude or [], + "func": func, + } + ) def lint_file_check(**kwargs): def decorator(func): _add_check(LINT_FILE_CHECKS, func, **kwargs) return func + return decorator @@ -82,6 +137,7 @@ def lint_content_check(**kwargs): def decorator(func): _add_check(LINT_CONTENT_CHECKS, func, **kwargs) return func + return decorator @@ -91,29 +147,36 @@ def lint_post_check(func): def lint_re_check(regex, **kwargs): - prog = re.compile(regex, re.MULTILINE) + flags = kwargs.pop("flags", re.MULTILINE) + prog = re.compile(regex, flags) decor = lint_content_check(**kwargs) def decorator(func): + @functools.wraps(func) def new_func(fname, content): errors = [] for match in prog.finditer(content): - if 'NOLINT' in match.group(0): + if "NOLINT" in match.group(0): continue lineno = content.count("\n", 0, match.start()) + 1 + substr = content[: match.start()] + col = len(substr) - substr.rfind("\n") err = func(fname, match) if err is None: continue - errors.append(f"{err} See line {lineno}.") + errors.append((lineno, col + 1, err)) return errors + return decor(new_func) + return decorator -def lint_content_find_check(find, **kwargs): +def lint_content_find_check(find, only_first=False, **kwargs): decor = lint_content_check(**kwargs) def decorator(func): + @functools.wraps(func) def new_func(fname, content): find_ = find if callable(find): @@ -121,74 +184,108 @@ def lint_content_find_check(find, **kwargs): errors = [] for line, col in find_all(content, find_): err = func(fname) - errors.append("{err} See line {line}:{col}." - "".format(err=err, line=line+1, col=col+1)) + errors.append((line + 1, col + 1, err)) + if only_first: + break return errors + return decor(new_func) + return decorator -@lint_file_check(include=['*.ino']) +@lint_file_check(include=["*.ino"]) def lint_ino(fname): return "This file extension (.ino) is not allowed. Please use either .cpp or .h" -@lint_file_check(exclude=[f'*{f}' for f in file_types] + [ - '.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc', - 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*', -]) +@lint_file_check( + exclude=[f"*{f}" for f in file_types] + + [ + ".clang-*", + ".dockerignore", + ".editorconfig", + "*.gitignore", + "LICENSE", + "pylintrc", + "MANIFEST.in", + "docker/Dockerfile*", + "docker/rootfs/*", + "script/*", + ] +) def lint_ext_check(fname): - return "This file extension is not a registered file type. If this is an error, please " \ - "update the script/ci-custom.py script." + return ( + "This file extension is not a registered file type. If this is an error, please " + "update the script/ci-custom.py script." + ) -@lint_file_check(exclude=[ - 'docker/rootfs/*', '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: - return 'File has invalid executable bit {}. If running from a windows machine please ' \ - 'see disabling executable bit in git.'.format(ex) + return ( + "File has invalid executable bit {}. If running from a windows machine please " + "see disabling executable bit in git.".format(ex) + ) return None -@lint_content_find_check('\t', exclude=[ - 'esphome/dashboard/static/ace.js', 'esphome/dashboard/static/ext-searchbox.js', -]) +@lint_content_find_check( + "\t", + only_first=True, + exclude=[ + "esphome/dashboard/static/ace.js", + "esphome/dashboard/static/ext-searchbox.js", + ], +) def lint_tabs(fname): return "File contains tab character. Please convert tabs to spaces." -@lint_content_find_check('\r') +@lint_content_find_check("\r", only_first=True) def lint_newline(fname): - return "File contains windows newline. Please set your editor to unix newline mode." + return "File contains Windows newline. Please set your editor to Unix newline mode." -@lint_content_check(exclude=['*.svg']) +@lint_content_check(exclude=["*.svg"]) def lint_end_newline(fname, content): - if content and not content.endswith('\n'): + if content and not content.endswith("\n"): return "File does not end with a newline, please add an empty line at the end of the file." return None -CPP_RE_EOL = r'\s*?(?://.*?)?$' +CPP_RE_EOL = r"\s*?(?://.*?)?$" def highlight(s): - return f'\033[36m{s}\033[0m' + return f"\033[36m{s}\033[0m" -@lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL, - include=cpp_include, exclude=['esphome/core/log.h']) +@lint_re_check( + r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL, + include=cpp_include, + exclude=[ + "esphome/core/log.h", + "esphome/components/socket/headers.h", + "esphome/core/defines.h", + ], +) def lint_no_defines(fname, match): - s = highlight('static const uint8_t {} = {};'.format(match.group(1), match.group(2))) - return ("#define macros for integer constants are not allowed, please use " - "{} style instead (replace uint8_t with the appropriate " - "datatype). See also Google style guide.".format(s)) + s = highlight( + "static const uint8_t {} = {};".format(match.group(1), match.group(2)) + ) + return ( + "#define macros for integer constants are not allowed, please use " + "{} style instead (replace uint8_t with the appropriate " + "datatype). See also Google style guide.".format(s) + ) -@lint_re_check(r'^\s*delay\((\d+)\);' + CPP_RE_EOL, include=cpp_include) +@lint_re_check(r"^\s*delay\((\d+)\);" + CPP_RE_EOL, include=cpp_include) def lint_no_long_delays(fname, match): duration_ms = int(match.group(1)) if duration_ms < 50: @@ -202,54 +299,171 @@ def lint_no_long_delays(fname, match): ) -@lint_content_check(include=['esphome/const.py']) +@lint_content_check(include=["esphome/const.py"]) def lint_const_ordered(fname, content): + """Lint that value in const.py are ordered. + + Reason: Otherwise people add it to the end, and then that results in merge conflicts. + """ lines = content.splitlines() errors = [] - for start in ['CONF_', 'ICON_', 'UNIT_']: - matching = [(i+1, line) for i, line in enumerate(lines) if line.startswith(start)] - ordered = list(sorted(matching, key=lambda x: x[1].replace('_', ' '))) + for start in ["CONF_", "ICON_", "UNIT_"]: + matching = [ + (i + 1, line) for i, line in enumerate(lines) if line.startswith(start) + ] + ordered = list(sorted(matching, key=lambda x: x[1].replace("_", " "))) ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)] for (mi, ml), (oi, ol) in zip(matching, ordered): if ml == ol: continue target = next(i for i, l in ordered if l == ml) target_text = next(l for i, l in matching if target == i) - errors.append("Constant {} is not ordered, please make sure all constants are ordered. " - "See line {} (should go to line {}, {})" - "".format(highlight(ml), mi, target, target_text)) + errors.append( + ( + mi, + 1, + f"Constant {highlight(ml)} is not ordered, please make sure all " + f"constants are ordered. See line {mi} (should go to line {target}, " + f"{target_text})", + ) + ) return errors -@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=['*.py']) +@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=["*.py"]) def lint_conf_matches(fname, match): const = match.group(1) value = match.group(2) const_norm = const.lower() - value_norm = value.replace('.', '_') + value_norm = value.replace(".", "_") if const_norm == value_norm: return None - return ("Constant {} does not match value {}! Please make sure the constant's name matches its " - "value!" - "".format(highlight('CONF_' + const), highlight(value))) + return ( + "Constant {} does not match value {}! Please make sure the constant's name matches its " + "value!" + "".format(highlight("CONF_" + const), highlight(value)) + ) CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$' -with codecs.open('esphome/const.py', 'r', encoding='utf-8') as f_handle: +with codecs.open("esphome/const.py", "r", encoding="utf-8") as f_handle: constants_content = f_handle.read() CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)] CONSTANTS_USES = collections.defaultdict(list) -@lint_re_check(CONF_RE, include=['*.py'], exclude=['esphome/const.py']) +@lint_re_check(CONF_RE, include=["*.py"], exclude=["esphome/const.py"]) def lint_conf_from_const_py(fname, match): name = match.group(1) if name not in CONSTANTS: CONSTANTS_USES[name].append(fname) return None - return ("Constant {} has already been defined in const.py - please import the constant from " - "const.py directly.".format(highlight(name))) + return ( + "Constant {} has already been defined in const.py - please import the constant from " + "const.py directly.".format(highlight(name)) + ) + + +RAW_PIN_ACCESS_RE = ( + r"^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)" +) + + +@lint_re_check(RAW_PIN_ACCESS_RE, include=cpp_include) +def lint_no_raw_pin_access(fname, match): + func = match.group(1) + pin = match.group(2) + mode = match.group(3) + new_func = { + "pinMode": "pin_mode", + "digitalWrite": "digital_write", + "digitalRead": "digital_read", + }[func] + new_code = highlight(f"{pin}->{new_func}({mode})") + return f"Don't use raw {func} calls. Instead, use the `->{new_func}` function: {new_code}" + + +# Functions from Arduino framework that are forbidden to use directly +ARDUINO_FORBIDDEN = [ + "digitalWrite", + "digitalRead", + "pinMode", + "shiftOut", + "shiftIn", + "radians", + "degrees", + "interrupts", + "noInterrupts", + "lowByte", + "highByte", + "bitRead", + "bitSet", + "bitClear", + "bitWrite", + "bit", + "analogRead", + "analogWrite", + "pulseIn", + "pulseInLong", + "tone", +] +ARDUINO_FORBIDDEN_RE = r"[^\w\d](" + r"|".join(ARDUINO_FORBIDDEN) + r")\(.*" + + +@lint_re_check( + ARDUINO_FORBIDDEN_RE, + include=cpp_include, + exclude=[ + "esphome/components/mqtt/custom_mqtt_device.h", + "esphome/components/sun/sun.cpp", + ], +) +def lint_no_arduino_framework_functions(fname, match): + nolint = highlight("// NOLINT") + return ( + f"The function {highlight(match.group(1))} from the Arduino framework is forbidden to be " + f"used directly in the ESPHome codebase. Please use ESPHome's abstractions and equivalent " + f"C++ instead.\n" + f"\n" + f"(If the function is strictly necessary, please add `{nolint}` to the end of the line)" + ) + + +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, + exclude={ + "esphome/components/tuya/tuya.h", + }, +) +def lint_no_byte_datatype(fname, match): + return ( + f"The datatype {highlight('byte')} is not allowed to be used in ESPHome. " + f"Please use {highlight('uint8_t')} instead." + ) @lint_post_check @@ -258,94 +472,152 @@ def lint_constants_usage(): for constant, uses in CONSTANTS_USES.items(): if len(uses) < 4: continue - errors.append("Constant {} is defined in {} files. Please move all definitions of the " - "constant to const.py (Uses: {})" - "".format(highlight(constant), len(uses), ', '.join(uses))) + errors.append( + "Constant {} is defined in {} files. Please move all definitions of the " + "constant to const.py (Uses: {})" + "".format(highlight(constant), len(uses), ", ".join(uses)) + ) return errors def relative_cpp_search_text(fname, content): - parts = fname.split('/') + parts = fname.split("/") integration = parts[2] return f'#include "esphome/components/{integration}' -@lint_content_find_check(relative_cpp_search_text, include=['esphome/components/*.cpp']) +@lint_content_find_check(relative_cpp_search_text, include=["esphome/components/*.cpp"]) def lint_relative_cpp_import(fname): - return ("Component contains absolute import - Components must always use " - "relative imports.\n" - "Change:\n" - ' #include "esphome/components/abc/abc.h"\n' - 'to:\n' - ' #include "abc.h"\n\n') + return ( + "Component contains absolute import - Components must always use " + "relative imports.\n" + "Change:\n" + ' #include "esphome/components/abc/abc.h"\n' + "to:\n" + ' #include "abc.h"\n\n' + ) def relative_py_search_text(fname, content): - parts = fname.split('/') + parts = fname.split("/") integration = parts[2] - return f'esphome.components.{integration}' + return f"esphome.components.{integration}" -@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'], - exclude=['esphome/components/web_server/__init__.py']) +@lint_content_find_check( + relative_py_search_text, + include=["esphome/components/*.py"], + exclude=["esphome/components/web_server/__init__.py"], +) def lint_relative_py_import(fname): - return ("Component contains absolute import - Components must always use " - "relative imports within the integration.\n" - "Change:\n" - ' from esphome.components.abc import abc_ns"\n' - 'to:\n' - ' from . import abc_ns\n\n') + return ( + "Component contains absolute import - Components must always use " + "relative imports within the integration.\n" + "Change:\n" + ' from esphome.components.abc import abc_ns"\n' + "to:\n" + " from . import abc_ns\n\n" + ) -@lint_content_check(include=['esphome/components/*.h', 'esphome/components/*.cpp', - 'esphome/components/*.tcc']) +@lint_content_check( + include=[ + "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(r'^esphome/components/([^/]+)/.*', - fname.replace(os.path.sep, '/')).group(1) - search = f'namespace {expected_name}' + expected_name = re.match( + r"^esphome/components/([^/]+)/.*", fname.replace(os.path.sep, "/") + ).group(1) + search = f"namespace {expected_name}" if search in content: return None - return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \ - 'functions in a separate namespace that matches the integration\'s name. ' \ - 'Please make sure the file contains {}'.format(highlight(search)) + return ( + "Invalid namespace found in C++ file. All integration C++ files should put all " + "functions in a separate namespace that matches the integration's name. " + "Please make sure the file contains {}".format(highlight(search)) + ) -@lint_content_find_check('"esphome.h"', include=cpp_include, exclude=['tests/custom.h']) +@lint_content_find_check('"esphome.h"', include=cpp_include, exclude=["tests/custom.h"]) def lint_esphome_h(fname): - return ("File contains reference to 'esphome.h' - This file is " - "auto-generated and should only be used for *custom* " - "components. Please replace with references to the direct files.") + return ( + "File contains reference to 'esphome.h' - This file is " + "auto-generated and should only be used for *custom* " + "components. Please replace with references to the direct files." + ) -@lint_content_check(include=['*.h']) +@lint_content_check(include=["*.h"]) def lint_pragma_once(fname, content): - if '#pragma once' not in content: - return ("Header file contains no 'pragma once' header guard. Please add a " - "'#pragma once' line at the top of the file.") + if "#pragma once" not in content: + return ( + "Header file contains no 'pragma once' header guard. Please add a " + "'#pragma once' line at the top of the file." + ) return None -@lint_content_find_check('ESP_LOG', include=['*.h', '*.tcc'], exclude=[ - 'esphome/components/binary_sensor/binary_sensor.h', - 'esphome/components/cover/cover.h', - 'esphome/components/display/display_buffer.h', - 'esphome/components/i2c/i2c.h', - 'esphome/components/mqtt/mqtt_component.h', - 'esphome/components/output/binary_output.h', - 'esphome/components/output/float_output.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/log.h', - 'tests/custom.h', -]) +@lint_re_check( + r"(whitelist|blacklist|slave)", + exclude=["script/ci-custom.py"], + flags=re.IGNORECASE | re.MULTILINE, +) +def lint_inclusive_language(fname, match): + # From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb + return ( + "Avoid the use of whitelist/blacklist/slave.\n" + "Recommended replacements for 'master / slave' are:\n" + " '{primary,main} / {secondary,replica,subordinate}\n" + " '{initiator,requester} / {target,responder}'\n" + " '{controller,host} / {device,worker,proxy}'\n" + " 'leader / follower'\n" + " 'director / performer'\n" + "\n" + "Recommended replacements for 'blacklist/whitelist' are:\n" + " 'denylist / allowlist'\n" + " 'blocklist / passlist'" + ) + + +@lint_content_find_check( + "ESP_LOG", + include=["*.h", "*.tcc"], + exclude=[ + "esphome/components/binary_sensor/binary_sensor.h", + "esphome/components/cover/cover.h", + "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/components/button/button.h", + "esphome/core/component.h", + "esphome/core/gpio.h", + "esphome/core/log.h", + "tests/custom.h", + ], +) def lint_log_in_header(fname): - return ('Found reference to ESP_LOG in header file. Using ESP_LOG* in header files ' - 'is currently not possible - please move the definition to a source file (.cpp)') + return ( + "Found reference to ESP_LOG in header file. Using ESP_LOG* in header files " + "is currently not possible - please move the definition to a source file (.cpp)" + ) errors = collections.defaultdict(list) @@ -354,13 +626,22 @@ errors = collections.defaultdict(list) def add_errors(fname, errs): if not isinstance(errs, list): errs = [errs] - errs = [x for x in errs if x is not None] for err in errs: - if not isinstance(err, str): + if err is None: + continue + try: + lineno, col, msg = err + except ValueError: + lineno = 1 + col = 1 + msg = err + if not isinstance(msg, str): raise ValueError("Error is not instance of string!") - if not errs: - return - errors[fname].extend(errs) + if not isinstance(lineno, int): + raise ValueError("Line number is not an int!") + if not isinstance(col, int): + raise ValueError("Column number is not an int!") + errors[fname].append((lineno, col, msg)) for fname in files: @@ -369,19 +650,33 @@ for fname in files: if ext in ignore_types: continue try: - with codecs.open(fname, 'r', encoding='utf-8') as f_handle: + with codecs.open(fname, "r", encoding="utf-8") as f_handle: content = f_handle.read() except UnicodeDecodeError: - add_errors(fname, "File is not readable as UTF-8. Please set your editor to UTF-8 mode.") + add_errors( + fname, + "File is not readable as UTF-8. Please set your editor to UTF-8 mode.", + ) continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) -run_checks(LINT_POST_CHECKS, 'POST') +run_checks(LINT_POST_CHECKS, "POST") for f, errs in sorted(errors.items()): - print(f"\033[0;32m************* File \033[1;32m{f}\033[0m") - for err in errs: - print(err) - print() + bold = functools.partial(styled, colorama.Style.BRIGHT) + bold_red = functools.partial(styled, (colorama.Style.BRIGHT, colorama.Fore.RED)) + err_str = (f"{bold(f'{f}:{lineno}:{col}:')} {bold_red('lint:')} {msg}\n" for lineno, col, msg in errs) + print_error_for_file(f, "\n".join(err_str)) + +if args.print_slowest: + lint_times = [] + for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS: + durations = lint.get("durations", []) + lint_times.append((sum(durations), len(durations), lint["func"].__name__)) + lint_times.sort(key=lambda x: -x[0]) + for i in range(min(len(lint_times), 10)): + dur, invocations, name = lint_times[i] + print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)") + print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s") sys.exit(len(errors)) diff --git a/script/clang-format b/script/clang-format index e9c3692bb8..515df4c027 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,81 +1,64 @@ #!/usr/bin/env python3 -from __future__ import print_function - +from helpers import print_error_for_file, get_output, git_ls_files, filter_changed import argparse +import click +import colorama import multiprocessing import os +import queue import re import subprocess import sys 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()) - - -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-7'] + 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_error_for_file(path, proc.stderr) + 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(): + colorama.init() + 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: - get_output('clang-format-7', '-version') + get_output('clang-format-11', '-version') except: print(""" Oops. It looks like clang-format is not installed. - Please check you can run "clang-format-7 -version" in your terminal and install - clang-format (v7) if necessary. + Please check you can run "clang-format-11 -version" in your terminal and install + 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 +66,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 +103,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 1005d15580..7450084634 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,61 +1,102 @@ #!/usr/bin/env python3 -from __future__ import print_function - +from helpers import print_error_for_file, get_output, filter_grep, \ + build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, basepath +import argparse +import click +import colorama import multiprocessing import os +import queue import re - -import pexpect import shutil import subprocess import sys import tempfile - -import argparse -import click import threading -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' +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_word(s)=(*(const uint16_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', + ] -if is_py2: - import Queue as queue -else: - import queue as queue + # 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, tmpdir, queue, lock, failed_files): +def run_tidy(args, options, tmpdir, queue, lock, failed_files): while True: path = queue.get() - invocation = ['clang-tidy-7', '-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_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: - print() - print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) - print(output) - print() + if args.quiet: + invocation.append('--quiet') + + if sys.stdout.isatty(): + invocation.append('--use-color') + + invocation.append(f"--header-filter={os.path.abspath(basepath)}/.*") + invocation.append(os.path.abspath(path)) + invocation.append('--') + invocation.extend(options) + + proc = subprocess.run(invocation, capture_output=True, encoding='utf-8') + if proc.returncode != 0: + with lock: + print_error_for_file(path, proc.stdout) failed_files.append(path) queue.task_done() @@ -65,56 +106,75 @@ def progress_bar_show(value): return '' +def split_list(a, n): + k, m = divmod(len(a), n) + return [a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)] + + def main(): + colorama.init() + parser = argparse.ArgumentParser() 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') + 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', + 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: - get_output('clang-tidy-7', '-version') + get_output('clang-tidy-11', '-version') except: print(""" - Oops. It looks like clang-tidy is not installed. - - Please check you can run "clang-tidy-7 -version" in your terminal and install - clang-tidy (v7) if necessary. - + Oops. It looks like clang-tidy-11 is not installed. + + Please check you can run "clang-tidy-11 -version" in your terminal and install + clang-tidy (v11) if necessary. + Note you can also upload your code as a pull request on GitHub and see the CI check output to apply clang-tidy. """) return 1 - 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.all_headers: + 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 @@ -122,13 +182,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() @@ -140,7 +199,6 @@ def main(): # Wait for all threads to be done. task_queue.join() - return_code = len(failed_files) except KeyboardInterrupt: print() @@ -152,12 +210,12 @@ def main(): if args.fix and failed_files: print('Applying fixes ...') try: - subprocess.call(['clang-apply-replacements-7', tmpdir]) + subprocess.call(['clang-apply-replacements-11', tmpdir]) except: print('Error applying fixes.\n', file=sys.stderr) raise - sys.exit(return_code) + sys.exit(len(failed_files)) if __name__ == '__main__': diff --git a/script/component_test b/script/component_test new file mode 100755 index 0000000000..549c68fb25 --- /dev/null +++ b/script/component_test @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +set -x + +pytest tests/component_tests 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/fulltest b/script/fulltest index 795482281a..a605beebfe 100755 --- a/script/fulltest +++ b/script/fulltest @@ -10,4 +10,5 @@ script/ci-custom.py script/lint-python script/lint-cpp script/unit_test +script/component_test script/test diff --git a/script/helpers.py b/script/helpers.py index c9bf5224b1..abf970b8a2 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,22 +1,28 @@ -import codecs -import json +import colorama 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') +root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) +basepath = os.path.join(root_path, "esphome") +temp_folder = os.path.join(root_path, ".temp") +temp_header_file = os.path.join(temp_folder, "all-include.cpp") -def shlex_quote(s): - if not s: - return "''" - if re.search(r'[^\w@%+=:,./-]', s) is None: - return s +def styled(color, msg, reset=True): + prefix = ''.join(color) if isinstance(color, tuple) else color + suffix = colorama.Style.RESET_ALL if reset else '' + return prefix + msg + suffix - return "'" + s.replace("'", "'\"'\"'") + "'" + +def print_error_for_file(file, body): + print(styled(colorama.Fore.GREEN, "### File ") + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file)) + print() + if body is not None: + print(body) + print() def build_all_include(): @@ -24,64 +30,18 @@ def build_all_include(): # Otherwise header-only integrations would not be tested by clang-tidy headers = [] for path in walk_files(basepath): - filetypes = ('.h',) + filetypes = (".h",) ext = os.path.splitext(path)[1] if ext in filetypes: path = os.path.relpath(path, root_path) - include_p = path.replace(os.path.sep, '/') + include_p = path.replace(os.path.sep, "/") headers.append(f'#include "{include_p}"') 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 - except: - pass - with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f: - json.dump(compile_commands, f, indent=2) + headers.append("") + content = "\n".join(headers) + p = Path(temp_header_file) + p.parent.mkdir(exist_ok=True) + p.write_text(content) def walk_files(path): @@ -93,7 +53,13 @@ def walk_files(path): def get_output(*args): proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, err = proc.communicate() - return output.decode('utf-8') + return output.decode("utf-8") + + +def get_err(*args): + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, err = proc.communicate() + return err.decode("utf-8") def splitlines_no_ends(string): @@ -101,16 +67,19 @@ def splitlines_no_ends(string): def changed_files(): - for remote in ('upstream', 'origin'): - command = ['git', 'merge-base', f'{remote}/dev', 'HEAD'] + check_remotes = ["upstream", "origin"] + check_remotes.extend(splitlines_no_ends(get_output("git", "remote"))) + for remote in check_remotes: + command = ["git", "merge-base", f"refs/remotes/{remote}/dev", "HEAD"] try: merge_base = splitlines_no_ends(get_output(*command))[0] break + # pylint: disable=bare-except except: pass else: raise ValueError("Git not configured") - command = ['git', 'diff', merge_base, '--name-only'] + command = ["git", "diff", merge_base, "--name-only"] changed = splitlines_no_ends(get_output(*command)) changed = [os.path.relpath(f, os.getcwd()) for f in changed] changed.sort() @@ -128,11 +97,55 @@ def filter_changed(files): return files -def git_ls_files(): - command = ['git', 'ls-files', '-s'] +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 - } + 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/script/lint-python b/script/lint-python index 4915115262..8ee038a661 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,16 +1,13 @@ #!/usr/bin/env python3 from __future__ import print_function - +from helpers import styled, print_error_for_file, get_output, get_err, git_ls_files, filter_changed import argparse -import collections +import colorama import os import re import sys -sys.path.append(os.path.dirname(__file__)) -from helpers import get_output, git_ls_files, filter_changed - curfile = None @@ -18,30 +15,36 @@ def print_error(file, lineno, msg): global curfile if curfile != file: - print() - print("\033[0;32m************* File \033[1;32m{}\033[0m".format(file)) + print_error_for_file(file, None) curfile = file - print(u'{}:{} - {}'.format(file, lineno, msg)) + if lineno is not None: + print(f"{styled(colorama.Style.BRIGHT, f'{file}:{lineno}:')} {msg}") + else: + print(f"{styled(colorama.Style.BRIGHT, f'{file}:')} {msg}") def main(): + colorama.init() + parser = argparse.ArgumentParser() - parser.add_argument('files', nargs='*', default=[], - help='files to be processed (regex on path)') - parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') + parser.add_argument( + "files", nargs="*", default=[], help="files to be processed (regex on path)" + ) + parser.add_argument( + "-c", "--changed", action="store_true", help="Only run on changed files" + ) args = parser.parse_args() files = [] for path in git_ls_files(): - filetypes = ('.py',) + filetypes = (".py",) ext = os.path.splitext(path)[1] - if ext in filetypes and path.startswith('esphome'): + if ext in filetypes and path.startswith("esphome"): path = os.path.relpath(path, os.getcwd()) files.append(path) # Match against re - file_name_re = re.compile('|'.join(args.files)) + file_name_re = re.compile("|".join(args.files)) files = [p for p in files if file_name_re.search(p)] if args.changed: @@ -52,34 +55,50 @@ def main(): sys.exit(0) errors = 0 - cmd = ['flake8'] + files + + cmd = ["black", "--verbose", "--check"] + files + print("Running black...") + print() + log = get_err(*cmd) + for line in log.splitlines(): + WOULD_REFORMAT = "would reformat" + if line.startswith(WOULD_REFORMAT): + file_ = line[len(WOULD_REFORMAT) + 1 :] + print_error(file_, None, "Please format this file with the black formatter") + errors += 1 + + cmd = ["flake8"] + files + print() print("Running flake8...") + print() log = get_output(*cmd) for line in log.splitlines(): - line = line.split(':', 4) + line = line.split(":", 4) if len(line) < 4: continue file_ = line[0] linno = line[1] - msg = (':'.join(line[3:])).strip() + msg = (":".join(line[3:])).strip() print_error(file_, linno, msg) errors += 1 - cmd = ['pylint', '-f', 'parseable', '--persistent=n'] + files + cmd = ["pylint", "-f", "parseable", "--persistent=n"] + files + print() print("Running pylint...") + print() log = get_output(*cmd) for line in log.splitlines(): - line = line.split(':', 3) + line = line.split(":", 3) if len(line) < 3: continue file_ = line[0] linno = line[1] - msg = (':'.join(line[2:])).strip() + msg = (":".join(line[2:])).strip() print_error(file_, linno, msg) errors += 1 sys.exit(errors) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/script/quicklint b/script/quicklint index e391ca3276..a4fae98195 100755 --- a/script/quicklint +++ b/script/quicklint @@ -6,6 +6,6 @@ cd "$(dirname "$0")/.." set -x -script/ci-custom.py +script/ci-custom.py -c script/lint-python -c script/lint-cpp -c diff --git a/script/setup b/script/setup index b6cff39f0c..6d095af46c 100755 --- a/script/setup +++ b/script/setup @@ -4,5 +4,7 @@ set -e cd "$(dirname "$0")/.." -pip install -r requirements_test.txt -pip install -e . +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt +pip3 install -e . + +pre-commit install diff --git a/script/test b/script/test index 5e91686aae..9f5dca65fa 100755 --- a/script/test +++ b/script/test @@ -6,6 +6,8 @@ cd "$(dirname "$0")/.." set -x -esphome tests/test1.yaml compile -esphome tests/test2.yaml compile -esphome tests/test3.yaml compile +esphome compile tests/test1.yaml +esphome compile tests/test2.yaml +esphome compile tests/test3.yaml +esphome compile tests/test4.yaml +esphome compile tests/test5.yaml diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000000..72ca3f6e9c --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,22 @@ +# 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 +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_TASK_WDT=y +CONFIG_ESP_TASK_WDT_PANIC=y +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n + +# esp32_ble +CONFIG_BT_ENABLED=y + +# esp32_camera +CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC=y +CONFIG_ESP32_SPIRAM_SUPPORT=y diff --git a/setup.cfg b/setup.cfg index 32a60839a5..755cef47c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,45 @@ Topic :: Home Automation [flake8] max-line-length = 120 +# Following 4 for black compatibility +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring + +# TODO fix flake8 +# D100 Missing docstring in public module +# D101 Missing docstring in public class +# D102 Missing docstring in public method +# D103 Missing docstring in public function +# D104 Missing docstring in public package +# D105 Missing docstring in magic method +# D107 Missing docstring in __init__ +# D200 One-line docstring should fit on one line with quotes +# D205 1 blank line required between summary line and description +# D209 Multi-line docstring closing quotes should be on a separate line +# D400 First line should end with a period +# D401 First line should be in imperative mood + +ignore = + E501, + W503, + E203, + D202, + + D100, + D101, + D102, + D103, + D104, + D105, + D107, + D200, + D205, + D209, + D400, + D401, + exclude = api_pb2.py [bdist_wheel] diff --git a/setup.py b/setup.py index f78ec49f28..967eadd70f 100755 --- a/setup.py +++ b/setup.py @@ -1,57 +1,55 @@ #!/usr/bin/env python3 """esphome setup script.""" -from setuptools import setup, find_packages import os +from setuptools import setup, find_packages + from esphome import const -PROJECT_NAME = 'esphome' -PROJECT_PACKAGE_NAME = 'esphome' -PROJECT_LICENSE = 'MIT' -PROJECT_AUTHOR = 'ESPHome' -PROJECT_COPYRIGHT = '2019, ESPHome' -PROJECT_URL = 'https://esphome.io/' -PROJECT_EMAIL = 'contact@esphome.io' +PROJECT_NAME = "esphome" +PROJECT_PACKAGE_NAME = "esphome" +PROJECT_LICENSE = "MIT" +PROJECT_AUTHOR = "ESPHome" +PROJECT_COPYRIGHT = "2019, ESPHome" +PROJECT_URL = "https://esphome.io/" +PROJECT_EMAIL = "esphome@nabucasa.com" -PROJECT_GITHUB_USERNAME = 'esphome' -PROJECT_GITHUB_REPOSITORY = 'esphome' +PROJECT_GITHUB_USERNAME = "esphome" +PROJECT_GITHUB_REPOSITORY = "esphome" -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) +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__) -REQUIRES = [ - 'voluptuous==0.11.7', - 'PyYAML==5.2', - 'paho-mqtt==1.5.0', - 'colorlog==4.0.2', - 'tornado==5.1.1', - 'protobuf==3.11.1', - 'tzlocal==2.0.0', - 'pytz==2019.3', - 'pyserial==3.4', - 'ifaddr==0.1.6', -] +here = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(here, "requirements.txt")) as requirements_txt: + REQUIRES = requirements_txt.read().splitlines() + +with open(os.path.join(here, "README.md")) as readme: + LONG_DESCRIPTION = readme.read() # If you have problems importing platformio and esptool as modules you can set # $ESPHOME_USE_SUBPROCESS to make ESPHome call their executables instead. # This means they have to be in your $PATH. -if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: - REQUIRES.extend([ - 'platformio==4.1.0', - 'esptool==2.7', - ]) +if "ESPHOME_USE_SUBPROCESS" in os.environ: + # Remove platformio and esptool from requirements + REQUIRES = [ + req + for req in REQUIRES + if not any(req.startswith(prefix) for prefix in ["platformio", "esptool"]) + ] CLASSIFIERS = [ - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: C++', - 'Programming Language :: Python :: 3', - 'Topic :: Home Automation', + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Topic :: Home Automation", ] setup( @@ -59,21 +57,26 @@ setup( version=const.__version__, license=PROJECT_LICENSE, url=GITHUB_URL, + project_urls={ + "Bug Tracker": "https://github.com/esphome/issues/issues", + "Feature Request Tracker": "https://github.com/esphome/feature-requests/issues", + "Source Code": "https://github.com/esphome/esphome", + "Documentation": "https://esphome.io", + "Twitter": "https://twitter.com/esphome_", + }, download_url=DOWNLOAD_URL, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, description="Make creating custom firmwares for ESP32/ESP8266 super easy.", + long_description=LONG_DESCRIPTION, + long_description_content_type="text/markdown", include_package_data=True, zip_safe=False, - platforms='any', - test_suite='tests', - python_requires='>=3.6,<4.0', + platforms="any", + test_suite="tests", + python_requires=">=3.7,<4.0", install_requires=REQUIRES, - keywords=['home', 'automation'], - entry_points={ - 'console_scripts': [ - 'esphome = esphome.__main__:main' - ] - }, - packages=find_packages() + keywords=["home", "automation"], + entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, + packages=find_packages(include="esphome.*"), ) diff --git a/tests/README.md b/tests/README.md index af650a2565..546025526f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,3 +9,18 @@ Of course this is all just very high-level and things like unit tests would be much better. So if you have time and know how to set up a unit testing framework for python, please do give it a try. + +When adding entries in test_.yaml files we usually need only +one file updated, unless conflicting code is generated for +different configurations, e.g. `wifi` and `ethernet` cannot +be tested on the same device. + +Current test_.yaml file contents. + +| Test name | Platform | Network | BLE | +|-|-|-|-| +| test1.yaml | ESP32 | wifi | None +| test2.yaml | ESP32 | ethernet | esp32_ble_tracker +| test3.yaml | ESP8266 | wifi | N/A +| test4.yaml | ESP32 | ethernet | None +| test5.yaml | ESP32 | wifi | ble_server diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py new file mode 100644 index 0000000000..514bc6ee5f --- /dev/null +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -0,0 +1,49 @@ +"""Tests for the binary sensor component.""" + + +def test_binary_sensor_is_setup(generate_main): + """ + When the binary sensor is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main( + "tests/component_tests/binary_sensor/test_binary_sensor.yaml" + ) + + # Then + assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "App.register_binary_sensor" in main_cpp + + +def test_binary_sensor_sets_mandatory_fields(generate_main): + """ + When the mandatory fields are set in the yaml, they should be set in main + """ + # Given + + # When + main_cpp = generate_main( + "tests/component_tests/binary_sensor/test_binary_sensor.yaml" + ) + + # Then + assert 'bs_1->set_name("test bs1");' in main_cpp + assert "bs_1->set_pin(" in main_cpp + + +def test_binary_sensor_config_value_internal_set(generate_main): + """ + Test that the "internal" config value is correctly set + """ + # Given + + # When + main_cpp = generate_main( + "tests/component_tests/binary_sensor/test_binary_sensor.yaml" + ) + + # Then + assert "bs_1->set_internal(true);" in main_cpp + assert "bs_2->set_internal(false);" in main_cpp diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.yaml b/tests/component_tests/binary_sensor/test_binary_sensor.yaml new file mode 100644 index 0000000000..912ae115eb --- /dev/null +++ b/tests/component_tests/binary_sensor/test_binary_sensor.yaml @@ -0,0 +1,18 @@ +esphome: + name: test + platform: ESP8266 + board: d1_mini_lite + +binary_sensor: + - platform: gpio + id: bs_1 + name: "test bs1" + internal: true + pin: + number: D0 + - platform: gpio + id: bs_2 + name: "test bs2" + internal: false + pin: + number: D1 diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py new file mode 100644 index 0000000000..aa564ed7b1 --- /dev/null +++ b/tests/component_tests/conftest.py @@ -0,0 +1,31 @@ +"""Fixtures for component tests.""" + +import sys +from pathlib import Path + +# Add package root to python path +here = Path(__file__).parent +package_root = here.parent.parent +sys.path.insert(0, package_root.as_posix()) + +import pytest + +from esphome.core import CORE +from esphome.config import read_config +from esphome.__main__ import generate_cpp_contents + + +@pytest.fixture +def generate_main(): + """Generates the C++ main.cpp file and returns it in string form.""" + + def generator(path: str) -> str: + CORE.config_path = path + CORE.config = read_config({}) + generate_cpp_contents(CORE.config) + print(CORE.cpp_main_section) + return CORE.cpp_main_section + + yield generator + + CORE.reset() diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py new file mode 100644 index 0000000000..690d323a50 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -0,0 +1,52 @@ +"""Tests for the deep sleep component.""" + + +def test_deep_sleep_setup(generate_main): + """ + When the deep sleep is set in the yaml file, it should be registered in main + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" + ) + + assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp + assert "App.register_component(deepsleep);" in main_cpp + + +def test_deep_sleep_sleep_duration(generate_main): + """ + When deep sleep is configured with sleep duration, it should be set. + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" + ) + + assert "deepsleep->set_sleep_duration(60000);" in main_cpp + + +def test_deep_sleep_run_duration_simple(generate_main): + """ + When deep sleep is configured with run duration, it should be set. + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" + ) + + assert "deepsleep->set_run_duration(10000);" in main_cpp + + +def test_deep_sleep_run_duration_dictionary(generate_main): + """ + When deep sleep is configured with dictionary run duration, it should be set. + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep2.yaml" + ) + + assert ( + "deepsleep->set_run_duration(deep_sleep::WakeupCauseToRunDuration{\n" + " .default_cause = 10000,\n" + " .touch_cause = 10000,\n" + " .gpio_cause = 30000,\n" + "});" + ) in main_cpp diff --git a/tests/component_tests/deep_sleep/test_deep_sleep1.yaml b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml new file mode 100644 index 0000000000..18a425df58 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + platform: ESP32 + board: nodemcu-32s + +deep_sleep: + id: deepsleep + sleep_duration: 1min + run_duration: 10s diff --git a/tests/component_tests/deep_sleep/test_deep_sleep2.yaml b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml new file mode 100644 index 0000000000..49a7f510f2 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + platform: ESP32 + board: nodemcu-32s + +deep_sleep: + id: deepsleep + sleep_duration: 1min + run_duration: + default: 10s + gpio_wakeup_reason: 30s diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py new file mode 100644 index 0000000000..35ce1f4e11 --- /dev/null +++ b/tests/component_tests/sensor/test_sensor.py @@ -0,0 +1,14 @@ +"""Tests for the sensor component.""" + + +def test_sensor_device_class_set(generate_main): + """ + When the device_class of sensor is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") + + # Then + assert 's_1->set_device_class("voltage");' in main_cpp diff --git a/tests/component_tests/sensor/test_sensor.yaml b/tests/component_tests/sensor/test_sensor.yaml new file mode 100644 index 0000000000..a38dd14041 --- /dev/null +++ b/tests/component_tests/sensor/test_sensor.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + platform: ESP8266 + board: d1_mini_lite + +sensor: + - platform: adc + pin: A0 + id: s_1 + name: "test s1" + update_interval: 60s + device_class: "voltage" diff --git a/tests/custom.h b/tests/custom.h index 278e300785..f1a35b9b3c 100644 --- a/tests/custom.h +++ b/tests/custom.h @@ -49,6 +49,7 @@ class CustomNativeAPI : public Component, public CustomAPIDevice { register_service(&CustomNativeAPI::on_start_dryer, "start_dryer", {"value"}); register_service(&CustomNativeAPI::on_many_values, "many_values", {"bool", "int", "float", "str1", "str2"}); subscribe_homeassistant_state(&CustomNativeAPI::on_light_state, "light.my_light"); + subscribe_homeassistant_state(&CustomNativeAPI::on_brightness_state, "light.my_light", "brightness"); } void on_hello_world() { ESP_LOGD("custom_api", "Hello World from native API service!"); } @@ -69,4 +70,5 @@ class CustomNativeAPI : public Component, public CustomAPIDevice { call_homeassistant_service("homeassistant.restart"); } void on_light_state(std::string state) { ESP_LOGD("custom_api", "Got state %s", state.c_str()); } + void on_brightness_state(std::string state) { ESP_LOGD("custom_api", "Got attribute state %s", state.c_str()); } }; diff --git a/tests/livingroom8266.cpp b/tests/dummy_main.cpp similarity index 56% rename from tests/livingroom8266.cpp rename to tests/dummy_main.cpp index ad017ce73b..d956387665 100644 --- a/tests/livingroom8266.cpp +++ b/tests/dummy_main.cpp @@ -1,3 +1,8 @@ +// Dummy main.cpp file for the PlatformIO project in the git repository. +// Primarily used to get IDE integration working (so the contents here don't +// matter at all, as long as it compiles). +// Not used during runtime nor for CI. + #include #include #include @@ -7,24 +12,20 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", __DATE__ " " __TIME__); - auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0); + App.pre_setup("livingroom", __DATE__ ", " __TIME__, false); + 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(8266); - ota->start_safe_mode(); - - auto *gpio = new gpio::GPIOSwitch("GPIO Switch", new GPIOPin(8, OUTPUT)); - App.register_component(gpio); - App.register_switch(gpio); + auto *ota = new ota::OTAComponent(); // NOLINT + ota->set_port(8266); App.setup(); } diff --git a/tests/livingroom32.cpp b/tests/livingroom32.cpp deleted file mode 100644 index 7005ec95e0..0000000000 --- a/tests/livingroom32.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include - -using namespace esphome; - -void setup() { - App.set_name("livingroom32"); - App.init_log(); - - App.init_wifi("YOUR_SSID", "YOUR_PASSWORD"); - App.init_mqtt("MQTT_HOST", "USERNAME", "PASSWORD"); - App.init_ota()->start_safe_mode(); - - // LEDC is only available on ESP32! for the ESP8266, take a look at App.make_esp8266_pwm_output(). - auto *red = App.make_ledc_output(32); // on pin 32 - auto *green = App.make_ledc_output(33); - auto *blue = App.make_ledc_output(34); - App.make_rgb_light("Livingroom Light", red, green, blue); - - App.make_dht_sensor("Livingroom Temperature", "Livingroom Humidity", 12); - App.make_status_binary_sensor("Livingroom Node Status"); - App.make_restart_switch("Livingroom Restart"); - - App.setup(); -} - -void loop() { App.loop(); } diff --git a/tests/test1.yaml b/tests/test1.yaml index 18adcf70a3..7494146d1e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1,7 +1,16 @@ +substitutions: + devicename: test1 + sensorname: my + textname: template + roomname: living_room + esphome: name: test1 + name_add_mac_suffix: true platform: ESP32 board: nodemcu-32s + platformio_options: + board_build.partitions: huge_app.csv on_boot: priority: 150.0 then: @@ -23,16 +32,45 @@ esphome: green: !lambda 'return 255;' blue: 0% white: 100% + - http_request.get: + url: https://esphome.io + headers: + Content-Type: application/json + verify_ssl: false + - http_request.post: + url: https://esphome.io + verify_ssl: false + json: + key: !lambda |- + return id(${textname}_text).state; + greeting: 'Hello World' + - http_request.send: + method: PUT + url: https://esphome.io + headers: + Content-Type: application/json + body: 'Some data' + verify_ssl: false + on_response: + then: + - logger.log: + format: 'Response status: %d' + args: + - status_code build_path: build/test1 +packages: + wifi: !include test_packages/test_packages_package_wifi.yaml + pkg_test: !include test_packages/test_packages_package1.yaml + wifi: networks: - - ssid: 'MySSID' - password: 'password1' - - ssid: 'MySSID2' - password: '' - channel: 14 - bssid: 'A1:63:95:47:D3:1D' + - ssid: 'MySSID' + password: 'password1' + - ssid: 'MySSID2' + password: '' + channel: 14 + bssid: 'A1:63:95:47:D3:1D' manual_ip: static_ip: 192.168.178.230 gateway: 192.168.178.1 @@ -41,7 +79,14 @@ 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 + timeout: 10s mqtt: broker: '192.168.178.84' @@ -49,9 +94,11 @@ mqtt: username: 'debug' password: 'debug' client_id: someclient + use_abbreviations: false discovery: True discovery_retain: False discovery_prefix: discovery + discovery_unique_id_generator: legacy topic_prefix: helloworld log_topic: topic: helloworld/hi @@ -73,47 +120,53 @@ mqtt: ESP_LOGD("main", "Got message %s", x.c_str()); - topic: livingroom/ota_mode then: - - deep_sleep.prevent + - deep_sleep.prevent - topic: livingroom/ota_mode then: - - deep_sleep.enter: + - deep_sleep.enter: on_json_message: topic: the/topic then: - - if: - condition: - - wifi.connected: - - mqtt.connected: - - light.is_on: kitchen - - light.is_off: kitchen - then: - - lambda: |- - int data = x["my_data"]; - ESP_LOGD("main", "The data is: %d", data); - - light.turn_on: - id: living_room_lights - brightness: !lambda |- - float brightness = 1.0; - if (x.containsKey("brightness")) - brightness = x["brightness"]; - return brightness; - effect: !lambda |- - const char *effect = "None"; - if (x.containsKey("effect")) - effect = x["effect"]; - return effect; - - light.control: - id: living_room_lights - brightness: !lambda 'return id(living_room_lights).current_values.get_brightness() + 0.5;' - - light.dim_relative: - id: living_room_lights - relative_brightness: 5% - - uart.write: - id: uart0 - data: Hello World - - uart.write: [0x00, 0x20, 0x30] - - uart.write: !lambda |- - return {}; + - if: + condition: + - wifi.connected: + - mqtt.connected: + - light.is_on: kitchen + - light.is_off: kitchen + - fan.is_on: fan_speed + - fan.is_off: fan_speed + then: + - lambda: |- + int data = x["my_data"]; + ESP_LOGD("main", "The data is: %d", data); + - light.turn_on: + id: ${roomname}_lights + brightness: !lambda |- + float brightness = 1.0; + if (x.containsKey("brightness")) + brightness = x["brightness"]; + return brightness; + effect: !lambda |- + const char *effect = "None"; + if (x.containsKey("effect")) + effect = x["effect"]; + return effect; + - light.control: + id: ${roomname}_lights + brightness: !lambda 'return id(${roomname}_lights).current_values.get_brightness() + 0.5;' + - light.dim_relative: + id: ${roomname}_lights + relative_brightness: 5% + - uart.write: + id: uart0 + data: Hello World + - uart.write: + id: uart0 + data: [0x00, 0x20, 0x30] + - uart.write: + id: uart0 + data: !lambda |- + return {}; i2c: sda: 21 @@ -121,6 +174,7 @@ i2c: scan: True frequency: 100kHz setup_priority: -100 + id: i2c_bus spi: clk_pin: GPIO21 @@ -128,15 +182,61 @@ spi: miso_pin: GPIO23 uart: - tx_pin: GPIO22 - rx_pin: GPIO23 - baud_rate: 115200 - id: uart0 + - 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 + debug: + dummy_receiver: true + direction: both + after: + bytes: 50 + timeout: 500ms + delimiter: "\r\n" + sequence: + - lambda: UARTDebug::log_hex(direction, bytes, ':'); + - lambda: UARTDebug::log_string(direction, bytes); + - lambda: UARTDebug::log_int(direction, bytes, ','); + - lambda: UARTDebug::log_binary(direction, bytes, ';'); + + - id: adalight_uart + tx_pin: GPIO25 + rx_pin: GPIO26 + baud_rate: 115200 + rx_buffer_size: 1024 ota: safe_mode: True password: 'superlongpasswordthatnoonewillknow' port: 3286 + reboot_timeout: 2min + num_attempts: 5 + on_state_change: + then: + lambda: >- + ESP_LOGD("ota", "State %d", state); + on_begin: + then: + logger.log: "OTA begin" + on_progress: + then: + lambda: >- + ESP_LOGD("ota", "Got progress %f", x); + on_end: + then: + logger.log: "OTA end" + on_error: + then: + lambda: >- + ESP_LOGD("ota", "Got error code %d", x); logger: baud_rate: 0 @@ -147,6 +247,7 @@ logger: web_server: port: 8080 + ota: true css_url: https://esphome.io/_static/webserver-v1.min.css js_url: https://esphome.io/_static/webserver-v1.min.js @@ -166,6 +267,7 @@ deep_sleep: ads1115: address: 0x48 + i2c_id: i2c_bus dallas: pin: GPIO23 @@ -174,14 +276,56 @@ as3935_spi: cs_pin: GPIO12 irq_pin: GPIO13 +wled: + +adalight: + +esp32_ble_tracker: + +ble_client: + - mac_address: AA:BB:CC:DD:EE:FF + id: ble_foo + - mac_address: 11:22:33:44:55:66 + id: ble_blah + on_connect: + then: + - switch.turn_on: ble1_status + on_disconnect: + then: + - switch.turn_on: ble1_status +mcp23s08: + - id: 'mcp23s08_hub' + cs_pin: GPIO12 + deviceaddress: 0 + +mcp23s17: + - id: 'mcp23s17_hub' + cs_pin: GPIO12 + deviceaddress: 1 + sensor: + - platform: ble_client + ble_client_id: ble_foo + name: 'Green iTag btn' + service_uuid: 'ffe0' + characteristic_uuid: 'ffe1' + descriptor_uuid: 'ffe2' + notify: true + update_interval: never + lambda: |- + ESP_LOGD("main", "Length of data is %i", x.size()); + return x[0]; + on_notify: + then: + - lambda: |- + ESP_LOGD("green_btn", "Button was pressed, val%f", x); - platform: adc pin: A0 - name: "Living Room Brightness" + name: 'Living Room Brightness' update_interval: '1:01' attenuation: 2.5db - unit_of_measurement: "°C" - icon: "mdi:water-percent" + unit_of_measurement: '°C' + icon: 'mdi:water-percent' accuracy_decimals: 5 expire_after: 120s setup_priority: -100 @@ -190,15 +334,23 @@ sensor: - offset: 2.0 - multiply: 1.2 - calibrate_linear: - - 0.0 -> 0.0 - - 40.0 -> 45.0 - - 100.0 -> 102.5 + - 0.0 -> 0.0 + - 40.0 -> 45.0 + - 100.0 -> 102.5 - filter_out: 42.0 - filter_out: nan - median: window_size: 5 send_every: 5 send_first_at: 3 + - min: + window_size: 5 + send_every: 5 + send_first_at: 3 + - max: + window_size: 5 + send_every: 5 + send_first_at: 3 - sliding_window_moving_average: window_size: 15 send_every: 15 @@ -206,37 +358,41 @@ sensor: - exponential_moving_average: alpha: 0.1 send_every: 15 + - throttle_average: 60s - throttle: 1s - heartbeat: 5s - debounce: 0.1s - delta: 5.0 - or: - - throttle: 1s - - delta: 5.0 + - throttle: 1s + - delta: 5.0 - lambda: return x * (9.0/5.0) + 32.0; on_value: then: - lambda: |- ESP_LOGD("main", "Got value %f", x); - id(my_sensor).publish_state(42.0); - ESP_LOGI("main", "Value of my sensor: %f", id(my_sensor).state); - ESP_LOGI("main", "Raw Value of my sensor: %f", id(my_sensor).state); + id(${sensorname}_sensor).publish_state(42.0); + ESP_LOGI("main", "Value of my sensor: %f", id(${sensorname}_sensor).state); + ESP_LOGI("main", "Raw Value of my sensor: %f", id(${sensorname}_sensor).state); on_value_range: above: 5 below: 10 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); - logger.log: level: DEBUG - format: "Got raw value %f" + format: 'Got raw value %f' args: ['x'] - - logger.log: "Got raw value NAN" + - logger.log: 'Got raw value NAN' - mqtt.publish: topic: some/topic payload: Hello @@ -247,7 +403,7 @@ sensor: - platform: ads1115 multiplexer: 'A0_A1' gain: 1.024 - id: my_sensor + id: ${sensorname}_sensor filters: state_topic: hi/me retain: false @@ -256,48 +412,48 @@ sensor: cs_pin: 5 phase_a: voltage: - name: "EMON Line Voltage A" + name: 'EMON Line Voltage A' current: - name: "EMON CT1 Current" + name: 'EMON CT1 Current' power: - name: "EMON Active Power CT1" + name: 'EMON Active Power CT1' reactive_power: - name: "EMON Reactive Power CT1" + name: 'EMON Reactive Power CT1' power_factor: - name: "EMON Power Factor CT1" + name: 'EMON Power Factor CT1' gain_voltage: 7305 gain_ct: 27961 phase_b: current: - name: "EMON CT2 Current" + name: 'EMON CT2 Current' power: - name: "EMON Active Power CT2" + name: 'EMON Active Power CT2' reactive_power: - name: "EMON Reactive Power CT2" + name: 'EMON Reactive Power CT2' power_factor: - name: "EMON Power Factor CT2" + name: 'EMON Power Factor CT2' gain_voltage: 7305 gain_ct: 27961 phase_c: current: - name: "EMON CT3 Current" + name: 'EMON CT3 Current' power: - name: "EMON Active Power CT3" + name: 'EMON Active Power CT3' reactive_power: - name: "EMON Reactive Power CT3" + name: 'EMON Reactive Power CT3' power_factor: - name: "EMON Power Factor CT3" + name: 'EMON Power Factor CT3' gain_voltage: 7305 gain_ct: 27961 frequency: - name: "EMON Line Frequency" + name: 'EMON Line Frequency' chip_temperature: - name: "EMON Chip Temp A" + name: 'EMON Chip Temp A' line_frequency: 60Hz current_phases: 3 gain_pga: 2X - platform: bh1750 - name: "Living Room Brightness 3" + name: 'Living Room Brightness 3' internal: true address: 0x23 resolution: 1.0 @@ -305,136 +461,155 @@ sensor: retain: False availability: state_topic: livingroom/custom_state_topic + measurement_duration: 31 + i2c_id: i2c_bus - platform: bme280 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' oversampling: 16x pressure: - name: "Outside Pressure" + name: 'Outside Pressure' oversampling: none humidity: - name: "Outside Humidity" + name: 'Outside Humidity' oversampling: 8x address: 0x77 iir_filter: 16x update_interval: 15s + i2c_id: i2c_bus - platform: bme680 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' oversampling: 16x pressure: - name: "Outside Pressure" + name: 'Outside Pressure' humidity: - name: "Outside Humidity" + name: 'Outside Humidity' gas_resistance: - name: "Outside Gas Sensor" + name: 'Outside Gas Sensor' address: 0x77 heater: temperature: 320 duration: 150ms update_interval: 15s + i2c_id: i2c_bus - platform: bmp085 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' pressure: - name: "Outside Pressure" + name: 'Outside Pressure' filters: - lambda: >- return x / powf(1.0 - (x / 44330.0), 5.255); update_interval: 15s + i2c_id: i2c_bus - platform: bmp280 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' oversampling: 16x pressure: - name: "Outside Pressure" + name: 'Outside Pressure' address: 0x77 update_interval: 15s iir_filter: 16x + i2c_id: i2c_bus - platform: dallas address: 0x1C0000031EDD2A28 - name: "Living Room Temperature" + name: 'Living Room Temperature' resolution: 9 - platform: dallas index: 1 - name: "Living Room Temperature 2" + name: 'Living Room Temperature 2' - platform: dht pin: GPIO26 temperature: - name: "Living Room Temperature 3" + name: 'Living Room Temperature 3' humidity: - name: "Living Room Humidity 3" + name: 'Living Room Humidity 3' model: AM2302 update_interval: 15s - platform: dht12 temperature: - name: "Living Room Temperature 4" + name: 'Living Room Temperature 4' humidity: - name: "Living Room Humidity 4" + name: 'Living Room Humidity 4' update_interval: 15s + i2c_id: i2c_bus - platform: duty_cycle pin: GPIO25 name: Duty Cycle Sensor - platform: esp32_hall - name: "ESP32 Hall Sensor" + name: 'ESP32 Hall Sensor' update_interval: 15s - platform: hdc1080 temperature: - name: "Living Room Temperature 5" + name: 'Living Room Temperature 5' humidity: - name: "Living Room Pressure 5" + name: 'Living Room Pressure 5' update_interval: 15s + i2c_id: i2c_bus - platform: hlw8012 sel_pin: 5 cf_pin: 14 cf1_pin: 13 current: - name: "HLW8012 Current" + name: 'HLW8012 Current' voltage: - name: "HLW8012 Voltage" + name: 'HLW8012 Voltage' power: - name: "HLW8012 Power" + name: 'HLW8012 Power' id: hlw8012_power + energy: + name: 'HLW8012 Energy' + id: hlw8012_energy update_interval: 15s current_resistor: 0.001 ohm voltage_divider: 2351 change_mode_every: 16 initial_mode: VOLTAGE + model: hlw8012 - platform: total_daily_energy power_id: hlw8012_power - name: "HLW8012 Total Daily Energy" + name: 'HLW8012 Total Daily Energy' - platform: integration sensor: hlw8012_power - name: "Integration Sensor" + 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: - name: "HMC5883L Field Strength X" + name: 'HMC5883L Field Strength X' field_strength_y: - name: "HMC5883L Field Strength Y" + name: 'HMC5883L Field Strength Y' field_strength_z: - name: "HMC5883L Field Strength Z" + name: 'HMC5883L Field Strength Z' heading: - name: "HMC5883L Heading" + name: 'HMC5883L Heading' range: 130uT oversampling: 8x update_interval: 15s + i2c_id: i2c_bus - platform: qmc5883l address: 0x0D field_strength_x: - name: "QMC5883L Field Strength X" + name: 'QMC5883L Field Strength X' field_strength_y: - name: "QMC5883L Field Strength Y" + name: 'QMC5883L Field Strength Y' field_strength_z: - name: "QMC5883L Field Strength Z" + name: 'QMC5883L Field Strength Z' heading: - name: "QMC5883L Heading" + name: 'QMC5883L Heading' range: 800uT oversampling: 256x update_interval: 15s + i2c_id: i2c_bus - platform: hx711 - name: "HX711 Value" + name: 'HX711 Value' dout_pin: GPIO23 clk_pin: GPIO25 gain: 128 @@ -443,112 +618,158 @@ sensor: address: 0x40 shunt_resistance: 0.1 ohm current: - name: "INA219 Current" + name: 'INA219 Current' power: - name: "INA219 Power" + name: 'INA219 Power' bus_voltage: - name: "INA219 Bus Voltage" + name: 'INA219 Bus Voltage' shunt_voltage: - name: "INA219 Shunt Voltage" + name: 'INA219 Shunt Voltage' max_voltage: 32.0V max_current: 3.2A update_interval: 15s + i2c_id: i2c_bus - platform: ina226 address: 0x40 shunt_resistance: 0.1 ohm current: - name: "INA226 Current" + name: 'INA226 Current' power: - name: "INA226 Power" + name: 'INA226 Power' bus_voltage: - name: "INA226 Bus Voltage" + name: 'INA226 Bus Voltage' shunt_voltage: - name: "INA226 Shunt Voltage" + name: 'INA226 Shunt Voltage' max_current: 3.2A update_interval: 15s + i2c_id: i2c_bus - platform: ina3221 address: 0x40 channel_1: shunt_resistance: 0.1 ohm current: - name: "INA3221 Channel 1 Current" + name: 'INA3221 Channel 1 Current' power: - name: "INA3221 Channel 1 Power" + name: 'INA3221 Channel 1 Power' bus_voltage: - name: "INA3221 Channel 1 Bus Voltage" + name: 'INA3221 Channel 1 Bus Voltage' shunt_voltage: - name: "INA3221 Channel 1 Shunt Voltage" + name: 'INA3221 Channel 1 Shunt Voltage' update_interval: 15s + i2c_id: i2c_bus - platform: htu21d temperature: - name: "Living Room Temperature 6" + name: 'Living Room Temperature 6' humidity: - name: "Living Room Humidity 6" + name: 'Living Room Humidity 6' update_interval: 15s + i2c_id: i2c_bus - platform: max6675 - name: "Living Room Temperature" + name: 'Living Room Temperature' cs_pin: GPIO23 update_interval: 15s - platform: max31855 - name: "Den Temperature" + name: 'Den Temperature' cs_pin: GPIO23 update_interval: 15s reference_temperature: - name: "MAX31855 Internal Temperature" + name: 'MAX31855 Internal Temperature' + - platform: max31856 + name: 'BBQ Temperature' + cs_pin: GPIO17 + update_interval: 15s + mains_filter: 50Hz - platform: max31865 - name: "Water Tank Temperature" + name: 'Water Tank Temperature' cs_pin: GPIO23 update_interval: 15s - reference_resistance: "430 Ω" - rtd_nominal_resistance: "100 Ω" + reference_resistance: '430 Ω' + rtd_nominal_resistance: '100 Ω' - platform: mhz19 + uart_id: uart0 co2: - name: "MH-Z19 CO2 Value" + name: 'MH-Z19 CO2 Value' temperature: - name: "MH-Z19 Temperature" + name: 'MH-Z19 Temperature' update_interval: 15s automatic_baseline_calibration: false - platform: mpu6050 address: 0x68 accel_x: - name: "MPU6050 Accel X" + name: 'MPU6050 Accel X' accel_y: - name: "MPU6050 Accel Y" + name: 'MPU6050 Accel Y' accel_z: - name: "MPU6050 Accel z" + name: 'MPU6050 Accel z' gyro_x: - name: "MPU6050 Gyro X" + name: 'MPU6050 Gyro X' gyro_y: - name: "MPU6050 Gyro Y" + name: 'MPU6050 Gyro Y' gyro_z: - name: "MPU6050 Gyro z" + name: 'MPU6050 Gyro z' temperature: - name: "MPU6050 Temperature" + name: 'MPU6050 Temperature' + i2c_id: i2c_bus - platform: ms5611 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' pressure: - name: "Outside Pressure" + name: 'Outside Pressure' address: 0x77 update_interval: 15s + i2c_id: i2c_bus + - platform: pmsa003i + pm_1_0: + name: "PMSA003i PM1.0" + pm_2_5: + name: "PMSA003i PM2.5" + pm_10_0: + name: "PMSA003i PM10.0" + pmc_0_3: + name: "PMSA003i PMC <0.3µm" + pmc_0_5: + name: "PMSA003i PMC <0.5µm" + pmc_1_0: + name: "PMSA003i PMC <1µm" + pmc_2_5: + name: "PMSA003i PMC <2.5µm" + pmc_5_0: + name: "PMSA003i PMC <5µm" + pmc_10_0: + name: "PMSA003i PMC <10µm" + address: 0x12 + standard_units: True + i2c_id: i2c_bus - platform: pulse_counter - name: "Pulse Counter" + name: 'Pulse Counter' pin: GPIO12 count_mode: rising_edge: INCREMENT falling_edge: DECREMENT internal_filter: 13us update_interval: 15s + - platform: pulse_meter + name: 'Pulse Meter' + id: pulse_meter_sensor + pin: GPIO12 + internal_filter: 100ms + timeout: 2 min + on_value: + - pulse_meter.set_total_pulses: + id: pulse_meter_sensor + value: 12345 + total: + name: 'Pulse Meter Total' - platform: rotary_encoder - name: "Rotary Encoder" + name: 'Rotary Encoder' id: rotary_encoder1 pin_a: GPIO23 pin_b: GPIO25 pin_reset: GPIO25 filters: - or: - - debounce: 0.1s - - delta: 10 + - debounce: 0.1s + - delta: 10 resolution: 4 min_value: -10 max_value: 30 @@ -559,82 +780,129 @@ sensor: - sensor.rotary_encoder.set_value: id: rotary_encoder1 value: !lambda 'return -1;' + on_clockwise: + - logger.log: 'Clockwise' + on_anticlockwise: + - logger.log: 'Anticlockwise' - platform: pulse_width name: Pulse Width pin: GPIO12 - - platform: senseair + - platform: sm300d2 + uart_id: uart0 co2: - name: "SenseAir CO2 Value" - update_interval: 15s + name: 'SM300D2 CO2 Value' + formaldehyde: + name: 'SM300D2 Formaldehyde Value' + tvoc: + name: 'SM300D2 TVOC Value' + pm_2_5: + name: 'SM300D2 PM2.5 Value' + pm_10_0: + name: 'SM300D2 PM10 Value' + temperature: + name: 'SM300D2 Temperature Value' + humidity: + name: 'SM300D2 Humidity Value' + update_interval: 60s - platform: sht3xd temperature: - name: "Living Room Temperature 8" + name: 'Living Room Temperature 8' humidity: - name: "Living Room Humidity 8" + name: 'Living Room Humidity 8' address: 0x44 + i2c_id: i2c_bus update_interval: 15s - platform: sts3x - name: "Living Room Temperature 9" + name: 'Living Room Temperature 9' address: 0x4A + i2c_id: i2c_bus - platform: scd30 co2: - name: "Living Room CO2 9" + name: 'Living Room CO2 9' temperature: - name: "Living Room Temperature 9" + name: 'Living Room Temperature 9' humidity: - name: "Living Room Humidity 9" + name: 'Living Room Humidity 9' address: 0x61 update_interval: 15s automatic_self_calibration: true altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + i2c_id: i2c_bus + - platform: scd4x + 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" + name: 'Workshop eCO2' accuracy_decimals: 1 tvoc: - name: "Workshop TVOC" + name: 'Workshop TVOC' accuracy_decimals: 1 address: 0x58 update_interval: 5s + i2c_id: i2c_bus - platform: sps30 pm_1_0: - name: "Workshop PM <1µm Weight concentration" - id: "workshop_PM_1_0" + name: 'Workshop PM <1µm Weight concentration' + id: 'workshop_PM_1_0' pm_2_5: - name: "Workshop PM <2.5µm Weight concentration" - id: "workshop_PM_2_5" + name: 'Workshop PM <2.5µm Weight concentration' + id: 'workshop_PM_2_5' pm_4_0: - name: "Workshop PM <4µm Weight concentration" - id: "workshop_PM_4_0" + name: 'Workshop PM <4µm Weight concentration' + id: 'workshop_PM_4_0' pm_10_0: - name: "Workshop PM <10µm Weight concentration" - id: "workshop_PM_10_0" + name: 'Workshop PM <10µm Weight concentration' + id: 'workshop_PM_10_0' pmc_0_5: - name: "Workshop PM <0.5µm Number concentration" - id: "workshop_PMC_0_5" + name: 'Workshop PM <0.5µm Number concentration' + id: 'workshop_PMC_0_5' pmc_1_0: - name: "Workshop PM <1µm Number concentration" - id: "workshop_PMC_1_0" + name: 'Workshop PM <1µm Number concentration' + id: 'workshop_PMC_1_0' pmc_2_5: - name: "Workshop PM <2.5µm Number concentration" - id: "workshop_PMC_2_5" + name: 'Workshop PM <2.5µm Number concentration' + id: 'workshop_PMC_2_5' pmc_4_0: - name: "Workshop PM <4µm Number concentration" - id: "workshop_PMC_4_0" + name: 'Workshop PM <4µm Number concentration' + id: 'workshop_PMC_4_0' pmc_10_0: - name: "Workshop PM <10µm Number concentration" - id: "workshop_PMC_10_0" + name: 'Workshop PM <10µm Number concentration' + id: 'workshop_PMC_10_0' address: 0x69 update_interval: 10s + i2c_id: i2c_bus + - platform: sht4x + temperature: + name: 'SHT4X Temperature' + humidity: + name: 'SHT4X Humidity' + address: 0x44 + update_interval: 15s + i2c_id: i2c_bus - platform: shtcx temperature: - name: "Living Room Temperature 10" + name: 'Living Room Temperature 10' humidity: - name: "Living Room Humidity 10" + name: 'Living Room Humidity 10' address: 0x70 update_interval: 15s + i2c_id: i2c_bus - platform: template - name: "Template Sensor" + name: 'Template Sensor' + state_class: measurement id: template_sensor lambda: |- if (id(ultrasonic_sensor1).state > 1) { @@ -651,28 +919,49 @@ sensor: id: template_sensor state: !lambda 'return NAN;' - platform: tsl2561 - name: "TSL2561 Ambient Light" + name: 'TSL2561 Ambient Light' address: 0x39 update_interval: 15s is_cs_package: true integration_time: 402ms gain: 16x + i2c_id: i2c_bus + - platform: tsl2591 + id: this_little_light_of_mine + address: 0x29 + update_interval: 15s + integration_time: 600ms + gain: high + visible: + name: "tsl2591 visible" + id: tsl2591_vis + unit_of_measurement: 'pH' + infrared: + name: "tsl2591 infrared" + id: tsl2591_ir + full_spectrum: + name: "tsl2591 full_spectrum" + id: tsl2591_fs + calculated_lux: + name: "tsl2591 calculated_lux" + id: tsl2591_cl + i2c_id: i2c_bus - platform: ultrasonic trigger_pin: GPIO25 echo_pin: number: GPIO23 inverted: true - name: "Ultrasonic Sensor" + name: 'Ultrasonic Sensor' timeout: 5.5m id: ultrasonic_sensor1 - platform: uptime name: Uptime Sensor - platform: wifi_signal - name: "WiFi Signal Sensor" + name: 'WiFi Signal Sensor' update_interval: 15s - platform: mqtt_subscribe - name: "MQTT Subscribe Sensor 1" - topic: "mqtt/topic" + name: 'MQTT Subscribe Sensor 1' + topic: 'mqtt/topic' id: the_sensor qos: 2 on_value: @@ -682,10 +971,11 @@ sensor: root["key"] = id(the_sensor).state; root["greeting"] = "Hello World"; - platform: sds011 + uart_id: uart0 pm_2_5: - name: "SDS011 PM2.5" + name: 'SDS011 PM2.5' pm_10_0: - name: "SDS011 PM10.0" + name: 'SDS011 PM10.0' update_interval: 5min rx_only: false - platform: ccs811 @@ -695,11 +985,12 @@ sensor: name: CCS811 TVOC update_interval: 30s baseline: 0x4242 + i2c_id: i2c_bus - platform: tx20 wind_speed: - name: "Windspeed" + name: 'Windspeed' wind_direction_degrees: - name: "Winddirection Degrees" + name: 'Winddirection Degrees' pin: number: GPIO04 mode: INPUT @@ -707,26 +998,74 @@ sensor: clock_pin: GPIO5 data_pin: GPIO4 co2: - name: "ZyAura CO2" + name: 'ZyAura CO2' temperature: - name: "ZyAura Temperature" + name: 'ZyAura Temperature' humidity: - name: "ZyAura Humidity" + name: 'ZyAura Humidity' - platform: as3935 lightning_energy: - name: "Lightning Energy" + name: 'Lightning Energy' distance: - name: "Distance Storm" + name: 'Distance Storm' - platform: tmp117 - name: "TMP117 Temperature" + name: 'TMP117 Temperature' update_interval: 5s + i2c_id: i2c_bus - platform: hm3301 pm_1_0: - name: "PM1.0" + name: 'PM1.0' pm_2_5: - name: "PM2.5" + name: 'PM2.5' pm_10_0: - name: "PM10.0" + name: 'PM10.0' + aqi: + name: 'AQI' + calculation_type: 'CAQI' + i2c_id: i2c_bus + - platform: teleinfo + tag_name: "HCHC" + name: "hchc" + unit_of_measurement: "Wh" + icon: mdi:flash + teleinfo_id: myteleinfo + - platform: mcp9808 + name: 'MCP9808 Temperature' + update_interval: 15s + i2c_id: i2c_bus + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: 'pH' + i2c_id: i2c_bus + - platform: sdp3x + name: "HVAC Filter Pressure drop" + id: filter_pressure + update_interval: 5s + accuracy_decimals: 3 + i2c_id: i2c_bus + - platform: cs5460a + id: cs5460a1 + current: + name: "Socket current" + voltage: + name: "Mains voltage" + power: + name: "Socket power" + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: on + voltage_hpf: on + phase_offset: 20 + pulse_energy: 0.01 kWh + cs_pin: + mcp23xxx: mcp23017_hub + number: 14 esp32_touch: setup_mode: False @@ -738,9 +1077,37 @@ esp32_touch: voltage_attenuation: 1.5V binary_sensor: + - platform: gpio + name: 'MCP23S08 Pin #1' + pin: + mcp23xxx: mcp23s08_hub + # Use pin number 1 + number: 1 + # One of INPUT or INPUT_PULLUP + mode: INPUT_PULLUP + inverted: False + - platform: gpio + name: 'MCP23S17 Pin #1' + pin: + mcp23xxx: mcp23s17_hub + # Use pin number 1 + number: 1 + # One of INPUT or INPUT_PULLUP + mode: INPUT_PULLUP + inverted: False + - platform: gpio + name: 'MCP23S17 Pin #1 with interrupt' + pin: + mcp23xxx: mcp23s17_hub + # Use pin number 1 + number: 1 + # One of INPUT or INPUT_PULLUP + mode: INPUT_PULLUP + inverted: False + interrupt: FALLING - platform: gpio pin: GPIO9 - name: "Living Room Window" + name: 'Living Room Window' device_class: window filters: - invert: @@ -767,63 +1134,59 @@ binary_sensor: - min_length: 50ms max_length: 350ms then: - - lambda: >- - ESP_LOGD("main", "Double Clicked"); + - lambda: >- + ESP_LOGD("main", "Double Clicked"); - then: - - lambda: >- - ESP_LOGD("main", "Double Clicked"); + - lambda: >- + ESP_LOGD("main", "Double Clicked"); on_multi_click: - - timing: - - ON for at most 1s - - OFF for at most 1s - - ON for at most 1s - - OFF for at least 0.2s - then: - - logger.log: - format: "Multi Clicked TWO" - level: warn - - timing: - - OFF for 1s to 2s - - ON for 1s to 2s - - OFF for at least 0.5s - then: - - logger.log: - format: "Multi Clicked LONG SINGLE" - level: warn - - timing: - - ON for at most 1s - - OFF for at least 0.5s - then: - - logger.log: - format: "Multi Clicked SINGLE" - level: warn + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.2s + then: + - logger.log: + format: 'Multi Clicked TWO' + level: warn + - timing: + - OFF for 1s to 2s + - ON for 1s to 2s + - OFF for at least 0.5s + then: + - logger.log: + format: 'Multi Clicked LONG SINGLE' + level: warn + - timing: + - ON for at most 1s + - OFF for at least 0.5s + then: + - logger.log: + format: 'Multi Clicked SINGLE' + level: warn id: binary_sensor1 - platform: gpio pin: number: GPIO9 mode: INPUT_PULLUP - name: "Living Room Window 2" + name: 'Living Room Window 2' - platform: status - name: "Living Room Status" + name: 'Living Room Status' - platform: esp32_touch - name: "ESP32 Touch Pad GPIO27" + name: 'ESP32 Touch Pad GPIO27' 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" + name: 'Garage Door Open' id: garage_door lambda: |- - if (isnan(id(my_sensor).state)) { + if (isnan(id(${sensorname}_sensor).state)) { // isnan checks if the ultrasonic sensor echo // has timed out, resulting in a NaN (not a number) state // in that case, return {} to indicate that we don't know. return {}; - } else if (id(my_sensor).state > 30) { + } else if (id(${sensorname}_sensor).state > 30) { // Garage Door is open. return true; } else { @@ -841,34 +1204,35 @@ binary_sensor: id: gpio_19 frequency: !lambda 'return 500.0;' - platform: pn532 + pn532_id: pn532_bs uid: 74-10-37-94 - name: "PN532 NFC Tag" + name: 'PN532 NFC Tag' - platform: rdm6300 uid: 7616525 - name: "RDM6300 NFC Tag" + name: 'RDM6300 NFC Tag' - platform: gpio - name: "PCF binary sensor" + name: 'PCF binary sensor' pin: pcf8574: pcf8574_hub number: 1 mode: INPUT inverted: True - platform: gpio - name: "MCP21 binary sensor" + name: 'MCP21 binary sensor' pin: - mcp23017: mcp23017_hub + mcp23xxx: mcp23017_hub number: 1 mode: INPUT inverted: True - platform: gpio - name: "MCP22 binary sensor" + name: 'MCP22 binary sensor' pin: - mcp23008: mcp23008_hub + mcp23xxx: mcp23008_hub number: 7 mode: INPUT_PULLUP inverted: False - platform: gpio - name: "MCP23 binary sensor" + name: 'MCP23 binary sensor' pin: mcp23016: mcp23016_hub number: 7 @@ -876,26 +1240,64 @@ binary_sensor: inverted: False - platform: remote_receiver - name: "Raw Remote Receiver Test" + name: 'Raw Remote Receiver Test' raw: - code: [5685, -4252, 1711, -2265, 1712, -2265, 1711, -2264, 1712, -2266, - 3700, -2263, 1712, -4254, 1711, -4249, 1715, -2266, 1710, -2267, - 1709, -2265, 3704, -4250, 1712, -4254, 3700, -2260, 1714, -2265, - 1712, -2262, 1714, -2267, 1709] + code: + [ + 5685, + -4252, + 1711, + -2265, + 1712, + -2265, + 1711, + -2264, + 1712, + -2266, + 3700, + -2263, + 1712, + -4254, + 1711, + -4249, + 1715, + -2266, + 1710, + -2267, + 1709, + -2265, + 3704, + -4250, + 1712, + -4254, + 3700, + -2260, + 1714, + -2265, + 1712, + -2262, + 1714, + -2267, + 1709, + ] - platform: as3935 - name: "Storm Alert" + name: 'Storm Alert' 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 @@ -986,14 +1388,14 @@ output: - platform: gpio id: id22 pin: - mcp23017: mcp23017_hub + mcp23xxx: mcp23017_hub number: 0 mode: OUTPUT inverted: False - platform: gpio id: id23 pin: - mcp23008: mcp23008_hub + mcp23xxx: mcp23008_hub number: 0 mode: OUTPUT inverted: False @@ -1030,15 +1432,23 @@ output: id: dimmer1 gate_pin: GPIO5 zero_cross_pin: GPIO26 + - platform: esp32_dac + pin: GPIO25 + id: dac_output + - platform: mcp4725 + id: mcp4725_dac_output + i2c_id: i2c_bus + +e131: light: - platform: binary - name: "Desk Lamp" + name: 'Desk Lamp' output: gpio_26 effects: - strobe: - strobe: - name: "My Strobe" + name: 'My Strobe' colors: - state: True duration: 250ms @@ -1053,7 +1463,7 @@ light: id: livingroom_lights state: yes - platform: monochromatic - name: "Kitchen Lights" + name: 'Kitchen Lights' id: kitchen output: gpio_19 gamma_correct: 2.8 @@ -1062,7 +1472,7 @@ light: - strobe: - flicker: - flicker: - name: "My Flicker" + name: 'My Flicker' alpha: 98% intensity: 1.5% - lambda: @@ -1074,19 +1484,20 @@ light: if (state == 4) state = 0; - platform: rgb - name: "Living Room Lights" - id: living_room_lights + name: 'Living Room Lights' + id: ${roomname}_lights red: pca_0 green: pca_1 blue: pca_2 - platform: rgbw - name: "Living Room Lights 2" + name: 'Living Room Lights 2' red: pca_3 green: pca_4 blue: pca_5 white: pca_6 + color_interlock: true - platform: rgbww - name: "Living Room Lights 2" + name: 'Living Room Lights 2' red: pca_3 green: pca_4 blue: pca_5 @@ -1094,13 +1505,30 @@ light: warm_white: pca_6 cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds + color_interlock: true + - platform: rgbct + name: 'Living Room Lights 2' + red: pca_3 + green: pca_4 + blue: pca_5 + color_temperature: pca_6 + white_brightness: pca_6 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true - platform: cwww - name: "Living Room Lights 2" + name: 'Living Room Lights 2' cold_white: pca_6 warm_white: pca_6 cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds constant_brightness: true + - platform: color_temperature + name: 'Living Room Lights 2' + color_temperature: pca_6 + brightness: pca_6 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds - platform: fastled_clockless id: addr1 chipset: WS2811 @@ -1110,96 +1538,105 @@ light: max_refresh_rate: 20ms power_supply: atx_power_supply color_correct: [75%, 100%, 50%] - name: "FastLED WS2811 Light" + name: 'FastLED WS2811 Light' effects: - - addressable_color_wipe: - - addressable_color_wipe: - name: Color Wipe Effect With Custom Values - colors: - - red: 100% - green: 100% - blue: 100% - num_leds: 1 - - red: 0% - green: 0% - blue: 0% - num_leds: 1 - add_led_interval: 100ms - reverse: False - - addressable_scan: - - addressable_scan: - name: Scan Effect With Custom Values - move_interval: 100ms - - addressable_twinkle: - - addressable_twinkle: - name: Twinkle Effect With Custom Values - twinkle_probability: 5% - progress_interval: 4ms - - addressable_random_twinkle: - - addressable_random_twinkle: - name: Random Twinkle Effect With Custom Values - twinkle_probability: 5% - progress_interval: 32ms - - addressable_fireworks: - - addressable_fireworks: - name: Fireworks Effect With Custom Values - update_interval: 32ms - spark_probability: 10% - use_random_color: false - fade_out_rate: 120 - - addressable_flicker: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% - - addressable_lambda: - name: "Test For Custom Lambda Effect" + - addressable_color_wipe: + - addressable_color_wipe: + name: Color Wipe Effect With Custom Values + colors: + - red: 100% + green: 100% + blue: 100% + num_leds: 1 + - red: 0% + green: 0% + blue: 0% + num_leds: 1 + add_led_interval: 100ms + reverse: False + - addressable_scan: + - addressable_scan: + name: Scan Effect With Custom Values + move_interval: 100ms + - addressable_twinkle: + - addressable_twinkle: + name: Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 4ms + - addressable_random_twinkle: + - addressable_random_twinkle: + name: Random Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 32ms + - addressable_fireworks: + - addressable_fireworks: + name: Fireworks Effect With Custom Values + update_interval: 32ms + spark_probability: 10% + use_random_color: false + fade_out_rate: 120 + - addressable_flicker: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + - addressable_lambda: + name: 'Test For Custom Lambda Effect' lambda: |- if (initial_run) { it[0] = current_color; } - - automation: - name: Custom Effect - sequence: - - light.addressable_set: - id: addr1 - red: 100% - green: 100% - blue: 0% - - delay: 100ms - - light.addressable_set: - id: addr1 - red: 0% - green: 100% - blue: 0% + - wled: + port: 11111 + + - adalight: + uart_id: adalight_uart + + - automation: + name: Custom Effect + sequence: + - light.addressable_set: + id: addr1 + red: 100% + green: 100% + blue: 0% + - delay: 100ms + - light.addressable_set: + id: addr1 + red: 0% + green: 100% + blue: 0% + - e131: + universe: 1 - platform: fastled_spi id: addr2 chipset: WS2801 data_pin: GPIO23 clock_pin: GPIO22 + data_rate: 2MHz num_leds: 60 rgb_order: BRG - name: "FastLED SPI Light" + name: 'FastLED SPI Light' - platform: neopixelbus id: addr3 - name: "Neopixelbus Light" + name: 'Neopixelbus Light' gamma_correct: 2.8 color_correct: [0.0, 0.0, 0.0, 0.0] default_transition_length: 10s power_supply: atx_power_supply effects: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% type: GRBW variant: SK6812 method: ESP32_I2S_0 num_leds: 60 pin: GPIO23 - platform: partition - name: "Partition Light" + name: 'Partition Light' segments: - id: addr1 from: 0 @@ -1210,6 +1647,7 @@ light: - id: addr2 from: 20 to: 25 + - single_light_id: ${roomname}_lights remote_transmitter: - pin: 32 @@ -1220,14 +1658,30 @@ climate: name: TCL112 Climate With Sensor supports_heat: True supports_cool: True - sensor: my_sensor + sensor: ${sensorname}_sensor - platform: tcl112 name: TCL112 Climate + action_state_topic: action/state/topic + away_command_topic: away/command/topic + away_state_topic: away/state/topic + current_temperature_state_topic: current/temperature/state/topic + fan_mode_command_topic: fan_mode/mode/command/topic + fan_mode_state_topic: fan_mode/mode/state/topic + mode_command_topic: mode/command/topic + mode_state_topic: mode/state/topic + swing_mode_command_topic: swing_mode/command/topic + swing_mode_state_topic: swing_mode/state/topic + target_temperature_command_topic: target/temperature/command/topic + target_temperature_high_command_topic: target/temperature/high/command/topic + target_temperature_high_state_topic: target/temperature/high/state/topic + target_temperature_low_command_topic: target/temperature/low/command/topic + target_temperature_low_state_topic: target/temperature/low/state/topic + target_temperature_state_topic: target/temperature/state/topic - platform: coolix name: Coolix Climate With Sensor supports_heat: True supports_cool: True - sensor: my_sensor + sensor: ${sensorname}_sensor - platform: coolix name: Coolix Climate - platform: fujitsu_general @@ -1240,12 +1694,120 @@ climate: name: Mitsubishi - platform: whirlpool name: Whirlpool Climate + - platform: climate_ir_lg + name: LG Climate + - platform: toshiba + name: Toshiba Climate + - platform: hitachi_ac344 + name: Hitachi Climate + - platform: heatpumpir + protocol: mitsubishi_heavy_zm + horizontal_default: left + vertical_default: up + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 + - platform: midea + on_state: + logger.log: "State changed!" + 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: + mcp23xxx: mcp23s08_hub + # Use pin number 0 + number: 0 + mode: OUTPUT + inverted: False + - platform: gpio + name: 'MCP23S17 Pin #0' + pin: + mcp23xxx: mcp23s17_hub + # Use pin number 0 + number: 1 + mode: OUTPUT + inverted: False - platform: gpio pin: GPIO25 - name: "Living Room Dehumidifier" - icon: "mdi:restart" + name: 'Living Room Dehumidifier' + icon: 'mdi:restart' inverted: True command_topic: custom_command_topic restore_mode: ALWAYS_OFF @@ -1273,6 +1835,18 @@ switch: turn_on_action: remote_transmitter.transmit_samsung: data: 0xABCDEF + - platform: template + name: Samsung36 + turn_on_action: + remote_transmitter.transmit_samsung36: + address: 0x0400 + command: 0x000E00FF + - platform: template + name: ToshibaAC + turn_on_action: + - remote_transmitter.transmit_toshiba_ac: + rc_code_1: 0xB24DBF4050AF + rc_code_2: 0xD5660001003C - platform: template name: Sony turn_on_action: @@ -1351,29 +1925,40 @@ switch: optimistic: True assumed_state: yes turn_on_action: - - switch.turn_on: living_room_lights_on - - output.set_level: - id: gpio_19 - level: 50% - - output.set_level: - id: gpio_19 - level: !lambda 'return 0.5;' + - switch.turn_on: living_room_lights_on + - output.set_level: + id: gpio_19 + level: 50% + - output.set_level: + id: gpio_19 + level: !lambda 'return 0.5;' + - output.set_level: + id: dac_output + level: 50% + - output.set_level: + id: dac_output + level: !lambda 'return 0.5;' + - output.set_level: + id: mcp4725_dac_output + level: !lambda 'return 0.5;' turn_off_action: - - switch.turn_on: living_room_lights_off + - switch.turn_on: living_room_lights_off restore_state: False on_turn_on: - switch.template.publish: id: livingroom_lights state: yes - platform: restart - name: "Living Room Restart" + name: 'Living Room Restart' + - platform: safe_mode + name: 'Living Room Restart (Safe Mode)' - platform: shutdown - name: "Living Room Shutdown" + name: 'Living Room Shutdown' - platform: output - name: "Generic Output" + name: 'Generic Output' output: pca_6 - platform: template - name: "Template Switch" + name: 'Template Switch' id: my_switch lambda: |- if (id(binary_sensor1).state) { @@ -1398,132 +1983,280 @@ switch: id: my_switch state: !lambda 'return false;' - platform: uart - name: "UART String Output" + uart_id: uart0 + name: 'UART String Output' data: 'DataToSend' - platform: uart - name: "UART Bytes Output" + uart_id: uart0 + name: 'UART Bytes Output' data: [0xDE, 0xAD, 0xBE, 0xEF] + - platform: uart + uart_id: uart0 + name: 'UART Recurring Output' + data: [0xDE, 0xAD, 0xBE, 0xEF] + send_every: 1s - platform: template assumed_state: yes name: Stepper Switch turn_on_action: - - stepper.set_target: - id: my_stepper - target: !lambda |- - static int32_t i = 0; - i += 1000; - if (i > 5000) { - i = -5000; - } - return i; - - stepper.report_position: - id: my_stepper - position: 0 + - stepper.set_target: + id: my_stepper + target: !lambda |- + static int32_t i = 0; + i += 1000; + if (i > 5000) { + i = -5000; + } + return i; + - stepper.report_position: + id: my_stepper + position: 0 + + - platform: gpio + name: 'SN74HC595 Pin #0' + pin: + sn74hc595: sn74hc595_hub + # Use pin number 0 + number: 0 + inverted: False + - platform: template + id: ble1_status + optimistic: true fan: - platform: binary output: gpio_26 - name: "Living Room Fan 1" + name: 'Living Room Fan 1' + oscillation_output: gpio_19 + direction_output: gpio_26 - platform: speed + id: fan_speed + icon: mdi:weather-windy output: pca_6 - name: "Living Room Fan 2" - speed: - low: 0.45 - medium: 0.75 - high: 1.0 + speed_count: 10 + name: 'Living Room Fan 2' + oscillation_output: gpio_19 + direction_output: gpio_26 + oscillation_state_topic: oscillation/state/topic + oscillation_command_topic: oscillation/command/topic + speed_level_state_topic: speed_level/state/topic + speed_level_command_topic: speed_level/command/topic + speed_state_topic: speed/state/topic + speed_command_topic: speed/command/topic + on_speed_set: + then: + - logger.log: "Fan speed was changed!" interval: - interval: 10s then: - - display.page.show: !lambda |- - if (true) return id(page1); else return id(page2); - - display.page.show_next: display1 - - display.page.show_previous: display1 + - display.page.show: !lambda |- + if (true) return id(page1); else return id(page2); + - display.page.show_next: display1 + - display.page.show_previous: display1 - interval: 2s then: - lambda: |- - static uint16_t btn_left_state = id(btn_left)->get_value(); + static uint16_t btn_left_state = id(btn_left)->get_value(); - ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); + ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); - btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; + btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; - id(btn_left)->set_threshold(btn_left_state * 0.9); + id(btn_left)->set_threshold(btn_left_state * 0.9); + - if: + condition: + display.is_displaying_page: + id: display1 + page_id: page1 + then: + - logger.log: 'Seeing page 1' + +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% display: -- platform: lcd_gpio - dimensions: 18x4 - data_pins: - - GPIO19 - - GPIO21 - - GPIO22 - - GPIO23 - enable_pin: GPIO23 - rs_pin: GPIO25 - lambda: |- - it.print("Hello World!"); -- platform: lcd_pcf8574 - dimensions: 18x4 - address: 0x3F - lambda: |- - it.print("Hello World!"); -- platform: max7219 - cs_pin: GPIO23 - num_chips: 1 - lambda: |- - it.print("01234567"); -- platform: tm1637 - clk_pin: GPIO23 - dio_pin: GPIO25 - intensity: 3 - lambda: |- + - platform: lcd_gpio + dimensions: 18x4 + data_pins: + - GPIO19 + - GPIO21 + - GPIO22 + - GPIO23 + enable_pin: GPIO23 + rs_pin: GPIO25 + lambda: |- + it.print("Hello World!"); + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + lambda: |- + it.print("Hello World!"); + i2c_id: i2c_bus + - platform: max7219 + cs_pin: GPIO23 + num_chips: 1 + lambda: |- + it.print("01234567"); + - platform: tm1637 + clk_pin: GPIO23 + dio_pin: GPIO25 + intensity: 3 + lambda: |- it.print("1234"); -- platform: nextion - lambda: |- - it.set_component_value("gauge", 50); - it.set_component_text("textview", "Hello World!"); -- platform: pcd8544 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: ssd1306_i2c - model: "SSD1306_128X64" - reset_pin: GPIO23 - address: 0x3C - id: display1 - brightness: 60% - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 - lambda: |- - // Nothing -- platform: ssd1306_spi - model: "SSD1306 128x64" - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: ssd1325_spi - model: "SSD1325 128x64" - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: waveshare_epaper - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 - model: 2.90in - full_update_every: 30 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: tm1637 + clk_pin: + mcp23xxx: mcp23017_hub + number: 1 + dio_pin: + mcp23xxx: mcp23017_hub + number: 2 + intensity: 3 + inverted: true + length: 4 + lambda: |- + it.print("1234"); + - platform: pcd8544 + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1306_i2c + model: 'SSD1306_128X64' + reset_pin: GPIO23 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + // Nothing + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); + i2c_id: i2c_bus + - platform: ssd1306_spi + model: 'SSD1306 128x64' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1322_spi + model: 'SSD1322 256x64' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1325_spi + model: 'SSD1325 128x64' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1327_i2c + model: 'SSD1327 128X128' + reset_pin: GPIO23 + address: 0x3D + id: display1327 + brightness: 60% + pages: + - id: page13271 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page13272 + lambda: |- + // Nothing + i2c_id: i2c_bus + - platform: ssd1327_spi + model: 'SSD1327 128x128' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1331_spi + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1351_spi + model: 'SSD1351 128x128' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7789v + cs_pin: GPIO5 + dc_pin: GPIO16 + reset_pin: GPIO23 + backlight_pin: GPIO4 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7920 + width: 128 + height: 64 + cs_pin: + number: GPIO23 + inverted: true + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7735 + model: 'INITR_BLACKTAB' + cs_pin: GPIO5 + dc_pin: GPIO16 + reset_pin: GPIO23 + rotation: 0 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9341 + model: "TFT 2.4" + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: GPIO22 + led_pin: + number: GPIO15 + inverted: true + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9341 + model: "TFT 2.4" + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: GPIO22 + led_pin: + number: GPIO15 + inverted: true + auto_clear_enabled: false + rotation: 90 + lambda: |- + if (!id(glob_bool_processed)) { + it.fill(Color::WHITE); + id(glob_bool_processed) = true; + } tm1651: id: tm1651_battery @@ -1537,7 +2270,8 @@ remote_receiver: status_led: pin: GPIO2 -pn532: +pn532_spi: + id: pn532_bs cs_pin: GPIO23 update_interval: 1s on_tag: @@ -1546,27 +2280,69 @@ pn532: - mqtt.publish: topic: the/topic payload: !lambda 'return x;' + on_tag_removed: + - lambda: |- + ESP_LOGD("main", "Removed tag %s", x.c_str()); + - mqtt.publish: + topic: the/topic + payload: !lambda 'return x;' + +pn532_i2c: + i2c_id: i2c_bus rdm6300: + uart_id: uart0 + +rc522_spi: + cs_pin: GPIO23 + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +rc522_i2c: + - update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + i2c_id: i2c_bus + + - update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + i2c_id: i2c_bus gps: + uart_id: uart0 time: -- platform: sntp - id: sntp_time - servers: - - 0.pool.ntp.org - - 1.pool.ntp.org - - 192.168.178.1 - on_time: - cron: '/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI' - then: - - lambda: 'ESP_LOGD("main", "time");' -- platform: gps + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + on_time: + cron: '/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI' + then: + - lambda: 'ESP_LOGD("main", "time");' + - platform: gps + on_time_sync: + then: + ds1307.write_time: + id: ds1307_time + - platform: ds1307 + id: ds1307_time + update_interval: never + on_time: + seconds: 0 + then: ds1307.read_time + i2c_id: i2c_bus cover: - platform: template - name: "Template Cover" + name: 'Template Cover' id: template_cover lambda: |- if (id(binary_sensor1).state) { @@ -1580,79 +2356,184 @@ 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 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_bus + - address: 0x71 + id: multiplex1 + i2c_id: multiplex0_chan0 + pcf8574: - id: 'pcf8574_hub' address: 0x21 pcf8575: False + i2c_id: i2c_bus mcp23017: - id: 'mcp23017_hub' + open_drain_interrupt: 'true' + i2c_id: i2c_bus mcp23008: - id: 'mcp23008_hub' address: 0x22 + open_drain_interrupt: 'true' + i2c_id: i2c_bus mcp23016: - id: 'mcp23016_hub' address: 0x23 + i2c_id: i2c_bus stepper: -- platform: a4988 - id: my_stepper - step_pin: GPIO23 - dir_pin: GPIO25 - sleep_pin: GPIO25 - max_speed: 250 steps/s - acceleration: 100 steps/s^2 - deceleration: 200 steps/s^2 - + - platform: a4988 + id: my_stepper + step_pin: GPIO23 + dir_pin: GPIO25 + sleep_pin: GPIO25 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 globals: -- id: glob_int - type: int - restore_value: yes - initial_value: '0' -- id: glob_float - type: float - restore_value: yes - initial_value: '0.0f' -- id: glob_bool - type: bool - restore_value: no - initial_value: 'true' -- id: glob_string - type: std::string - restore_value: no - # initial_value: "" + - id: glob_int + type: int + restore_value: yes + initial_value: '0' + - id: glob_float + type: float + restore_value: yes + initial_value: '0.0f' + - id: glob_bool + type: bool + restore_value: no + initial_value: 'true' + - id: glob_string + type: std::string + restore_value: no + # initial_value: "" + - id: glob_bool_processed + type: bool + restore_value: no + initial_value: 'false' text_sensor: -- platform: mqtt_subscribe - name: "MQTT Subscribe Text" - topic: "the/topic" - qos: 2 - on_value: - - text_sensor.template.publish: - id: template_text - state: Hello World - - text_sensor.template.publish: - id: template_text - state: |- - return "Hello World2"; - - globals.set: - id: glob_int - value: '0' -- platform: template - name: Template Text Sensor - id: template_text -- platform: wifi_info - ip_address: - name: "IP Address" - ssid: - name: "SSID" - bssid: - name: "BSSID" - mac_address: - name: "Mac Address" + - platform: mqtt_subscribe + name: 'MQTT Subscribe Text' + topic: 'the/topic' + qos: 2 + on_value: + - text_sensor.template.publish: + id: ${textname}_text + state: Hello World + - text_sensor.template.publish: + id: ${textname}_text + state: |- + return "Hello World2"; + - globals.set: + id: glob_int + value: '0' + - canbus.send: + can_id: 23 + data: [0x10, 0x20, 0x30] + - platform: template + name: Template Text Sensor + id: ${textname}_text + - platform: wifi_info + scan_results: + name: 'Scan Results' + ip_address: + name: 'IP Address' + ssid: + name: 'SSID' + bssid: + name: 'BSSID' + mac_address: + name: 'Mac Address' + - platform: version + name: 'ESPHome Version No Timestamp' + hide_timestamp: True + - platform: teleinfo + tag_name: "OPTARIF" + name: "optarif" + teleinfo_id: myteleinfo + +sn74hc595: + - id: 'sn74hc595_hub' + data_pin: GPIO21 + clock_pin: GPIO23 + latch_pin: GPIO22 + oe_pin: GPIO32 + sr_count: 2 + +rtttl: + output: gpio_19 + +canbus: + - platform: mcp2515 + cs_pin: GPIO17 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", &b[0] ); + - can_id: 23 + then: + - if: + condition: + lambda: 'return x[0] == 0x11;' + then: + light.toggle: ${roomname}_lights + +teleinfo: + id: myteleinfo + uart_id: uart0 + update_interval: 60s + historical_mode: true + +number: + - platform: template + id: test_number + state_topic: livingroom/custom_state_topic + command_topic: livingroom/custom_command_topic + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + state_topic: livingroom/custom_state_topic + command_topic: livingroom/custom_command_topic + options: + - one + - two + optimistic: true diff --git a/tests/test2.yaml b/tests/test2.yaml index d02c093b86..67b819a4d3 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -20,6 +20,9 @@ ethernet: subnet: 255.255.255.0 domain: .local +mdns: + disabled: true + api: i2c: @@ -36,197 +39,347 @@ uart: tx_pin: GPIO22 rx_pin: GPIO23 baud_rate: 115200 + # Specifically added for testing debug with no after: definition. + debug: + dummy_receiver: false + direction: rx + sequence: + - lambda: UARTDebug::log_hex(direction, bytes, ':'); ota: safe_mode: True port: 3286 + num_attempts: 15 logger: level: DEBUG -web_server: - auth: - username: admin - password: admin +deep_sleep: + run_duration: + default: 20s + gpio_wakeup_reason: 10s + touch_wakeup_reason: 15s + sleep_duration: 50s + wakeup_pin: GPIO39 + wakeup_pin_mode: INVERT_WAKEUP as3935_i2c: irq_pin: GPIO12 +mcp3008: + - id: 'mcp3008_hub' + cs_pin: GPIO12 + +output: + - platform: ac_dimmer + id: dimmer1 + gate_pin: GPIO5 + zero_cross_pin: GPIO12 sensor: - - platform: ble_rssi - mac_address: AC:37:43:77:5F:4C - name: "BLE Google Home Mini RSSI value" - - platform: ble_rssi - service_uuid: '11aa' - name: "BLE Test Service 16" - - platform: ble_rssi - service_uuid: '11223344' - name: "BLE Test Service 32" - - platform: ble_rssi - service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' - name: "BLE Test Service 128" - - platform: xiaomi_hhccjcy01 - mac_address: 94:2B:FF:5C:91:61 - temperature: - name: "Xiaomi HHCCJCY01 Temperature" - moisture: - name: "Xiaomi HHCCJCY01 Moisture" - illuminance: - name: "Xiaomi HHCCJCY01 Illuminance" - conductivity: - name: "Xiaomi HHCCJCY01 Soil Conductivity" - battery_level: - name: "Xiaomi HHCCJCY01 Battery Level" - - platform: xiaomi_lywsdcgq - mac_address: 7A:80:8E:19:36:BA - temperature: - name: "Xiaomi LYWSDCGQ Temperature" - humidity: - name: "Xiaomi LYWSDCGQ Humidity" - battery_level: - name: "Xiaomi LYWSDCGQ Battery Level" - - platform: xiaomi_lywsd02 - mac_address: 3F:5B:7D:82:58:4E - temperature: - name: "Xiaomi LYWSD02 Temperature" - humidity: - name: "Xiaomi LYWSD02 Humidity" - - platform: xiaomi_cgg1 - mac_address: 7A:80:8E:19:36:BA - temperature: - name: "Xiaomi CGG1 Temperature" - humidity: - name: "Xiaomi CGG1 Humidity" - battery_level: - name: "Xiaomi CGG1 Battery Level" - - platform: pmsx003 - type: PMSX003 - pm_1_0: - name: "PM 1.0 Concentration" - pm_2_5: - name: "PM 2.5 Concentration" - pm_10_0: - name: "PM 10.0 Concentration" - - platform: pmsx003 - type: PMS5003T - pm_2_5: - name: "PM 2.5 Concentration" - temperature: - name: "PMS Temperature" - humidity: - name: "PMS Humidity" - - platform: pmsx003 - type: PMS5003ST - pm_2_5: - name: "PM 2.5 Concentration" - temperature: - name: "PMS Temperature" - humidity: - name: "PMS Humidity" - formaldehyde: - name: "PMS Formaldehyde Concentration" - - platform: cse7766 - voltage: - name: "CSE7766 Voltage" - current: - name: "CSE7766 Current" - power: - name: "CSE776 Power" - - platform: apds9960 - type: proximity - name: APDS9960 Proximity - - platform: apds9960 - type: clear - name: APDS9960 Clear - - platform: apds9960 - type: red - name: APDS9960 Red - - platform: apds9960 - type: green - name: APDS9960 Green - - platform: apds9960 - type: blue - name: APDS9960 Blue - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world - - platform: as3935 - lightning_energy: - name: "Lightning Energy" - distance: - name: "Distance Storm" + - platform: homeassistant + entity_id: climate.living_room + attribute: temperature + id: ha_hello_world_temperature + - platform: ble_rssi + mac_address: AC:37:43:77:5F:4C + name: 'BLE Google Home Mini RSSI value' + - platform: ble_rssi + service_uuid: '11aa' + name: 'BLE Test Service 16' + - platform: ble_rssi + service_uuid: '11223344' + name: 'BLE Test Service 32' + - platform: ble_rssi + service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + name: 'BLE Test Service 128' + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: 'b-parasite Air Humidity' + temperature: + name: 'b-parasite Air Temperature' + moisture: + name: 'b-parasite Soil Moisture' + battery_voltage: + name: 'b-parasite Battery Voltage' + illuminance: + name: 'b-parasite Illuminance' + - platform: senseair + id: senseair0 + co2: + name: 'SenseAir CO2 Value' + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s - platform: ruuvitag mac_address: FF:56:D3:2F:7D:E8 humidity: - name: "RuuviTag Humidity" + name: 'RuuviTag Humidity' temperature: - name: "RuuviTag Temperature" + name: 'RuuviTag Temperature' pressure: - name: "RuuviTag Pressure" + name: 'RuuviTag Pressure' acceleration_x: - name: "RuuviTag Acceleration X" + name: 'RuuviTag Acceleration X' acceleration_y: - name: "RuuviTag Acceleration Y" + name: 'RuuviTag Acceleration Y' acceleration_z: - name: "RuuviTag Acceleration Z" + name: 'RuuviTag Acceleration Z' battery_voltage: - name: "RuuviTag Battery Voltage" + name: 'RuuviTag Battery Voltage' tx_power: - name: "RuuviTag TX Power" + name: 'RuuviTag TX Power' movement_counter: - name: "RuuviTag Movement Counter" + name: 'RuuviTag Movement Counter' measurement_sequence_number: - name: "RuuviTag Measurement Sequence Number" + name: 'RuuviTag Measurement Sequence Number' + - platform: as3935 + lightning_energy: + name: 'Lightning Energy' + distance: + name: 'Distance Storm' + - platform: xiaomi_hhccjcy01 + mac_address: 94:2B:FF:5C:91:61 + temperature: + name: 'Xiaomi HHCCJCY01 Temperature' + moisture: + name: 'Xiaomi HHCCJCY01 Moisture' + illuminance: + name: 'Xiaomi HHCCJCY01 Illuminance' + conductivity: + name: 'Xiaomi HHCCJCY01 Soil Conductivity' + battery_level: + name: 'Xiaomi HHCCJCY01 Battery Level' + - platform: xiaomi_lywsdcgq + mac_address: 7A:80:8E:19:36:BA + temperature: + name: 'Xiaomi LYWSDCGQ Temperature' + humidity: + name: 'Xiaomi LYWSDCGQ Humidity' + battery_level: + name: 'Xiaomi LYWSDCGQ Battery Level' + - platform: xiaomi_lywsd02 + mac_address: 3F:5B:7D:82:58:4E + temperature: + name: 'Xiaomi LYWSD02 Temperature' + humidity: + name: 'Xiaomi LYWSD02 Humidity' + battery_level: + name: 'Xiaomi LYWSD02 Battery Level' + - platform: xiaomi_cgg1 + mac_address: 7A:80:8E:19:36:BA + temperature: + name: 'Xiaomi CGG1 Temperature' + humidity: + name: 'Xiaomi CGG1 Humidity' + battery_level: + name: 'Xiaomi CGG1 Battery Level' + - platform: xiaomi_gcls002 + mac_address: '94:2B:FF:5C:91:61' + temperature: + name: 'GCLS02 Temperature' + moisture: + name: 'GCLS02 Moisture' + conductivity: + name: 'GCLS02 Soil Conductivity' + illuminance: + name: 'GCLS02 Illuminance' + - platform: xiaomi_hhccpot002 + mac_address: '94:2B:FF:5C:91:61' + moisture: + name: 'HHCCPOT002 Moisture' + conductivity: + name: 'HHCCPOT002 Soil Conductivity' + - platform: xiaomi_lywsd03mmc + mac_address: 'A4:C1:38:4E:16:78' + bindkey: 'e9efaa6873f9f9c87a5e75a5f814801c' + temperature: + name: 'Xiaomi LYWSD03MMC Temperature' + humidity: + name: 'Xiaomi LYWSD03MMC Humidity' + battery_level: + name: 'Xiaomi LYWSD03MMC Battery Level' + - platform: xiaomi_cgd1 + mac_address: 'A4:C1:38:D1:61:7D' + bindkey: 'c99d2313182473b38001086febf781bd' + temperature: + name: 'Xiaomi CGD1 Temperature' + humidity: + name: 'Xiaomi CGD1 Humidity' + battery_level: + name: 'Xiaomi CGD1 Battery Level' + - platform: xiaomi_jqjcy01ym + mac_address: '7A:80:8E:19:36:BA' + temperature: + name: 'JQJCY01YM Temperature' + humidity: + name: 'JQJCY01YM Humidity' + formaldehyde: + name: 'JQJCY01YM Formaldehyde' + battery_level: + name: 'JQJCY01YM Battery Level' + - platform: atc_mithermometer + mac_address: 'A4:C1:38:4E:16:78' + temperature: + name: 'ATC Temperature' + humidity: + name: 'ATC Humidity' + battery_level: + name: 'ATC Battery-Level' + battery_voltage: + name: 'ATC Battery-Voltage' + - platform: pvvx_mithermometer + mac_address: 'A4:C1:38:4E:16:78' + temperature: + name: 'PVVX Temperature' + humidity: + name: 'PVVX Humidity' + battery_level: + name: 'PVVX Battery-Level' + battery_voltage: + name: 'PVVX Battery-Voltage' + - platform: inkbird_ibsth1_mini + mac_address: 38:81:D7:0A:9C:11 + temperature: + name: 'Inkbird IBS-TH1 Temperature' + humidity: + name: 'Inkbird IBS-TH1 Humidity' + battery_level: + name: 'Inkbird IBS-TH1 Battery Level' + - platform: 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 + store_baseline: 'true' + - platform: mcp3008 + update_interval: 5s + mcp3008_id: 'mcp3008_hub' + id: freezer_temp_source + reference_voltage: 3.19 + number: 0 + - platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + temperature: + name: "Wave Plus Temperature" + radon: + name: "Wave Plus Radon" + radon_long_term: + name: "Wave Plus Radon Long Term" + pressure: + name: "Wave Plus Pressure" + humidity: + name: "Wave Plus Humidity" + co2: + name: "Wave Plus CO2" + tvoc: + name: "Wave Plus VOC" + - platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + temperature: + name: "Wave Mini Temperature" + humidity: + name: "Wave Mini Humidity" + pressure: + name: "Wave Mini Pressure" + tvoc: + name: "Wave Mini VOC" time: -- platform: homeassistant - on_time: - - at: '16:00:00' - then: - - logger.log: It's 16:00 - -apds9960: - address: 0x20 - update_interval: 60s + - platform: homeassistant + on_time: + - at: '16:00:00' + then: + - logger.log: It's 16:00 esp32_touch: setup_mode: True binary_sensor: - - platform: ble_presence - mac_address: AC:37:43:77:5F:4C - name: "ESP32 BLE Tracker Google Home Mini" - - platform: ble_presence - service_uuid: '11aa' - name: "BLE Test Service 16 Presence" - - platform: ble_presence - service_uuid: '11223344' - name: "BLE Test Service 32 Presence" - - platform: ble_presence - service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' - name: "BLE Test Service 128 Presence" - - platform: esp32_touch - name: "ESP32 Touch Pad GPIO27" - pin: GPIO27 - threshold: 1000 - - platform: apds9960 - direction: up - name: APDS9960 Up - - platform: apds9960 - direction: down - name: APDS9960 Down - - platform: apds9960 - direction: left - name: APDS9960 Left - - platform: apds9960 - direction: right - name: APDS9960 Right - platform: homeassistant entity_id: binary_sensor.hello_world id: ha_hello_world_binary + - platform: homeassistant + entity_id: binary_sensor.hello + attribute: world + id: ha_hello_world_binary_attribute + - platform: ble_presence + mac_address: AC:37:43:77:5F:4C + name: 'ESP32 BLE Tracker Google Home Mini' + - platform: ble_presence + service_uuid: '11aa' + name: 'BLE Test Service 16 Presence' + - platform: ble_presence + service_uuid: '11223344' + name: 'BLE Test Service 32 Presence' + - platform: ble_presence + service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + name: 'BLE Test Service 128 Presence' + - platform: ble_presence + ibeacon_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + ibeacon_major: 100 + ibeacon_minor: 1 + name: 'BLE Test iBeacon Presence' + - platform: esp32_touch + name: 'ESP32 Touch Pad GPIO27' + pin: GPIO27 + threshold: 1000 - platform: as3935 - name: "Storm Alert" + name: 'Storm Alert' + - platform: xiaomi_mue4094rt + name: 'MUE4094RT Motion' + mac_address: '7A:80:8E:19:36:BA' + timeout: '5s' + - platform: xiaomi_mjyd02yla + name: 'MJYD02YL-A Motion' + mac_address: '50:EC:50:CD:32:02' + bindkey: '48403ebe2d385db8d0c187f81e62cb64' + idle_time: + name: 'MJYD02YL-A Idle Time' + light: + name: 'MJYD02YL-A Light Status' + battery_level: + name: 'MJYD02YL-A Battery Level' + - platform: xiaomi_wx08zm + name: 'WX08ZM Activation State' + mac_address: '74:a3:4a:b5:07:34' + tablet: + name: 'WX08ZM Tablet Resource' + battery_level: + name: 'WX08ZM Battery Level' + - platform: xiaomi_cgpr1 + name: 'CGPR1 Motion' + mac_address: '12:34:56:12:34:56' + bindkey: '48403ebe2d385db8d0c187f81e62cb64' + battery_level: + name: 'CGPR1 battery Level' + idle_time: + name: 'CGPR1 Idle Time' + illuminance: + name: 'CGPR1 Illuminance' esp32_ble_tracker: on_ble_advertise: @@ -248,6 +401,19 @@ 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: + +ruuvi_ble: + +xiaomi_ble: + #esp32_ble_beacon: # type: iBeacon @@ -258,7 +424,7 @@ status_led: text_sensor: - platform: version - name: "ESPHome Version" + name: 'ESPHome Version' icon: mdi:icon id: version_sensor on_value: @@ -266,8 +432,8 @@ text_sensor: condition: - api.connected: then: - - lambda: !lambda |- - ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); + - lambda: !lambda |- + ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); - script.execute: my_script - homeassistant.service: service: notify.html5 @@ -284,16 +450,54 @@ text_sensor: service: light.turn_on data: entity_id: light.my_light + - homeassistant.tag_scanned: + tag: 1234-abcd + - homeassistant.tag_scanned: 1234-abcd + - deep_sleep.enter: + sleep_duration: 30min + - deep_sleep.enter: + sleep_duration: !lambda "return 30 * 60 * 1000;" - platform: template - name: "Template Text Sensor" + name: 'Template Text Sensor' lambda: |- return {"Hello World"}; + filters: + - to_upper: + - to_lower: + - append: "xyz" + - prepend: "abcd" + - substitute: + - Hello -> Goodbye + - map: + - red -> green + - lambda: return {"1234"}; - platform: homeassistant entity_id: sensor.hello_world2 id: ha_hello_world2 + - platform: homeassistant + entity_id: sensor.hello_world3 + id: ha_hello_world3 + attribute: some_attribute + - platform: ble_scanner + name: Scanner script: - id: my_script + mode: single + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_queued + mode: queued + max_runs: 2 + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_parallel + mode: parallel + max_runs: 2 + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_restart + mode: restart then: - lambda: 'ESP_LOGD("main", "Hello World!");' @@ -315,7 +519,13 @@ stepper: interval: interval: 5s then: - - logger.log: "Interval Run" + - logger.log: 'Interval Run' display: +cap1188: + id: cap1188_component + address: 0x29 + touch_threshold: 0x20 + allow_multiple_touches: true + reset_pin: 14 diff --git a/tests/test3.yaml b/tests/test3.yaml index 2a37095aca..61d68d824b 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,24 +1,32 @@ esphome: - name: $devicename - comment: $devicecomment + name: $device_name + comment: $device_comment platform: ESP8266 board: d1_mini build_path: build/test3 on_boot: - - wait_until: - - api.connected - - wifi.connected + - if: + condition: + - api.connected + - wifi.connected + - time.has_time + then: + - logger.log: "Have time" includes: - custom.h substitutions: - devicename: test3 - devicecomment: test3 device + device_name: test3 + device_comment: test3 device + min_sub: '0.03' + max_sub: '12.0%' api: port: 8000 password: 'pwd' reboot_timeout: 0min + encryption: + key: 'bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=' services: - service: hello_world variables: @@ -138,6 +146,14 @@ api: then: - dfplayer.random + - service: dfplayer_volume_up + then: + - dfplayer.volume_up + + - service: dfplayer_volume_down + then: + - dfplayer.volume_down + - service: battery_level_percent variables: level_percent: int @@ -167,6 +183,33 @@ api: then: - tm1651.turn_off: id: tm1651_battery + - service: pid_set_control_parameters + then: + - climate.pid.set_control_parameters: + id: pid_climate + kp: 1.0 + kd: 1.0 + ki: 1.0 + - service: fingerprint_grow_enroll + variables: + finger_id: int + num_scans: int + then: + - fingerprint_grow.enroll: + finger_id: !lambda 'return finger_id;' + num_scans: !lambda 'return num_scans;' + - service: fingerprint_grow_cancel_enroll + then: + - fingerprint_grow.cancel_enroll: + - service: fingerprint_grow_delete + variables: + finger_id: int + then: + - fingerprint_grow.delete: + finger_id: !lambda 'return finger_id;' + - service: fingerprint_grow_delete_all + then: + - fingerprint_grow.delete_all: wifi: ssid: 'MySSID' @@ -183,29 +226,104 @@ spi: miso_pin: GPIO14 uart: - tx_pin: GPIO1 - rx_pin: GPIO3 - baud_rate: 115200 + - id: uart1 + tx_pin: + number: GPIO1 + inverted: yes + rx_pin: GPIO3 + baud_rate: 115200 + - id: uart2 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart3 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 4800 + - id: uart4 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart5 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart6 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart7 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 38400 + # Specifically added for testing debug with no options at all. + debug: + +modbus: + uart_id: uart1 ota: safe_mode: True port: 3286 + reboot_timeout: 15min logger: hardware_uart: UART1 level: DEBUG - esp8266_store_log_strings_in_flash: false + esp8266_store_log_strings_in_flash: true -web_server: +improv_serial: deep_sleep: run_duration: 20s sleep_duration: 50s +wled: + +adalight: + + sensor: + - platform: daly_bms + voltage: + name: "Battery Voltage" + current: + name: "Battery Current" + battery_level: + name: "Battery Level" + max_cell_voltage: + name: "Max Cell Voltage" + max_cell_voltage_number: + name: "Max Cell Voltage Number" + min_cell_voltage: + name: "Min Cell Voltage" + min_cell_voltage_number: + name: "Min Cell Voltage Number" + max_temperature: + name: "Max Temperature" + max_temperature_probe_number: + name: "Max Temperature Probe Number" + min_temperature: + name: "Min Temperature" + min_temperature_probe_number: + name: "Min Temperature Probe Number" + remaining_capacity: + name: "Remaining Capacity" + cells_number: + name: "Cells Number" + temperature_1: + name: "Temperature 1" + temperature_2: + name: "Temperature 2" - platform: apds9960 type: proximity name: APDS9960 Proximity + - platform: vl53l0x + name: 'VL53L0x Distance' + address: 0x29 + update_interval: 60s + enable_pin: GPIO13 + timeout: 200us - platform: apds9960 type: clear name: APDS9960 Clear @@ -221,16 +339,16 @@ sensor: - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world - - platform: aht10 + - platform: aht10 temperature: - name: "Temperature" + name: 'Temperature' humidity: - name: "Humidity" + name: 'Humidity' - platform: am2320 temperature: - name: "Temperature" + name: 'Temperature' humidity: - name: "Humidity" + name: 'Humidity' - platform: adc pin: VCC id: my_sensor @@ -240,6 +358,11 @@ sensor: - filter_out: NAN - sliding_window_moving_average: - exponential_moving_average: + - quantile: + window_size: 5 + send_every: 5 + send_first_at: 3 + quantile: .8 - lambda: 'return 0;' - delta: 100 - throttle: 100ms @@ -295,7 +418,7 @@ sensor: name: Illuminance color_temperature: name: Color Temperature - integration_time: 700ms + integration_time: 614ms gain: 60x - platform: custom lambda: |- @@ -316,53 +439,242 @@ sensor: - binary_sensor: bin3 value: 100.0 - platform: ade7953 + irq_pin: GPIO16 voltage: name: ADE7953 Voltage + id: ade7953_voltage current_a: name: ADE7953 Current A + id: ade7953_current_a current_b: name: ADE7953 Current B + id: ade7953_current_b active_power_a: name: ADE7953 Active Power A + id: ade7953_active_power_a active_power_b: name: ADE7953 Active Power B + id: ade7953_active_power_b - platform: pzem004t + uart_id: uart3 voltage: - name: "PZEM00T Voltage" + name: 'PZEM00T Voltage' current: - name: "PZEM004T Current" + name: 'PZEM004T Current' power: - name: "PZEM004T Power" + name: 'PZEM004T Power' - platform: pzemac voltage: - name: "PZEMAC Voltage" + name: 'PZEMAC Voltage' current: - name: "PZEMAC Current" + name: 'PZEMAC Current' power: - name: "PZEMAC Power" + name: 'PZEMAC Power' energy: - name: "PZEMAC Energy" + name: 'PZEMAC Energy' frequency: - name: "PZEMAC Frequency" + name: 'PZEMAC Frequency' power_factor: - name: "PZEMAC Power Factor" + name: 'PZEMAC Power Factor' - platform: pzemdc voltage: - name: "PZEMDC Voltage" + name: 'PZEMDC Voltage' current: - name: "PZEMDC Current" + name: 'PZEMDC Current' power: - name: "PZEMDC Power" + name: 'PZEMDC Power' + - platform: tmp102 + name: 'TMP102 Temperature' - platform: hm3301 pm_1_0: - name: "PM1.0" + name: 'PM1.0' pm_2_5: - name: "PM2.5" + name: 'PM2.5' pm_10_0: - name: "PM10.0" + name: 'PM10.0' + aqi: + name: 'AQI' + calculation_type: 'AQI' + - platform: pmsx003 + uart_id: uart2 + type: PMSX003 + pm_1_0: + name: 'PM 1.0 Concentration' + pm_2_5: + name: 'PM 2.5 Concentration' + pm_10_0: + name: 'PM 10.0 Concentration' + pm_1_0_std: + name: 'PM 1.0 Standard Atmospher Concentration' + pm_2_5_std: + name: 'PM 2.5 Standard Atmospher Concentration' + pm_10_0_std: + name: 'PM 10.0 Standard Atmospher Concentration' + pm_0_3um: + name: 'Particulate Count >0.3um' + pm_0_5um: + name: 'Particulate Count >0.5um' + pm_1_0um: + name: 'Particulate Count >1.0um' + pm_2_5um: + name: 'Particulate Count >2.5um' + pm_5_0um: + name: 'Particulate Count >5.0um' + pm_10_0um: + name: 'Particulate Count >10.0um' + - 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: cse7761 + uart_id: uart7 + voltage: + name: 'CSE7761 Voltage' + current_1: + name: 'CSE7761 Current 1' + current_2: + name: 'CSE7761 Current 2' + active_power_1: + name: 'CSE7761 Active Power 1' + active_power_2: + name: 'CSE7761 Active Power 2' + - platform: cse7766 + uart_id: uart3 + voltage: + name: 'CSE7766 Voltage' + current: + name: 'CSE7766 Current' + power: + name: 'CSE776 Power' + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: 'pH' + - platform: tof10120 + name: "Distance sensor" + update_interval: 5s + - platform: fingerprint_grow + fingerprint_count: + name: "Fingerprint Count" + status: + name: "Fingerprint Status" + capacity: + name: "Fingerprint Capacity" + security_level: + name: "Fingerprint Security Level" + last_finger_id: + name: "Fingerprint Last Finger ID" + last_confidence: + name: "Fingerprint Last Confidence" + - platform: sdm_meter + phase_a: + current: + name: 'Phase A Current' + voltage: + name: 'Phase A Voltage' + active_power: + name: 'Phase A Power' + power_factor: + name: 'Phase A Power Factor' + apparent_power: + name: 'Phase A Apparent Power' + reactive_power: + name: 'Phase A Reactive Power' + phase_angle: + name: 'Phase A Phase Angle' + phase_b: + current: + name: 'Phase B Current' + voltage: + name: 'Phase B Voltage' + active_power: + name: 'Phase B Power' + power_factor: + name: 'Phase B Power Factor' + apparent_power: + name: 'Phase B Apparent Power' + reactive_power: + name: 'Phase B Reactive Power' + phase_angle: + name: 'Phase B Phase Angle' + phase_c: + current: + name: 'Phase C Current' + voltage: + name: 'Phase C Voltage' + active_power: + name: 'Phase C Power' + power_factor: + name: 'Phase C Power Factor' + apparent_power: + name: 'Phase C Apparent Power' + reactive_power: + name: 'Phase C Reactive Power' + phase_angle: + name: 'Phase C Phase Angle' + frequency: + name: 'Frequency' + import_active_energy: + name: 'Import Active Energy' + export_active_energy: + name: 'Export Active Energy' + import_reactive_energy: + name: 'Import Reactive Energy' + export_reactive_energy: + name: 'Export Reactive Energy' + - platform: dsmr + energy_delivered_tariff1: + name: dsmr_energy_delivered_tariff1 + - platform: nextion + id: testnumber + name: 'testnumber' + variable_name: testnumber + - platform: nextion + id: testwave + name: 'testwave' + component_id: 2 + wave_channel_id: 1 time: -- platform: homeassistant + - platform: homeassistant apds9960: address: 0x20 @@ -373,6 +685,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 @@ -400,18 +717,18 @@ binary_sensor: - platform: mpr121 id: touchkey0 channel: 0 - name: "touchkey0" + name: 'touchkey0' - platform: mpr121 channel: 1 - name: "touchkey1" + name: 'touchkey1' id: bin1 - platform: mpr121 channel: 2 - name: "touchkey2" + name: 'touchkey2' id: bin2 - platform: mpr121 channel: 3 - name: "touchkey3" + name: 'touchkey3' id: bin3 on_press: then: @@ -422,6 +739,8 @@ binary_sensor: - platform: ttp229_bsf channel: 1 name: TTP229 BSF Test + - platform: fingerprint_grow + name: "Fingerprint Enrolling" - platform: custom lambda: |- auto s = new CustomBinarySensor(); @@ -430,6 +749,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 @@ -444,8 +776,11 @@ status_led: pin: GPIO2 text_sensor: + - platform: daly_bms + status: + name: "BMS Status" - platform: version - name: "ESPHome Version" + name: 'ESPHome Version' icon: mdi:icon id: version_sensor on_value: @@ -464,7 +799,7 @@ text_sensor: my_variable: |- return id(version_sensor).state; - platform: template - name: "Template Text Sensor" + name: 'Template Text Sensor' lambda: |- return {"Hello World"}; - platform: homeassistant @@ -478,28 +813,42 @@ 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!");' +sm2135: + data_pin: GPIO12 + clock_pin: GPIO14 + switch: - platform: template - name: "mpr121_toggle" + name: 'mpr121_toggle' id: mpr121_toggle optimistic: True - platform: gpio id: gpio_switch1 pin: - mcp23017: mcp23017_hub + mcp23xxx: mcp23017_hub number: 0 mode: OUTPUT interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio id: gpio_switch2 pin: - mcp23008: mcp23008_hub + mcp23xxx: mcp23008_hub number: 0 mode: OUTPUT interlock: *interlock @@ -514,12 +863,16 @@ switch: switches: - id: custom_switch name: Custom Switch + - platform: nextion + id: r0 + name: 'R0 Switch' + component_name: page0.r0 custom_component: - lambda: |- - auto s = new CustomComponent(); - s->set_update_interval(15000); - return {s}; + lambda: |- + auto s = new CustomComponent(); + s->set_update_interval(15000); + return {s}; stepper: - platform: uln2003 @@ -544,7 +897,7 @@ stepper: interval: interval: 5s then: - - logger.log: "Interval Run" + - logger.log: 'Interval Run' - stepper.set_target: id: my_stepper2 target: 500 @@ -573,6 +926,98 @@ climate: away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C + - platform: thermostat + name: Thermostat Climate + sensor: ha_hello_world + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: gpio_switch1 + cool_action: + - switch.turn_on: gpio_switch2 + supplemental_cooling_action: + - switch.turn_on: gpio_switch3 + heat_action: + - switch.turn_on: gpio_switch1 + supplemental_heating_action: + - switch.turn_on: gpio_switch3 + dry_action: + - switch.turn_on: gpio_switch2 + fan_only_action: + - switch.turn_on: gpio_switch1 + auto_mode: + - switch.turn_on: gpio_switch2 + off_mode: + - switch.turn_on: gpio_switch1 + heat_mode: + - switch.turn_on: gpio_switch2 + cool_mode: + - switch.turn_on: gpio_switch1 + dry_mode: + - switch.turn_on: gpio_switch2 + fan_only_mode: + - switch.turn_on: gpio_switch1 + fan_mode_auto_action: + - switch.turn_on: gpio_switch2 + fan_mode_on_action: + - switch.turn_on: gpio_switch1 + fan_mode_off_action: + - switch.turn_on: gpio_switch2 + fan_mode_low_action: + - switch.turn_on: gpio_switch1 + fan_mode_medium_action: + - switch.turn_on: gpio_switch2 + fan_mode_high_action: + - switch.turn_on: gpio_switch1 + fan_mode_middle_action: + - switch.turn_on: gpio_switch2 + fan_mode_focus_action: + - switch.turn_on: gpio_switch1 + fan_mode_diffuse_action: + - switch.turn_on: gpio_switch2 + swing_off_action: + - switch.turn_on: gpio_switch1 + swing_horizontal_action: + - switch.turn_on: gpio_switch2 + swing_vertical_action: + - switch.turn_on: gpio_switch1 + swing_both_action: + - switch.turn_on: gpio_switch2 + 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 + - platform: pid + id: pid_climate + name: 'PID Climate Controller' + sensor: ha_hello_world + default_target_temperature: 21°C + heat_output: my_slow_pwm + control_parameters: + kp: 0.0 + ki: 0.0 + kd: 0.0 cover: - platform: endstop @@ -607,6 +1052,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: @@ -615,24 +1061,49 @@ 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;" + tilt_lambda: 'return 0.5;' tilt_action: - output.set_level: id: out - level: !lambda "return tilt;" + level: !lambda 'return tilt;' position_action: - output.set_level: id: out - level: !lambda "return pos;" - + level: !lambda 'return pos;' output: - platform: esp8266_pwm id: out pin: D3 frequency: 50Hz + - platform: esp8266_pwm + id: out2 + pin: D4 - platform: custom type: binary lambda: |- @@ -649,10 +1120,25 @@ 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 + period: 15s + - platform: sm2135 + id: sm2135_0 + channel: 0 + - platform: sm2135 + id: sm2135_1 + channel: 1 + - platform: sm2135 + id: sm2135_2 + channel: 2 + - platform: sm2135 + id: sm2135_3 + channel: 3 + - platform: sm2135 + id: sm2135_4 + channel: 4 mcp23017: id: mcp23017_hub @@ -660,6 +1146,8 @@ mcp23017: mcp23008: id: mcp23008_hub +e131: + light: - platform: neopixelbus name: Neopixelbus Light @@ -668,19 +1156,32 @@ light: variant: SK6812 method: ESP8266_UART0 num_leds: 100 + effects: + - wled: + - adalight: + uart_id: uart3 + - e131: + universe: 1 + - platform: hbridge + name: Icicle Lights + pin_a: out + pin_b: out2 servo: id: my_servo output: out restore: true + min_level: $min_sub + max_level: $max_sub ttp229_lsf: ttp229_bsf: - sdo_pin: D0 + sdo_pin: D2 scl_pin: D1 sim800l: + uart_id: uart4 on_sms_received: - lambda: |- std::string str; @@ -689,21 +1190,25 @@ sim800l: - sim800l.send_sms: message: 'hello you' recipient: '+1234' + - sim800l.dial: + recipient: '+1234' dfplayer: + uart_id: uart5 on_finished_playback: then: if: condition: - not: - dfplayer.is_playing + not: dfplayer.is_playing then: logger.log: 'Playback finished event' tm1651: id: tm1651_battery clk_pin: D6 dio_pin: D5 + rf_bridge: + uart_id: uart5 on_code_received: - lambda: |- uint32_t test; @@ -717,3 +1222,106 @@ rf_bridge: high: 0x1234 code: 0x123456 - rf_bridge.learn + + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing + - rf_bridge.stop_advanced_sniffing + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: 'ABC123' + - rf_bridge.send_raw: + raw: 'AAA5070008001000ABC12355' + - http_request.get: + url: https://esphome.io + headers: + Content-Type: application/json + verify_ssl: false + - http_request.post: + url: https://esphome.io + verify_ssl: false + json: + key: !lambda |- + return id(version_sensor).state; + greeting: 'Hello World' + - http_request.send: + method: PUT + url: https://esphome.io + headers: + Content-Type: application/json + body: 'Some data' + verify_ssl: false + +display: + - platform: max7219digit + cs_pin: GPIO15 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: 'STOP' + 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 + timeout: 10s + +fingerprint_grow: + sensing_pin: 4 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_matched: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_finger_scan_matched + data: + finger_id: !lambda 'return finger_id;' + confidence: !lambda 'return confidence;' + on_finger_scan_unmatched: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_finger_scan_unmatched + on_enrollment_scan: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_enrollment_scan + data: + finger_id: !lambda 'return finger_id;' + scan_num: !lambda 'return scan_num;' + on_enrollment_done: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_node_enrollment_done + data: + finger_id: !lambda 'return finger_id;' + on_enrollment_failed: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_enrollment_failed + data: + finger_id: !lambda 'return finger_id;' + uart_id: uart6 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + uart_id: uart6 + max_telegram_length: 1000 + request_pin: D5 + request_interval: 20s + receive_timeout: 100ms + +daly_bms: + update_interval: 20s + uart_id: uart1 diff --git a/tests/test4.yaml b/tests/test4.yaml new file mode 100644 index 0000000000..7c1ddee6d8 --- /dev/null +++ b/tests/test4.yaml @@ -0,0 +1,537 @@ +esphome: + name: $devicename + platform: ESP32 + board: nodemcu-32s + build_path: build/test4 + +substitutions: + devicename: test-4 + +ethernet: + type: LAN8720 + mdc_pin: GPIO23 + mdio_pin: GPIO25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: GPIO25 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + +api: + +i2c: + sda: 21 + scl: 22 + scan: False + +spi: + clk_pin: GPIO21 + mosi_pin: GPIO22 + miso_pin: GPIO23 + +uart: + tx_pin: GPIO22 + rx_pin: GPIO23 + baud_rate: 115200 + +ota: + safe_mode: True + port: 3286 + +logger: + level: DEBUG + +web_server: + ota: false + auth: + username: admin + password: admin + include_internal: true + +time: + - platform: sntp + id: sntp_time + +tuya: + time_id: sntp_time + +pipsolar: + id: inverter0 + +sx1509: + - id: sx1509_hub + address: 0x3E + +sensor: + - platform: homeassistant + entity_id: sensor.hello_world + id: ha_hello_world + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + - platform: "hrxl_maxsonar_wr" + name: "Rainwater Tank Level" + filters: + - sliding_window_moving_average: + window_size: 12 + send_every: 12 + - or: + - throttle: "20min" + - delta: 0.02 + +# +# platform sensor.apds9960 requires component apds9960 +# +# - platform: apds9960 +# type: proximity +# name: APDS9960 Proximity +# - platform: apds9960 +# type: clear +# name: APDS9960 Clear +# - platform: apds9960 +# type: red +# name: APDS9960 Red +# - platform: apds9960 +# type: green +# name: APDS9960 Green +# - platform: apds9960 +# type: blue +# name: APDS9960 Blue + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + - platform: template + id: ar1 + lambda: 'return {};' + filters: + - autorepeat: + - delay: 2s + time_off: 100ms + time_on: 900ms + - delay: 4s + time_off: 100ms + time_on: 400ms + on_state: + then: + - lambda: 'ESP_LOGI("ar1:", "%d", x);' + - platform: xpt2046 + xpt2046_id: touchscreen + id: touch_key0 + x_min: 80 + x_max: 160 + y_min: 106 + y_max: 212 + on_state: + - lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));' + - platform: gpio + name: GPIO SX1509 test + pin: + sx1509: sx1509_hub + number: 3 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + +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 + id: led_matrix_32x8 + name: "led_matrix_32x8" + chipset: WS2812B + pin: GPIO15 + num_leds: 256 + rgb_order: GRB + default_transition_length: 0s + color_correct: [50%, 50%, 50%] + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms + + - platform: waveshare_epaper + cs_pin: GPIO23 + dc_pin: GPIO23 + busy_pin: GPIO23 + reset_pin: GPIO23 + model: 2.13in-ttgo-b1 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: GPIO23 + dc_pin: GPIO23 + busy_pin: GPIO23 + reset_pin: GPIO23 + model: 2.90in + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: GPIO23 + dc_pin: GPIO23 + busy_pin: GPIO23 + reset_pin: GPIO23 + model: 2.90inv2 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: inkplate6 + id: inkplate_display + greyscale: false + partial_updating: false + update_interval: 60s + + ckv_pin: GPIO1 + sph_pin: GPIO1 + gmod_pin: GPIO1 + gpio0_enable_pin: GPIO1 + oe_pin: GPIO1 + spv_pin: GPIO1 + powerup_pin: GPIO1 + wakeup_pin: GPIO1 + vcom_pin: GPIO1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +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] + vsync_pin: GPIO22 + href_pin: GPIO26 + pixel_clock_pin: GPIO21 + external_clock: + pin: GPIO27 + frequency: 20MHz + i2c_pins: + sda: GPIO25 + scl: GPIO23 + reset_pin: GPIO15 + power_down_pin: GPIO1 + resolution: 640x480 + jpeg_quality: 10 + +esp32_camera_web_server: + - port: 8080 + mode: stream + - port: 8081 + mode: snapshot + +external_components: + - source: github://esphome/esphome@dev + refresh: 1d + components: ["bh1750"] + - source: ../esphome/components + components: ["sntp"] +xpt2046: + id: touchscreen + cs_pin: 17 + irq_pin: 16 + update_interval: 50ms + report_interval: 1s + threshold: 400 + dimension_x: 240 + dimension_y: 320 + calibration_x_min: 3860 + calibration_x_max: 280 + calibration_y_min: 340 + calibration_y_max: 3860 + swap_x_y: False + on_state: + - lambda: |- + ESP_LOGI("main", "args x=%d, y=%d, touched=%s", x, y, (touched ? "touch" : "release")); + ESP_LOGI("main", "member x=%d, y=%d, touched=%d, x_raw=%d, y_raw=%d, z_raw=%d", + id(touchscreen).x, + id(touchscreen).y, + (int) id(touchscreen).touched, + id(touchscreen).x_raw, + id(touchscreen).y_raw, + id(touchscreen).z_raw + ); + +button: + - platform: restart + name: Restart Button + - platform: safe_mode + name: Safe Mode Button + - platform: shutdown + name: Shutdown Button diff --git a/tests/test5.yaml b/tests/test5.yaml new file mode 100644 index 0000000000..37e65e7da2 --- /dev/null +++ b/tests/test5.yaml @@ -0,0 +1,189 @@ +esphome: + name: test5 + build_path: build/test5 + project: + name: esphome.test5_project + version: "1.0.0" + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + advanced: + ignore_efuse_mac_crc: true + +wifi: + networks: + - ssid: 'MySSID' + password: 'password1' + manual_ip: + static_ip: 192.168.1.23 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + +api: + +ota: + +logger: + +uart: + - id: 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: + manufacturer: "ESPHome" + model: "Test5" + +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 + unit_of_measurement: '%' + mode: slider + +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 + +script: + - id: automation_test + then: + - repeat: + count: 5 + then: + - logger.log: "looping!" diff --git a/tests/test_packages/test_packages_package1.yaml b/tests/test_packages/test_packages_package1.yaml new file mode 100644 index 0000000000..0495984d42 --- /dev/null +++ b/tests/test_packages/test_packages_package1.yaml @@ -0,0 +1,2 @@ +sensor: + - <<: !include ./test_uptime_sensor.yaml diff --git a/tests/test_packages/test_packages_package_wifi.yaml b/tests/test_packages/test_packages_package_wifi.yaml new file mode 100644 index 0000000000..7d5d41ddab --- /dev/null +++ b/tests/test_packages/test_packages_package_wifi.yaml @@ -0,0 +1,4 @@ +wifi: + networks: + - ssid: 'WiFiFromPackage' + password: 'password1' diff --git a/tests/test_packages/test_uptime_sensor.yaml b/tests/test_packages/test_uptime_sensor.yaml new file mode 100644 index 0000000000..1bf52a6d0b --- /dev/null +++ b/tests/test_packages/test_uptime_sensor.yaml @@ -0,0 +1,5 @@ +# Uptime sensor. +platform: uptime +id: ${devicename}_uptime_pcg +name: Uptime From Package +update_interval: 5min diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index adef39a0b3..41d0f3dadb 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -27,4 +27,3 @@ def fixture_path() -> Path: Location of all fixture files. """ return here / "fixtures" - diff --git a/tests/unit_tests/strategies.py b/tests/unit_tests/strategies.py index f4763f047f..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. @@ -12,4 +12,6 @@ def mac_addr_strings(): This consists of six strings representing integers [0..255], without zero-padding, joined by dots. """ - return st.builds("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}".format, *(6 * [st.integers(0, 255)])) + return st.builds( + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}".format, *(6 * [st.integers(0, 255)]) + ) diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 931e191de6..32d82b3062 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -4,23 +4,75 @@ from esphome import codegen as cg # Test interface remains the same. -@pytest.mark.parametrize("attr", ( - # from cpp_generator - "Expression", "RawExpression", "RawStatement", "TemplateArguments", - "StructInitializer", "ArrayInitializer", "safe_exp", "Statement", "LineComment", - "progmem_array", "statement", "variable", "Pvariable", "new_Pvariable", - "add", "add_global", "add_library", "add_build_flag", "add_define", - "get_variable", "get_variable_with_full_id", "process_lambda", "is_template", "templatable", "MockObj", - "MockObjClass", - # from cpp_helpers - "gpio_pin_expression", "register_component", "build_registry_entry", - "build_registry_list", "extract_registry_entry_config", "register_parented", - "global_ns", "void", "nullptr", "float_", "double", "bool_", "int_", "std_ns", "std_string", - "std_vector", "uint8", "uint16", "uint32", "int32", "const_char_ptr", "NAN", - "esphome_ns", "App", "Nameable", "Component", "ComponentPtr", - # from cpp_types - "PollingComponent", "Application", "optional", "arduino_json_ns", "JsonObject", - "JsonObjectRef", "JsonObjectConstRef", "Controller", "GPIOPin" -)) +@pytest.mark.parametrize( + "attr", + ( + # from cpp_generator + "Expression", + "RawExpression", + "RawStatement", + "TemplateArguments", + "StructInitializer", + "ArrayInitializer", + "safe_exp", + "Statement", + "LineComment", + "progmem_array", + "statement", + "variable", + "Pvariable", + "new_Pvariable", + "add", + "add_global", + "add_library", + "add_build_flag", + "add_define", + "get_variable", + "get_variable_with_full_id", + "process_lambda", + "is_template", + "templatable", + "MockObj", + "MockObjClass", + # from cpp_helpers + "gpio_pin_expression", + "register_component", + "build_registry_entry", + "build_registry_list", + "extract_registry_entry_config", + "register_parented", + "global_ns", + "void", + "nullptr", + "float_", + "double", + "bool_", + "int_", + "std_ns", + "std_string", + "std_vector", + "uint8", + "uint16", + "uint32", + "int32", + "const_char_ptr", + "NAN", + "esphome_ns", + "App", + "EntityBase", + "Component", + "ComponentPtr", + # from cpp_types + "PollingComponent", + "Application", + "optional", + "arduino_json_ns", + "JsonObject", + "JsonObjectRef", + "JsonObjectConstRef", + "Controller", + "GPIOPin", + ), +) def test_exists(attr): assert hasattr(cg, attr) diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index a19330062e..9e9af52d00 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -2,39 +2,39 @@ import pytest import string from hypothesis import given, example -from hypothesis.strategies import one_of, text, integers, booleans, builds +from hypothesis.strategies import one_of, text, integers, builds from esphome import config_validation from esphome.config_validation import Invalid from esphome.core import Lambda, HexInt -def test_check_not_tamplatable__invalid(): +def test_check_not_templatable__invalid(): with pytest.raises(Invalid, match="This option is not templatable!"): config_validation.check_not_templatable(Lambda("")) -@given(one_of( - booleans(), - integers(), - text(alphabet=string.ascii_letters + string.digits)), -) +@pytest.mark.parametrize("value", ("foo", 1, "D12", False)) def test_alphanumeric__valid(value): actual = config_validation.alphanumeric(value) assert actual == str(value) -@given(value=text(alphabet=string.ascii_lowercase + string.digits + "_")) +@pytest.mark.parametrize("value", ("£23", "Foo!")) +def test_alphanumeric__invalid(value): + with pytest.raises(Invalid): + config_validation.alphanumeric(value) + + +@given(value=text(alphabet=string.ascii_lowercase + string.digits + "-_")) def test_valid_name__valid(value): actual = config_validation.valid_name(value) assert actual == value -@pytest.mark.parametrize("value", ( - "foo bar", "FooBar", "foo::bar" -)) +@pytest.mark.parametrize("value", ("foo bar", "FooBar", "foo::bar")) def test_valid_name__invalid(value): with pytest.raises(Invalid): config_validation.valid_name(value) @@ -47,9 +47,7 @@ def test_string__valid(value): assert actual == str(value) -@pytest.mark.parametrize("value", ( - {}, [], True, False, None -)) +@pytest.mark.parametrize("value", ({}, [], True, False, None)) def test_string__invalid(value): with pytest.raises(Invalid): config_validation.string(value) @@ -68,7 +66,7 @@ def test_string_string__invalid(value): config_validation.string_strict(value) -@given(builds(lambda v: "mdi:" + v, text())) +@given(builds(lambda v: "mdi:" + v, text(alphabet=string.ascii_letters + string.digits + "-_", min_size=1, max_size=20))) @example("") def test_icon__valid(value): actual = config_validation.icon(value) @@ -77,27 +75,21 @@ def test_icon__valid(value): def test_icon__invalid(): - with pytest.raises(Invalid, match="Icons should start with prefix"): + with pytest.raises(Invalid, match="Icons must match the format "): config_validation.icon("foo") -@pytest.mark.parametrize("value", ( - "True", "YES", "on", "enAblE", True -)) +@pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True)) def test_boolean__valid_true(value): assert config_validation.boolean(value) is True -@pytest.mark.parametrize("value", ( - "False", "NO", "off", "disAblE", False -)) +@pytest.mark.parametrize("value", ("False", "NO", "off", "disAblE", False)) def test_boolean__valid_false(value): assert config_validation.boolean(value) is False -@pytest.mark.parametrize("value", ( - None, 1, 0, "foo" -)) +@pytest.mark.parametrize("value", (None, 1, 0, "foo")) def test_boolean__invalid(value): with pytest.raises(Invalid, match="Expected boolean value"): config_validation.boolean(value) @@ -110,4 +102,3 @@ def hex_int__valid(value): assert isinstance(actual, HexInt) assert actual == value - diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index cd0b0947f3..9a15bf0b9c 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,20 +1,23 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip4_addr_strings +from hypothesis.provisional import ip_addresses from strategies import mac_addr_strings from esphome import core, const class TestHexInt: - @pytest.mark.parametrize("value, expected", ( - (1, "0x01"), - (255, "0xFF"), - (128, "0x80"), - (256, "0x100"), - (-1, "-0x01"), # TODO: this currently fails - )) + @pytest.mark.parametrize( + "value, expected", + ( + (1, "0x01"), + (255, "0xFF"), + (128, "0x80"), + (256, "0x100"), + (-1, "-0x01"), # TODO: this currently fails + ), + ) def test_str(self, value, expected): target = core.HexInt(value) @@ -24,7 +27,7 @@ class TestHexInt: class TestIPAddress: - @given(value=ip4_addr_strings()) + @given(value=ip_addresses(v=4).map(str)) def test_init__valid(self, value): core.IPAddress(*value.split(".")) @@ -33,7 +36,7 @@ class TestIPAddress: with pytest.raises(ValueError, match="IPAddress must consist of 4 items"): core.IPAddress(*value.split(".")) - @given(value=ip4_addr_strings()) + @given(value=ip_addresses(v=4).map(str)) def test_str(self, value): target = core.IPAddress(*value.split(".")) @@ -68,18 +71,14 @@ class TestMACAddress: assert actual.text == "0xDEADBEEF00FFULL" -@pytest.mark.parametrize("value", ( - 1, 2, -1, 0, 1.0, -1.0, 42.0009, -42.0009 -)) +@pytest.mark.parametrize("value", (1, 2, -1, 0, 1.0, -1.0, 42.0009, -42.0009)) def test_is_approximately_integer__in_range(value): actual = core.is_approximately_integer(value) assert actual is True -@pytest.mark.parametrize("value", ( - 42.01, -42.01, 1.5 -)) +@pytest.mark.parametrize("value", (42.01, -42.01, 1.5)) def test_is_approximately_integer__not_in_range(value): actual = core.is_approximately_integer(value) @@ -87,26 +86,29 @@ def test_is_approximately_integer__not_in_range(value): class TestTimePeriod: - @pytest.mark.parametrize("kwargs, expected", ( - ({}, {}), - ({"microseconds": 1}, {"microseconds": 1}), - ({"microseconds": 1.0001}, {"microseconds": 1}), - ({"milliseconds": 2}, {"milliseconds": 2}), - ({"milliseconds": 2.0001}, {"milliseconds": 2}), - ({"milliseconds": 2.01}, {"milliseconds": 2, "microseconds": 10}), - ({"seconds": 3}, {"seconds": 3}), - ({"seconds": 3.0001}, {"seconds": 3}), - ({"seconds": 3.01}, {"seconds": 3, "milliseconds": 10}), - ({"minutes": 4}, {"minutes": 4}), - ({"minutes": 4.0001}, {"minutes": 4}), - ({"minutes": 4.1}, {"minutes": 4, "seconds": 6}), - ({"hours": 5}, {"hours": 5}), - ({"hours": 5.0001}, {"hours": 5}), - ({"hours": 5.1}, {"hours": 5, "minutes": 6}), - ({"days": 6}, {"days": 6}), - ({"days": 6.0001}, {"days": 6}), - ({"days": 6.1}, {"days": 6, "hours": 2, "minutes": 24}), - )) + @pytest.mark.parametrize( + "kwargs, expected", + ( + ({}, {}), + ({"microseconds": 1}, {"microseconds": 1}), + ({"microseconds": 1.0001}, {"microseconds": 1}), + ({"milliseconds": 2}, {"milliseconds": 2}), + ({"milliseconds": 2.0001}, {"milliseconds": 2}), + ({"milliseconds": 2.01}, {"milliseconds": 2, "microseconds": 10}), + ({"seconds": 3}, {"seconds": 3}), + ({"seconds": 3.0001}, {"seconds": 3}), + ({"seconds": 3.01}, {"seconds": 3, "milliseconds": 10}), + ({"minutes": 4}, {"minutes": 4}), + ({"minutes": 4.0001}, {"minutes": 4}), + ({"minutes": 4.1}, {"minutes": 4, "seconds": 6}), + ({"hours": 5}, {"hours": 5}), + ({"hours": 5.0001}, {"hours": 5}), + ({"hours": 5.1}, {"hours": 5, "minutes": 6}), + ({"days": 6}, {"days": 6}), + ({"days": 6.0001}, {"days": 6}), + ({"days": 6.1}, {"days": 6, "hours": 2, "minutes": 24}), + ), + ) def test_init(self, kwargs, expected): target = core.TimePeriod(**kwargs) @@ -118,26 +120,29 @@ class TestTimePeriod: with pytest.raises(ValueError, match="Maximum precision is microseconds"): core.TimePeriod(microseconds=1.1) - @pytest.mark.parametrize("kwargs, expected", ( - ({}, "0s"), - ({"microseconds": 1}, "1us"), - ({"microseconds": 1.0001}, "1us"), - ({"milliseconds": 2}, "2ms"), - ({"milliseconds": 2.0001}, "2ms"), - ({"milliseconds": 2.01}, "2010us"), - ({"seconds": 3}, "3s"), - ({"seconds": 3.0001}, "3s"), - ({"seconds": 3.01}, "3010ms"), - ({"minutes": 4}, "4min"), - ({"minutes": 4.0001}, "4min"), - ({"minutes": 4.1}, "246s"), - ({"hours": 5}, "5h"), - ({"hours": 5.0001}, "5h"), - ({"hours": 5.1}, "306min"), - ({"days": 6}, "6d"), - ({"days": 6.0001}, "6d"), - ({"days": 6.1}, "8784min"), - )) + @pytest.mark.parametrize( + "kwargs, expected", + ( + ({}, "0s"), + ({"microseconds": 1}, "1us"), + ({"microseconds": 1.0001}, "1us"), + ({"milliseconds": 2}, "2ms"), + ({"milliseconds": 2.0001}, "2ms"), + ({"milliseconds": 2.01}, "2010us"), + ({"seconds": 3}, "3s"), + ({"seconds": 3.0001}, "3s"), + ({"seconds": 3.01}, "3010ms"), + ({"minutes": 4}, "4min"), + ({"minutes": 4.0001}, "4min"), + ({"minutes": 4.1}, "246s"), + ({"hours": 5}, "5h"), + ({"hours": 5.0001}, "5h"), + ({"hours": 5.1}, "306min"), + ({"days": 6}, "6d"), + ({"days": 6.0001}, "6d"), + ({"days": 6.1}, "8784min"), + ), + ) def test_str(self, kwargs, expected): target = core.TimePeriod(**kwargs) @@ -145,61 +150,59 @@ class TestTimePeriod: assert actual == expected - @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.TimePeriod(microseconds=900), False), - ("__eq__", core.TimePeriod(milliseconds=1), True), - ("__eq__", core.TimePeriod(microseconds=1100), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), - - ("__ne__", core.TimePeriod(microseconds=900), True), - ("__ne__", core.TimePeriod(milliseconds=1), False), - ("__ne__", core.TimePeriod(microseconds=1100), True), - ("__ne__", 1000, NotImplemented), - ("__ne__", "1000", NotImplemented), - ("__ne__", True, NotImplemented), - ("__ne__", object(), NotImplemented), - ("__ne__", None, NotImplemented), - - ("__lt__", core.TimePeriod(microseconds=900), False), - ("__lt__", core.TimePeriod(milliseconds=1), False), - ("__lt__", core.TimePeriod(microseconds=1100), True), - ("__lt__", 1000, NotImplemented), - ("__lt__", "1000", NotImplemented), - ("__lt__", True, NotImplemented), - ("__lt__", object(), NotImplemented), - ("__lt__", None, NotImplemented), - - ("__gt__", core.TimePeriod(microseconds=900), True), - ("__gt__", core.TimePeriod(milliseconds=1), False), - ("__gt__", core.TimePeriod(microseconds=1100), False), - ("__gt__", 1000, NotImplemented), - ("__gt__", "1000", NotImplemented), - ("__gt__", True, NotImplemented), - ("__gt__", object(), NotImplemented), - ("__gt__", None, NotImplemented), - - ("__le__", core.TimePeriod(microseconds=900), False), - ("__le__", core.TimePeriod(milliseconds=1), True), - ("__le__", core.TimePeriod(microseconds=1100), True), - ("__le__", 1000, NotImplemented), - ("__le__", "1000", NotImplemented), - ("__le__", True, NotImplemented), - ("__le__", object(), NotImplemented), - ("__le__", None, NotImplemented), - - ("__ge__", core.TimePeriod(microseconds=900), True), - ("__ge__", core.TimePeriod(milliseconds=1), True), - ("__ge__", core.TimePeriod(microseconds=1100), False), - ("__ge__", 1000, NotImplemented), - ("__ge__", "1000", NotImplemented), - ("__ge__", True, NotImplemented), - ("__ge__", object(), NotImplemented), - ("__ge__", None, NotImplemented), - )) + @pytest.mark.parametrize( + "comparison, other, expected", + ( + ("__eq__", core.TimePeriod(microseconds=900), False), + ("__eq__", core.TimePeriod(milliseconds=1), True), + ("__eq__", core.TimePeriod(microseconds=1100), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + ("__ne__", core.TimePeriod(microseconds=900), True), + ("__ne__", core.TimePeriod(milliseconds=1), False), + ("__ne__", core.TimePeriod(microseconds=1100), True), + ("__ne__", 1000, NotImplemented), + ("__ne__", "1000", NotImplemented), + ("__ne__", True, NotImplemented), + ("__ne__", object(), NotImplemented), + ("__ne__", None, NotImplemented), + ("__lt__", core.TimePeriod(microseconds=900), False), + ("__lt__", core.TimePeriod(milliseconds=1), False), + ("__lt__", core.TimePeriod(microseconds=1100), True), + ("__lt__", 1000, NotImplemented), + ("__lt__", "1000", NotImplemented), + ("__lt__", True, NotImplemented), + ("__lt__", object(), NotImplemented), + ("__lt__", None, NotImplemented), + ("__gt__", core.TimePeriod(microseconds=900), True), + ("__gt__", core.TimePeriod(milliseconds=1), False), + ("__gt__", core.TimePeriod(microseconds=1100), False), + ("__gt__", 1000, NotImplemented), + ("__gt__", "1000", NotImplemented), + ("__gt__", True, NotImplemented), + ("__gt__", object(), NotImplemented), + ("__gt__", None, NotImplemented), + ("__le__", core.TimePeriod(microseconds=900), False), + ("__le__", core.TimePeriod(milliseconds=1), True), + ("__le__", core.TimePeriod(microseconds=1100), True), + ("__le__", 1000, NotImplemented), + ("__le__", "1000", NotImplemented), + ("__le__", True, NotImplemented), + ("__le__", object(), NotImplemented), + ("__le__", None, NotImplemented), + ("__ge__", core.TimePeriod(microseconds=900), True), + ("__ge__", core.TimePeriod(milliseconds=1), True), + ("__ge__", core.TimePeriod(microseconds=1100), False), + ("__ge__", 1000, NotImplemented), + ("__ge__", "1000", NotImplemented), + ("__ge__", True, NotImplemented), + ("__ge__", object(), NotImplemented), + ("__ge__", None, NotImplemented), + ), + ) def test_comparison(self, comparison, other, expected): target = core.TimePeriod(microseconds=1000) @@ -238,19 +241,19 @@ class TestLambda: "it.strftime(64, 0, ", "my_font", "", - ", TextAlign::TOP_CENTER, \"%H:%M:%S\", ", + ', TextAlign::TOP_CENTER, "%H:%M:%S", ', "esptime", ".", "now());\nit.printf(64, 16, ", "my_font2", "", - ", TextAlign::TOP_CENTER, \"%.1f°C (%.1f%%)\", ", + ', TextAlign::TOP_CENTER, "%.1f°C (%.1f%%)", ', "office_tmp", ".", "state, ", "office_hmd", ".", - "state);\n \nint x = 4; " + "state);\n \nint x = 4; ", ] def test_requires_ids(self): @@ -296,24 +299,33 @@ class TestID: def target(self): return core.ID(None, is_declaration=True, type="binary_sensor::Example") - @pytest.mark.parametrize("id, is_manual, expected", ( - ("foo", None, True), - (None, None, False), - ("foo", True, True), - ("foo", False, False), - (None, True, True), - )) + @pytest.mark.parametrize( + "id, is_manual, expected", + ( + ("foo", None, True), + (None, None, False), + ("foo", True, True), + ("foo", False, False), + (None, True, True), + ), + ) def test_init__resolve_is_manual(self, id, is_manual, expected): target = core.ID(id, is_manual=is_manual) assert target.is_manual == expected - @pytest.mark.parametrize("registered_ids, expected", ( - ([], "binary_sensor_example"), - (["binary_sensor_example"], "binary_sensor_example_2"), - (["foo"], "binary_sensor_example"), - (["binary_sensor_example", "foo", "binary_sensor_example_2"], "binary_sensor_example_3"), - )) + @pytest.mark.parametrize( + "registered_ids, expected", + ( + ([], "binary_sensor_example"), + (["binary_sensor_example"], "binary_sensor_example_2"), + (["foo"], "binary_sensor_example"), + ( + ["binary_sensor_example", "foo", "binary_sensor_example_2"], + "binary_sensor_example_3", + ), + ), + ) def test_resolve(self, target, registered_ids, expected): actual = target.resolve(registered_ids) @@ -326,18 +338,23 @@ class TestID: actual = target.copy() assert actual is not target - assert all(getattr(actual, n) == getattr(target, n) - for n in ("id", "is_declaration", "type", "is_manual")) + assert all( + getattr(actual, n) == getattr(target, n) + for n in ("id", "is_declaration", "type", "is_manual") + ) - @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.ID(id="foo"), True), - ("__eq__", core.ID(id="bar"), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), - )) + @pytest.mark.parametrize( + "comparison, other, expected", + ( + ("__eq__", core.ID(id="foo"), True), + ("__eq__", core.ID(id="bar"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + ), + ) def test_comparison(self, comparison, other, expected): target = core.ID(id="foo") @@ -384,14 +401,17 @@ class TestDocumentRange: class TestDefine: - @pytest.mark.parametrize("name, value, prop, expected", ( - ("ANSWER", None, "as_build_flag", "-DANSWER"), - ("ANSWER", None, "as_macro", "#define ANSWER"), - ("ANSWER", None, "as_tuple", ("ANSWER", None)), - ("ANSWER", 42, "as_build_flag", "-DANSWER=42"), - ("ANSWER", 42, "as_macro", "#define ANSWER 42"), - ("ANSWER", 42, "as_tuple", ("ANSWER", 42)), - )) + @pytest.mark.parametrize( + "name, value, prop, expected", + ( + ("ANSWER", None, "as_build_flag", "-DANSWER"), + ("ANSWER", None, "as_macro", "#define ANSWER"), + ("ANSWER", None, "as_tuple", ("ANSWER", None)), + ("ANSWER", 42, "as_build_flag", "-DANSWER=42"), + ("ANSWER", 42, "as_macro", "#define ANSWER 42"), + ("ANSWER", 42, "as_tuple", ("ANSWER", 42)), + ), + ) def test_properties(self, name, value, prop, expected): target = core.Define(name, value) @@ -399,18 +419,21 @@ class TestDefine: assert actual == expected - @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.Define(name="FOO", value=42), True), - ("__eq__", core.Define(name="FOO", value=13), False), - ("__eq__", core.Define(name="FOO"), False), - ("__eq__", core.Define(name="BAR", value=42), False), - ("__eq__", core.Define(name="BAR"), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), - )) + @pytest.mark.parametrize( + "comparison, other, expected", + ( + ("__eq__", core.Define(name="FOO", value=42), True), + ("__eq__", core.Define(name="FOO", value=13), False), + ("__eq__", core.Define(name="FOO"), False), + ("__eq__", core.Define(name="BAR", value=42), False), + ("__eq__", core.Define(name="BAR"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + ), + ) def test_comparison(self, comparison, other, expected): target = core.Define(name="FOO", value=42) @@ -420,29 +443,48 @@ class TestDefine: class TestLibrary: - @pytest.mark.parametrize("name, value, 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")), - )) - def test_properties(self, name, value, prop, expected): - target = core.Library(name, value) + @pytest.mark.parametrize( + "name, version, repository, prop, expected", + ( + ("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, version, repository, prop, expected): + target = core.Library(name, version, repository) actual = getattr(target, prop) assert actual == expected - @pytest.mark.parametrize("comparison, other, expected", ( - ("__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__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), - )) + @pytest.mark.parametrize( + "comparison, other, expected", + ( + ("__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), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + ), + ) def test_comparison(self, comparison, other, expected): target = core.Library(name="libfoo", version="1.2.3") @@ -459,37 +501,43 @@ class TestEsphomeCore: target.config_path = "foo/config" return target - @pytest.mark.xfail(reason="raw_config and config differ, should they?") def test_reset(self, target): """Call reset on target and compare to new instance""" - other = core.EsphomeCore() + other = core.EsphomeCore().__dict__ target.reset() + t = target.__dict__ + # ignore event loop + del other["event_loop"] + del t["event_loop"] - assert target.__dict__ == other.__dict__ + assert t == other def test_address__none(self, target): + target.config = {} assert target.address is None def test_address__wifi(self, target): + target.config = {} target.config[const.CONF_WIFI] = {const.CONF_USE_ADDRESS: "1.2.3.4"} - target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} + target.config[const.CONF_ETHERNET] = {const.CONF_USE_ADDRESS: "4.3.2.1"} assert target.address == "1.2.3.4" def test_address__ethernet(self, target): - target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} + target.config = {} + target.config[const.CONF_ETHERNET] = {const.CONF_USE_ADDRESS: "4.3.2.1"} 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_generator.py b/tests/unit_tests/test_cpp_generator.py index b130124b54..5a8087ffa9 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -9,18 +9,18 @@ from esphome import cpp_types as ct class TestExpressions: - @pytest.mark.parametrize("target, expected", ( - (cg.RawExpression("foo && bar"), "foo && bar"), - - (cg.AssignmentExpression(None, None, "foo", "bar", None), 'foo = "bar"'), - (cg.AssignmentExpression(ct.float_, "*", "foo", 1, None), 'float *foo = 1'), - (cg.AssignmentExpression(ct.float_, "", "foo", 1, None), 'float foo = 1'), - - (cg.VariableDeclarationExpression(ct.int32, "*", "foo"), "int32_t *foo"), - (cg.VariableDeclarationExpression(ct.int32, "", "foo"), "int32_t foo"), - - (cg.ParameterExpression(ct.std_string, "foo"), "std::string foo"), - )) + @pytest.mark.parametrize( + "target, expected", + ( + (cg.RawExpression("foo && bar"), "foo && bar"), + (cg.AssignmentExpression(None, None, "foo", "bar", None), 'foo = "bar"'), + (cg.AssignmentExpression(ct.float_, "*", "foo", 1, None), "float *foo = 1"), + (cg.AssignmentExpression(ct.float_, "", "foo", 1, None), "float foo = 1"), + (cg.VariableDeclarationExpression(ct.int32, "*", "foo"), "int32_t *foo"), + (cg.VariableDeclarationExpression(ct.int32, "", "foo"), "int32_t foo"), + (cg.ParameterExpression(ct.std_string, "foo"), "std::string foo"), + ), + ) def test_str__simple(self, target: cg.Expression, expected: str): actual = str(target) @@ -67,10 +67,7 @@ class TestTemplateArguments: class TestCallExpression: def test_str__no_template_args(self): - target = cg.CallExpression( - cg.RawExpression("my_function"), - 1, "2", False - ) + target = cg.CallExpression(cg.RawExpression("my_function"), 1, "2", False) actual = str(target) @@ -80,7 +77,9 @@ class TestCallExpression: target = cg.CallExpression( cg.RawExpression("my_function"), cg.TemplateArguments(int, float), - 1, "2", False + 1, + "2", + False, ) actual = str(target) @@ -100,36 +99,32 @@ class TestStructInitializer: actual = str(target) - assert actual == 'foo::MyStruct{\n' \ - ' .state = "on",\n' \ - ' .min_length = 1,\n' \ - ' .max_length = 5,\n' \ - '}' + assert ( + actual == "foo::MyStruct{\n" + ' .state = "on",\n' + " .min_length = 1,\n" + " .max_length = 5,\n" + "}" + ) class TestArrayInitializer: def test_str__empty(self): - target = cg.ArrayInitializer( - None, None - ) + target = cg.ArrayInitializer(None, None) actual = str(target) assert actual == "{}" def test_str__not_multiline(self): - target = cg.ArrayInitializer( - 1, 2, 3, 4 - ) + target = cg.ArrayInitializer(1, 2, 3, 4) actual = str(target) assert actual == "{1, 2, 3, 4}" def test_str__multiline(self): - target = cg.ArrayInitializer( - 1, 2, 3, 4, multiline=True - ) + target = cg.ArrayInitializer(1, 2, 3, 4, multiline=True) actual = str(target) @@ -169,7 +164,7 @@ class TestLambdaExpression: def test_str__with_return(self): target = cg.LambdaExpression( - ("return (foo == 5) && (bar < 10));", ), + ("return (foo == 5) && (bar < 10));",), cg.ParameterListExpression((int, "foo"), (float, "bar")), "=", bool, @@ -185,27 +180,26 @@ class TestLambdaExpression: class TestLiterals: - @pytest.mark.parametrize("target, expected", ( - (cg.StringLiteral("foo"), '"foo"'), - - (cg.IntLiteral(0), "0"), - (cg.IntLiteral(42), "42"), - (cg.IntLiteral(4304967295), "4304967295ULL"), - (cg.IntLiteral(2150483647), "2150483647UL"), - (cg.IntLiteral(-2150083647), "-2150083647LL"), - - (cg.BoolLiteral(True), "true"), - (cg.BoolLiteral(False), "false"), - - (cg.HexIntLiteral(0), "0x00"), - (cg.HexIntLiteral(42), "0x2A"), - (cg.HexIntLiteral(682), "0x2AA"), - - (cg.FloatLiteral(0.0), "0.0f"), - (cg.FloatLiteral(4.2), "4.2f"), - (cg.FloatLiteral(1.23456789), "1.23456789f"), - (cg.FloatLiteral(math.nan), "NAN"), - )) + @pytest.mark.parametrize( + "target, expected", + ( + (cg.StringLiteral("foo"), '"foo"'), + (cg.IntLiteral(0), "0"), + (cg.IntLiteral(42), "42"), + (cg.IntLiteral(4304967295), "4304967295ULL"), + (cg.IntLiteral(2150483647), "2150483647UL"), + (cg.IntLiteral(-2150083647), "-2150083647LL"), + (cg.BoolLiteral(True), "true"), + (cg.BoolLiteral(False), "false"), + (cg.HexIntLiteral(0), "0x00"), + (cg.HexIntLiteral(42), "0x2A"), + (cg.HexIntLiteral(682), "0x2AA"), + (cg.FloatLiteral(0.0), "0.0f"), + (cg.FloatLiteral(4.2), "4.2f"), + (cg.FloatLiteral(1.23456789), "1.23456789f"), + (cg.FloatLiteral(math.nan), "NAN"), + ), + ) def test_str__simple(self, target: cg.Literal, expected: str): actual = str(target) @@ -216,7 +210,9 @@ FAKE_ENUM_VALUE = cg.EnumValue() FAKE_ENUM_VALUE.enum_value = "foo" -@pytest.mark.parametrize("obj, expected_type", ( +@pytest.mark.parametrize( + "obj, expected_type", + ( (cg.RawExpression("foo"), cg.RawExpression), (FAKE_ENUM_VALUE, cg.StringLiteral), (True, cg.BoolLiteral), @@ -230,49 +226,59 @@ FAKE_ENUM_VALUE.enum_value = "foo" (cg.TimePeriodMinutes(minutes=42), cg.IntLiteral), ((1, 2, 3), cg.ArrayInitializer), ([1, 2, 3], cg.ArrayInitializer), -)) + ), +) def test_safe_exp__allowed_values(obj, expected_type): actual = cg.safe_exp(obj) assert isinstance(actual, expected_type) -@pytest.mark.parametrize("obj, expected_type", ( +@pytest.mark.parametrize( + "obj, expected_type", + ( (bool, ct.bool_), (int, ct.int32), (float, ct.float_), -)) + ), +) def test_safe_exp__allowed_types(obj, expected_type): actual = cg.safe_exp(obj) assert actual is expected_type -@pytest.mark.parametrize("obj, expected_error", ( +@pytest.mark.parametrize( + "obj, expected_error", + ( (cg.ID("foo"), "Object foo is an ID."), ((x for x in "foo"), r"Object <.*> is a coroutine."), (None, "Object is not an expression"), -)) + ), +) def test_safe_exp__invalid_values(obj, expected_error): with pytest.raises(ValueError, match=expected_error): cg.safe_exp(obj) class TestStatements: - @pytest.mark.parametrize("target, expected", ( - (cg.RawStatement("foo && bar"), "foo && bar"), - - (cg.ExpressionStatement("foo"), '"foo";'), - (cg.ExpressionStatement(42), '42;'), - - (cg.LineComment("The point of foo is..."), "// The point of foo is..."), - (cg.LineComment("Help help\nI'm being repressed"), "// Help help\n// I'm being repressed"), - + @pytest.mark.parametrize( + "target, expected", ( - cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar", None), - 'static const uint16_t foo[] PROGMEM = "bar"' - ) - )) + (cg.RawStatement("foo && bar"), "foo && bar"), + (cg.ExpressionStatement("foo"), '"foo";'), + (cg.ExpressionStatement(42), "42;"), + (cg.LineComment("The point of foo is..."), "// The point of foo is..."), + ( + cg.LineComment("Help help\nI'm being repressed"), + "// Help help\n// I'm being repressed", + ), + ( + cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar", None), + 'static const uint16_t foo[] PROGMEM = "bar"', + ), + ), + ) def test_str__simple(self, target: cg.Statement, expected: str): actual = str(target) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index d8f32e7a51..ad234250ce 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -3,30 +3,17 @@ from mock import Mock from esphome import cpp_helpers as ch from esphome import const -from esphome.cpp_generator import MockObj -def test_gpio_pin_expression__conf_is_none(monkeypatch): - target = ch.gpio_pin_expression(None) - - actual = next(target) +@pytest.mark.asyncio +async def test_gpio_pin_expression__conf_is_none(monkeypatch): + actual = await ch.gpio_pin_expression(None) assert actual is None -def test_gpio_pin_expression__new_pin(monkeypatch): - target = ch.gpio_pin_expression({ - const.CONF_NUMBER: 42, - const.CONF_MODE: "input", - const.CONF_INVERTED: False - }) - - actual = next(target) - - assert isinstance(actual, MockObj) - - -def test_register_component(monkeypatch): +@pytest.mark.asyncio +async def test_register_component(monkeypatch): var = Mock(base="foo.bar") app_mock = Mock(register_component=Mock(return_value=var)) @@ -38,28 +25,27 @@ def test_register_component(monkeypatch): add_mock = Mock() monkeypatch.setattr(ch, "add", add_mock) - target = ch.register_component(var, {}) - - actual = next(target) + 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 == [] -def test_register_component__no_component_id(monkeypatch): +@pytest.mark.asyncio +async def test_register_component__no_component_id(monkeypatch): var = Mock(base="foo.eek") core_mock = Mock(component_ids=["foo.bar"]) monkeypatch.setattr(ch, "CORE", core_mock) with pytest.raises(ValueError, match="Component ID foo.eek was not declared to"): - target = ch.register_component(var, {}) - next(target) + await ch.register_component(var, {}) -def test_register_component__with_setup_priority(monkeypatch): +@pytest.mark.asyncio +async def test_register_component__with_setup_priority(monkeypatch): var = Mock(base="foo.bar") app_mock = Mock(register_component=Mock(return_value=var)) @@ -71,15 +57,16 @@ def test_register_component__with_setup_priority(monkeypatch): add_mock = Mock() monkeypatch.setattr(ch, "add", add_mock) - target = ch.register_component(var, { - const.CONF_SETUP_PRIORITY: "123", - const.CONF_UPDATE_INTERVAL: "456", - }) - - actual = next(target) + actual = await ch.register_component( + var, + { + const.CONF_SETUP_PRIORITY: "123", + const.CONF_UPDATE_INTERVAL: "456", + }, + ) 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_helpers.py b/tests/unit_tests/test_helpers.py index e48286ae51..00a6b08133 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,94 +1,117 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip4_addr_strings +from hypothesis.provisional import ip_addresses from esphome import helpers -@pytest.mark.parametrize("preferred_string, current_strings, expected", ( +@pytest.mark.parametrize( + "preferred_string, current_strings, expected", + ( ("foo", [], "foo"), # TODO: Should this actually start at 1? ("foo", ["foo"], "foo_2"), ("foo", ("foo",), "foo_2"), ("foo", ("foo", "foo_2"), "foo_3"), ("foo", ("foo", "foo_2", "foo_2"), "foo_3"), -)) + ), +) def test_ensure_unique_string(preferred_string, current_strings, expected): actual = helpers.ensure_unique_string(preferred_string, current_strings) assert actual == expected -@pytest.mark.parametrize("text, expected", ( +@pytest.mark.parametrize( + "text, expected", + ( ("foo", "foo"), ("foo\nbar", "foo\nbar"), ("foo\nbar\neek", "foo\n bar\neek"), -)) + ), +) def test_indent_all_but_first_and_last(text, expected): actual = helpers.indent_all_but_first_and_last(text) assert actual == expected -@pytest.mark.parametrize("text, expected", ( +@pytest.mark.parametrize( + "text, expected", + ( ("foo", [" foo"]), ("foo\nbar", [" foo", " bar"]), ("foo\nbar\neek", [" foo", " bar", " eek"]), -)) + ), +) def test_indent_list(text, expected): actual = helpers.indent_list(text) assert actual == expected -@pytest.mark.parametrize("text, expected", ( +@pytest.mark.parametrize( + "text, expected", + ( ("foo", " foo"), ("foo\nbar", " foo\n bar"), ("foo\nbar\neek", " foo\n bar\n eek"), -)) + ), +) def test_indent(text, expected): actual = helpers.indent(text) assert actual == expected -@pytest.mark.parametrize("string, expected", ( +@pytest.mark.parametrize( + "string, expected", + ( ("foo", '"foo"'), ("foo\nbar", '"foo\\012bar"'), ("foo\\bar", '"foo\\134bar"'), ('foo "bar"', '"foo \\042bar\\042"'), - ('foo 🐍', '"foo \\360\\237\\220\\215"'), -)) + ("foo 🐍", '"foo \\360\\237\\220\\215"'), + ), +) def test_cpp_string_escape(string, expected): actual = helpers.cpp_string_escape(string) assert actual == expected -@pytest.mark.parametrize("host", ( - "127.0.0", "localhost", "127.0.0.b", -)) +@pytest.mark.parametrize( + "host", + ( + "127.0.0", + "localhost", + "127.0.0.b", + ), +) def test_is_ip_address__invalid(host): actual = helpers.is_ip_address(host) assert actual is False -@given(value=ip4_addr_strings()) +@given(value=ip_addresses(v=4).map(str)) def test_is_ip_address__valid(value): actual = helpers.is_ip_address(value) assert actual is True -@pytest.mark.parametrize("var, value, default, expected", ( +@pytest.mark.parametrize( + "var, value, default, expected", + ( ("FOO", None, False, False), ("FOO", None, True, True), ("FOO", "", False, False), ("FOO", "Yes", False, True), ("FOO", "123", False, True), -)) + ), +) def test_get_bool_env(monkeypatch, var, value, default, expected): if value is None: monkeypatch.delenv(var, raising=False) @@ -100,10 +123,7 @@ def test_get_bool_env(monkeypatch, var, value, default, expected): assert actual == expected -@pytest.mark.parametrize("value, expected", ( - (None, False), - ("Yes", True) -)) +@pytest.mark.parametrize("value, expected", ((None, False), ("Yes", True))) def test_is_hassio(monkeypatch, value, expected): if value is None: monkeypatch.delenv("ESPHOME_IS_HASSIO", raising=False) @@ -121,7 +141,7 @@ def test_walk_files(fixture_path): actual = list(helpers.walk_files(path)) # Ensure paths start with the root - assert all(p.startswith(path.as_posix()) for p in actual) + assert all(p.startswith(str(path)) for p in actual) class Test_write_file_if_changed: @@ -185,7 +205,9 @@ class Test_copy_file_if_changed: assert src.read_text() == dst.read_text() -@pytest.mark.parametrize("file1, file2, expected", ( +@pytest.mark.parametrize( + "file1, file2, expected", + ( # Same file ("file-a.txt", "file-a.txt", True), # Different files, different size @@ -198,7 +220,8 @@ class Test_copy_file_if_changed: ("file-a.txt", "", False), # File doesn't exist ("file-a.txt", "file-d.txt", False), -)) + ), +) def test_file_compare(fixture_path, file1, file2, expected): path1 = fixture_path / "helpers" / file1 path2 = fixture_path / "helpers" / file2 diff --git a/tests/unit_tests/test_pins.py b/tests/unit_tests/test_pins.py deleted file mode 100644 index 606c20eea2..0000000000 --- a/tests/unit_tests/test_pins.py +++ /dev/null @@ -1,326 +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 - - @pytest.mark.xfail(reason="This may be 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 new file mode 100644 index 0000000000..59fcfbff60 --- /dev/null +++ b/tests/unit_tests/test_wizard.py @@ -0,0 +1,399 @@ +"""Tests for the wizard.py file.""" + +import esphome.wizard as wz +import pytest +from esphome.components.esp8266.boards import ESP8266_BOARD_PINS +from mock import MagicMock + + +@pytest.fixture +def default_config(): + return { + "name": "test-name", + "platform": "test_platform", + "board": "esp01_1m", + "ssid": "test_ssid", + "psk": "test_psk", + "password": "", + } + + +@pytest.fixture +def wizard_answers(): + return [ + "test-node", # Name of the node + "ESP8266", # platform + "nodemcuv2", # board + "SSID", # ssid + "psk", # wifi password + "ota_pass", # ota password + ] + + +def test_sanitize_quotes_replaces_with_escaped_char(): + """ + The sanitize_quotes function should replace double quotes with their escaped equivalents + """ + # Given + input_str = '"key": "value"' + + # When + output_str = wz.sanitize_double_quotes(input_str) + + # Then + assert output_str == '\\"key\\": \\"value\\"' + + +def test_config_file_fallback_ap_includes_descriptive_name(default_config): + """ + The fallback AP should include the node and a descriptive name + """ + # Given + default_config["name"] = "test_node" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert 'ssid: "Test Node Fallback Hotspot"' in config + + +def test_config_file_fallback_ap_name_less_than_32_chars(default_config): + """ + The fallback AP name must be less than 32 chars. + Since it is composed of the node name and "Fallback Hotspot" this can be too long and needs truncating + """ + # Given + default_config["name"] = "a_very_long_name_for_this_node" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert 'ssid: "A Very Long Name For This Node"' in config + + +def test_config_file_should_include_ota(default_config): + """ + The Over-The-Air update should be enabled by default + """ + # Given + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "ota:" in config + + +def test_config_file_should_include_ota_when_password_set(default_config): + """ + The Over-The-Air update should be enabled when a password is set + """ + # Given + default_config["password"] = "foo" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "ota:" in config + + +def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): + """ + If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards + """ + # Given + del default_config["platform"] + monkeypatch.setattr(wz, "write_file", MagicMock()) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "esp8266:" in generated_config + + +def test_wizard_write_defaults_platform_from_board_esp8266( + default_config, tmp_path, monkeypatch +): + """ + If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards + """ + # Given + del default_config["platform"] + default_config["board"] = [*ESP8266_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "esp8266:" in generated_config + + +def test_wizard_write_defaults_platform_from_board_esp32( + default_config, tmp_path, monkeypatch +): + """ + If the platform is not explicitly set, use "ESP32" if the board is not one of the ESP8266 boards + """ + # Given + del default_config["platform"] + default_config["board"] = "foo" + + monkeypatch.setattr(wz, "write_file", MagicMock()) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "esp32:" in generated_config + + +def test_safe_print_step_prints_step_number_and_description(monkeypatch): + """ + The safe_print_step function prints the step number and the passed description + """ + # Given + monkeypatch.setattr(wz, "safe_print", MagicMock()) + monkeypatch.setattr(wz, "sleep", lambda time: 0) + + step_num = 22 + step_desc = "foobartest" + + # When + wz.safe_print_step(step_num, step_desc) + + # Then + # Collect arguments to all safe_print() calls (substituting "" for any empty ones) + all_args = [ + call.args[0] if len(call.args) else "" for call in wz.safe_print.call_args_list + ] + + assert any(step_desc == arg for arg in all_args) + assert any(f"STEP {step_num}" in arg for arg in all_args) + + +def test_default_input_uses_default_if_no_input_supplied(monkeypatch): + """ + The default_input() function should return the supplied default value if the user doesn't enter anything + """ + + # Given + monkeypatch.setattr("builtins.input", lambda _: "") + default_string = "foobar" + + # When + retval = wz.default_input("", default_string) + + # Then + assert retval == default_string + + +def test_default_input_uses_user_supplied_value(monkeypatch): + """ + The default_input() function should return the value that the user enters + """ + + # Given + user_input = "A value" + monkeypatch.setattr("builtins.input", lambda _: user_input) + default_string = "foobar" + + # When + retval = wz.default_input("", default_string) + + # Then + assert retval == user_input + + +def test_strip_accents_removes_diacritics(): + """ + The strip_accents() function should remove diacritics (umlauts) + """ + + # Given + input_str = "Kühne" + expected_str = "Kuhne" + + # When + output_str = wz.strip_accents(input_str) + + # Then + assert output_str == expected_str + + +def test_wizard_rejects_path_with_invalid_extension(): + """ + The wizard should reject config files that are not yaml + """ + + # Given + config_file = "test.json" + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 1 + + +def test_wizard_rejects_existing_files(tmpdir): + """ + The wizard should reject any configuration file that already exists + """ + + # Given + config_file = tmpdir.join("test.yaml") + config_file.write("") + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 2 + + +def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answers): + """ + The wizard should accept the given default answers for esp8266 + """ + + # Given + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answers): + """ + The wizard should accept the given default answers for esp32 + """ + + # Given + wizard_answers[1] = "ESP32" + wizard_answers[2] = "nodemcu-32s" + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): + """ + When the node name does not conform, a better alternative is offered + * Removes special chars + * Replaces spaces with hyphens + * Replaces underscores with hyphens + * Converts all uppercase letters to lowercase + """ + + # Given + wizard_answers[0] = "Küche_Unten #2" + expected_name = "kuche-unten-2" + monkeypatch.setattr( + wz, "default_input", MagicMock(side_effect=lambda _, default: default) + ) + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + assert wz.default_input.call_args.args[1] == expected_name + + +def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): + """ + When the platform is not either esp32 or esp8266, the wizard should reject it + """ + + # Given + wizard_answers.insert(1, "foobar") # add invalid entry for platform + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): + """ + When the board is not a valid esp8266 board, the wizard should reject it + """ + + # Given + wizard_answers.insert(2, "foobar") # add an invalid entry for board + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): + """ + When the board is not a valid esp8266 board, the wizard should reject it + """ + + # Given + wizard_answers.insert(3, "") # add an invalid entry for ssid + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0