diff --git a/.clang-tidy b/.clang-tidy index 79276f81c3..b40e606121 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,11 +5,8 @@ Checks: >- -altera-*, -android-*, -boost-*, - -bugprone-branch-clone, - -bugprone-easily-swappable-parameters, -bugprone-narrowing-conversions, -bugprone-signed-char-misuse, - -bugprone-too-small-loop-variable, -cert-dcl50-cpp, -cert-err58-cpp, -cert-oop57-cpp, @@ -19,12 +16,10 @@ Checks: >- -clang-diagnostic-delete-abstract-non-virtual-dtor, -clang-diagnostic-delete-non-abstract-non-virtual-dtor, -clang-diagnostic-shadow-field, - -clang-diagnostic-sign-compare, - -clang-diagnostic-unused-variable, -clang-diagnostic-unused-const-variable, + -clang-diagnostic-unused-parameter, -concurrency-*, -cppcoreguidelines-avoid-c-arrays, - -cppcoreguidelines-avoid-goto, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-init-variables, -cppcoreguidelines-macro-usage, @@ -41,7 +36,6 @@ Checks: >- -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-special-member-functions, - -fuchsia-default-arguments, -fuchsia-multiple-inheritance, -fuchsia-overloaded-operator, -fuchsia-statically-constructed-objects, @@ -51,6 +45,7 @@ Checks: >- -google-explicit-constructor, -google-readability-braces-around-statements, -google-readability-casting, + -google-readability-namespace-comments, -google-readability-todo, -google-runtime-references, -hicpp-*, @@ -97,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' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aa90ef365f..25411c19f5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,6 +16,7 @@ Quick description and explanation of changes ## Test Environment - [ ] ESP32 +- [ ] ESP32 IDF - [ ] ESP8266 ## Example entry for `config.yaml`: 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/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 12f5a7dfc2..1d1cc169b2 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -17,6 +17,10 @@ on: - 'requirements*.txt' - 'platformio.ini' +permissions: + contents: read + packages: read + jobs: check-docker: name: Build docker containers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45e2f2735c..9473dc87dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,3 @@ -# THESE JOBS ARE COPIED IN release.yml and release-dev.yml -# PLEASE ALSO UPDATE THOSE FILES WHEN CHANGING LINES HERE name: CI on: @@ -8,6 +6,13 @@ on: 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 }} @@ -31,7 +36,7 @@ jobs: - id: test file: tests/test3.yaml name: Test tests/test3.yaml - pio_cache_key: test1 + pio_cache_key: test3 - id: test file: tests/test4.yaml name: Test tests/test4.yaml @@ -46,26 +51,26 @@ jobs: name: Run script/clang-format - id: clang-tidy name: Run script/clang-tidy for ESP8266 - options: --environment esp8266-tidy --grep USE_ESP8266 + options: --environment esp8266-arduino-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 + name: Run script/clang-tidy for ESP32 Arduino 1/4 + options: --environment esp32-arduino-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 + name: Run script/clang-tidy for ESP32 Arduino 2/4 + options: --environment esp32-arduino-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 + name: Run script/clang-tidy for ESP32 Arduino 3/4 + options: --environment esp32-arduino-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 + name: Run script/clang-tidy for ESP32 Arduino 4/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 4 pio_cache_key: tidyesp32 - id: clang-tidy - name: Run script/clang-tidy for ESP32 esp-idf + name: Run script/clang-tidy for ESP32 IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF pio_cache_key: tidyesp32-idf @@ -77,18 +82,23 @@ jobs: with: python-version: '3.7' - - name: Cache pip modules + - name: Cache virtualenv uses: actions/cache@v2 with: - path: ~/.cache/pip - key: pip-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} + path: .venv + key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} restore-keys: | - pip-${{ steps.python.outputs.python-version }}- + venv-${{ steps.python.outputs.python-version }}- - - name: Set up python environment + - name: Set up virtualenv run: | - pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt - pip3 install -e . + 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 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 375b8f1db4..ceb45b2a91 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,13 +9,19 @@ permissions: issues: write pull-requests: write +concurrency: + group: lock + jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v3 with: - github-token: ${{ github.token }} - pr-lock-inactive-days: "1" + pr-inactive-days: "1" pr-lock-reason: "" - process-only: prs + 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 index 4e1eafff5e..1d5f2551cd 100644 --- a/.github/workflows/matchers/ci-custom.json +++ b/.github/workflows/matchers/ci-custom.json @@ -4,7 +4,7 @@ "owner": "ci-custom", "pattern": [ { - "regexp": "^ERROR (.*):(\\d+):(\\d+) - (.*)$", + "regexp": "^(.*):(\\d+):(\\d+):\\s+lint:\\s+(.*)$", "file": 1, "line": 2, "column": 3, diff --git a/.github/workflows/matchers/gcc.json b/.github/workflows/matchers/gcc.json index 899239f816..a00d9c33f4 100644 --- a/.github/workflows/matchers/gcc.json +++ b/.github/workflows/matchers/gcc.json @@ -5,7 +5,7 @@ "severity": "error", "pattern": [ { - "regexp": "^(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "regexp": "^src/(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, diff --git a/.github/workflows/matchers/lint-python.json b/.github/workflows/matchers/lint-python.json index decbe36c4a..6a09f04770 100644 --- a/.github/workflows/matchers/lint-python.json +++ b/.github/workflows/matchers/lint-python.json @@ -1,11 +1,22 @@ { "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}.*)$", + "regexp": "^(.*):(\\d+): ([EFCDNW]\\d{3}.*)$", "file": 1, "line": 2, "message": 3 @@ -17,7 +28,7 @@ "severity": "error", "pattern": [ { - "regexp": "^(.*):(\\d+) - (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$", + "regexp": "^(.*):(\\d+): (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$", "file": 1, "line": 2, "message": 3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afd893d065..d6895becc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,9 @@ on: schedule: - cron: "0 2 * * *" +permissions: + contents: read + jobs: init: name: Initialize build @@ -52,6 +55,9 @@ jobs: 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: @@ -93,6 +99,9 @@ jobs: deploy-docker-manifest: if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write runs-on: ubuntu-latest needs: [init, deploy-docker] strategy: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 712ae1a289..c3e450d0cf 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,13 +9,15 @@ permissions: issues: write pull-requests: write +concurrency: + group: lock + jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v4 with: - repo-token: ${{ github.token }} days-before-pr-stale: 90 days-before-pr-close: 7 days-before-issue-stale: -1 @@ -28,3 +30,19 @@ jobs: 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/.gitpod.yml b/.gitpod.yml index 2ff20a0366..e3f786a403 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,4 +3,4 @@ ports: onOpen: open-preview tasks: - before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup - command: python -m esphome config dashboard + command: python -m esphome dashboard config diff --git a/CODEOWNERS b/CODEOWNERS index 7b16959b87..3f9c4f89ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,17 +28,23 @@ esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bl0940/* @tobias- esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth +esphome/components/bmp3xx/* @martgras +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/cd74hc4067/* @asoehlke 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 @@ -52,6 +58,8 @@ esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_controller/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz +esphome/components/esp32_camera_web_server/* @ayufan +esphome/components/esp32_can/* @Sympatron esphome/components/esp32_improv/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/exposure_notifications/* @OttoWinter @@ -62,6 +70,7 @@ 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 @@ -70,12 +79,14 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core -esphome/components/improv/* @jesserockz +esphome/components/improv_serial/* @esphome/core +esphome/components/ina260/* @MrEditor97 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/kalman_combinator/* @Cat-Ion esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/logger/* @esphome/core @@ -89,9 +100,13 @@ esphome/components/mcp23x08_base/* @jesserockz esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho +esphome/components/mcp3204/* @rsumner +esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9808/* @k7hpn +esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/midea/* @dudanov +esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras @@ -119,6 +134,7 @@ 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/psram/* @esphome/core esphome/components/pulse_meter/* @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/rc522/* @glmnet @@ -128,7 +144,7 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet -esphome/components/safe_mode/* @paulmonigatti +esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/scd4x/* @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces @@ -138,7 +154,7 @@ esphome/components/select/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny -esphome/components/shutdown/* @esphome/core +esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/socket/* @esphome/core @@ -175,8 +191,10 @@ 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 diff --git a/docker/Dockerfile b/docker/Dockerfile index e66c3e1d95..330901a776 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,12 +5,12 @@ # One of "docker", "hassio" ARG BASEIMGTYPE=docker -FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.0 AS base-hassio-amd64 -FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.0 AS base-hassio-arm64 -FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.0 AS base-hassio-armv7 -FROM debian:bullseye-20210902-slim AS base-docker-amd64 -FROM debian:bullseye-20210902-slim AS base-docker-arm64 -FROM debian:bullseye-20210902-slim AS base-docker-armv7 +FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 +FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 +FROM debian:bullseye-20211220-slim AS base-docker-amd64 +FROM debian:bullseye-20211220-slim AS base-docker-arm64 +FROM debian:bullseye-20211220-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 @@ -27,7 +27,7 @@ RUN \ python3-cryptography=3.3.2-1 \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ - curl=7.74.0-1.3+b1 \ + curl=7.74.0-1.3+deb11u1 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ @@ -42,8 +42,8 @@ ENV \ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ - wheel==0.36.2 \ - platformio==5.2.0 \ + wheel==0.37.1 \ + platformio==5.2.4 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_libraries_interval 1000000 \ @@ -64,7 +64,7 @@ RUN \ # Copy esphome and install COPY . /esphome -RUN pip3 install --no-cache-dir -e /esphome +RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome # Settings for dashboard ENV USERNAME="" PASSWORD="" @@ -112,7 +112,7 @@ RUN \ # Copy esphome and install COPY . /esphome -RUN pip3 install --no-cache-dir -e /esphome +RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome # Labels LABEL \ @@ -147,9 +147,9 @@ RUN \ /var/{cache,log}/* \ /var/lib/apt/lists/* -COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / +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 \ + pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ && /platformio_install_deps.py /platformio.ini VOLUME ["/esphome"] diff --git a/docker/build.py b/docker/build.py index 1157d8287a..d5926ae3d4 100755 --- a/docker/build.py +++ b/docker/build.py @@ -32,6 +32,7 @@ parser.add_argument("--dry-run", action="store_true", help="Don't run any comman 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") +build_parser.add_argument("--load", help="Load the docker image locally", action="store_true") manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") @@ -132,6 +133,8 @@ def main(): cmd += ["--tag", img] if args.push: cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"] + if args.load: + cmd += ["--load"] run_command(*cmd, ".") elif args.command == "manifest": diff --git a/docker/platformio_install_deps.py b/docker/platformio_install_deps.py index 5625bd4d01..c7b11cf321 100755 --- a/docker/platformio_install_deps.py +++ b/docker/platformio_install_deps.py @@ -8,6 +8,23 @@ import sys config = configparser.ConfigParser(inline_comment_prefixes=(';', )) config.read(sys.argv[1]) -libs = [x for x in config['common']['lib_deps'].splitlines() if len(x) != 0] + +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/esphome/__main__.py b/esphome/__main__.py index feb95e93c7..6f57791480 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, + SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine from esphome.helpers import indent @@ -144,6 +145,8 @@ def wrap_to_code(name, comp): if comp.config_schema is not None: conf_str = yaml_util.dump(conf) 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))) await coro(conf) @@ -180,7 +183,11 @@ 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): @@ -196,8 +203,7 @@ def upload_using_esptool(config, port): firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" flash_images = [ platformio_api.FlashImage( - path=idedata.firmware_bin_path, - offset=firmware_offset, + path=idedata.firmware_bin_path, offset=firmware_offset ), *idedata.extra_flash_images, ] @@ -222,6 +228,8 @@ def upload_using_esptool(config, port): mcu, "write_flash", "-z", + "--flash_size", + "detect", ] for img in flash_images: cmd += [img.offset, img.path] @@ -458,6 +466,21 @@ def command_update_all(args): 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, @@ -475,6 +498,7 @@ POST_CONFIG_ACTIONS = { "clean-mqtt": command_clean_mqtt, "mqtt-fingerprint": command_mqtt_fingerprint, "clean": command_clean, + "idedata": command_idedata, } @@ -585,10 +609,7 @@ def parse_args(argv): "wizard", help="A helpful setup wizard that will guide you through setting up ESPHome.", ) - parser_wizard.add_argument( - "configuration", - help="Your YAML configuration file.", - ) + parser_wizard.add_argument("configuration", help="Your YAML configuration file.") parser_fingerprint = subparsers.add_parser( "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." @@ -610,8 +631,7 @@ def parse_args(argv): "dashboard", help="Create a simple web server for a dashboard." ) parser_dashboard.add_argument( - "configuration", - help="Your YAML configuration file directory.", + "configuration", help="Your YAML configuration file directory." ) parser_dashboard.add_argument( "--port", @@ -619,6 +639,12 @@ def parse_args(argv): 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.", @@ -650,6 +676,11 @@ def parse_args(argv): "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 . # @@ -733,7 +764,12 @@ def run_esphome(argv): args = parse_args(argv) CORE.dashboard = args.dashboard - setup_log(args.verbose, args.quiet) + 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 " @@ -757,12 +793,16 @@ def run_esphome(argv): 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(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: diff --git a/esphome/automation.py b/esphome/automation.py index 0768bf8869..fab998527f 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_AUTOMATION_ID, CONF_CONDITION, + CONF_COUNT, CONF_ELSE, CONF_ID, CONF_THEN, @@ -66,6 +67,7 @@ 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") @@ -241,6 +243,25 @@ async def while_action_to_code(config, action_id, template_arg, args): 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( { diff --git a/esphome/codegen.py b/esphome/codegen.py index 4f9f67245d..3ea3df8706 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -75,10 +75,10 @@ from esphome.cpp_types import ( # noqa optional, arduino_json_ns, JsonObject, - JsonObjectRef, - JsonObjectConstRef, + JsonObjectConst, Controller, GPIOPin, InternalGPIOPin, gpio_Flags, + EntityCategory, ) diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index d9c2892d21..35e98d7360 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -25,7 +25,7 @@ void AdalightLightEffect::stop() { AddressableLightEffect::stop(); } -int AdalightLightEffect::get_frame_size_(int led_count) const { +unsigned int AdalightLightEffect::get_frame_size_(int led_count) const { // 3 bytes: Ada // 2 bytes: LED count // 1 byte: checksum diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h index c1df55659b..b757191864 100644 --- a/esphome/components/adalight/adalight_light_effect.h +++ b/esphome/components/adalight/adalight_light_effect.h @@ -25,7 +25,7 @@ class AdalightLightEffect : public light::AddressableLightEffect, public uart::U CONSUMED, }; - int get_frame_size_(int led_count) const; + 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); diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index c8f8b0e0f6..0a439f8b8d 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -1,5 +1,6 @@ #include "adc_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC @@ -15,50 +16,6 @@ namespace adc { static const char *const TAG = "adc"; -#ifdef USE_ESP32 -void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } - -inline adc1_channel_t gpio_to_adc1(uint8_t pin) { -#if CONFIG_IDF_TARGET_ESP32 - switch (pin) { - case 36: - return ADC1_CHANNEL_0; - case 37: - return ADC1_CHANNEL_1; - case 38: - return ADC1_CHANNEL_2; - case 39: - return ADC1_CHANNEL_3; - case 32: - return ADC1_CHANNEL_4; - case 33: - return ADC1_CHANNEL_5; - case 34: - return ADC1_CHANNEL_6; - case 35: - return ADC1_CHANNEL_7; - default: - return ADC1_CHANNEL_MAX; - } -#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 - switch (pin) { - case 0: - return ADC1_CHANNEL_0; - case 1: - return ADC1_CHANNEL_1; - case 2: - return ADC1_CHANNEL_2; - case 3: - return ADC1_CHANNEL_3; - case 4: - return ADC1_CHANNEL_4; - default: - return ADC1_CHANNEL_MAX; - } -#endif -} -#endif - void ADCSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); #ifndef USE_ADC_SENSOR_VCC @@ -66,13 +23,36 @@ void ADCSensor::setup() { #endif #ifdef USE_ESP32 - adc1_config_channel_atten(gpio_to_adc1(pin_->get_pin()), attenuation_); adc1_config_width(ADC_WIDTH_BIT_12); -#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 - adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_->get_pin())); -#endif + 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 USE_ESP8266 @@ -81,84 +61,107 @@ void ADCSensor::dump_config() { #else LOG_PIN(" Pin: ", pin_); #endif -#endif +#endif // USE_ESP8266 + #ifdef USE_ESP32 LOG_PIN(" Pin: ", pin_); - 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 + 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); } -float ADCSensor::sample() { -#ifdef USE_ESP32 - int raw = adc1_get_raw(gpio_to_adc1(pin_->get_pin())); - float value_v = raw / 4095.0f; -#if CONFIG_IDF_TARGET_ESP32 - switch (this->attenuation_) { - case ADC_ATTEN_DB_0: - value_v *= 1.1; - break; - case ADC_ATTEN_DB_2_5: - value_v *= 1.5; - break; - case ADC_ATTEN_DB_6: - value_v *= 2.2; - break; - case ADC_ATTEN_DB_11: - value_v *= 3.9; - break; - default: // This is to satisfy the unused ADC_ATTEN_MAX - break; - } -#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 - switch (this->attenuation_) { - case ADC_ATTEN_DB_0: - value_v *= 0.84; - break; - case ADC_ATTEN_DB_2_5: - value_v *= 1.13; - break; - case ADC_ATTEN_DB_6: - value_v *= 1.56; - break; - case ADC_ATTEN_DB_11: - value_v *= 3.0; - break; - default: // This is to satisfy the unused ADC_ATTEN_MAX - break; - } -#endif - return value_v; -#endif #ifdef USE_ESP8266 +float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC - return ESP.getVcc() / 1024.0f; // NOLINT(readability-static-accessed-through-instance) + int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) #else - return analogRead(this->pin_->get_pin()) / 1024.0f; // NOLINT -#endif + int raw = analogRead(this->pin_->get_pin()); // NOLINT #endif + if (output_raw_) { + return raw; + } + return raw / 1024.0f; } +#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); +} +#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 b8c702be4e..12272a1577 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -8,6 +8,7 @@ #ifdef USE_ESP32 #include "driver/adc.h" +#include #endif namespace esphome { @@ -17,7 +18,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage public: #ifdef USE_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. - void set_attenuation(adc_atten_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. @@ -28,6 +31,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage /// `HARDWARE_LATE` setup priority. float get_setup_priority() const override; void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } + void set_output_raw(bool output_raw) { output_raw_ = output_raw; } float sample() override; #ifdef USE_ESP8266 @@ -36,9 +40,13 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage protected: InternalGPIOPin *pin_; + bool output_raw_{false}; #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 9a0407d0f4..c812e67a68 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -4,14 +4,24 @@ from esphome import pins from esphome.components import sensor, voltage_sampler 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"] @@ -21,6 +31,62 @@ ATTENUATION_MODES = { "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, + }, } @@ -29,15 +95,16 @@ def validate_adc_pin(value): return cv.only_on_esp8266("VCC") if CORE.is_esp32: - from esphome.components.esp32 import is_esp32c3 - value = pins.internal_gpio_input_pin_number(value) - if is_esp32c3(): - if not (0 <= value <= 4): # ADC1 - raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") - if not (32 <= value <= 39): # ADC1 - raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") - elif CORE.is_esp8266: + 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})( @@ -49,10 +116,14 @@ def validate_adc_pin(value): return pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) - else: - raise NotImplementedError - return pins.internal_gpio_input_pin_schema(value) + raise NotImplementedError + + +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 adc_ns = cg.esphome_ns.namespace("adc") @@ -60,7 +131,7 @@ ADCSensor = adc_ns.class_( "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, @@ -71,12 +142,14 @@ CONFIG_SCHEMA = ( { 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")) + .extend(cv.polling_component_schema("60s")), + validate_config, ) @@ -91,5 +164,17 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) + if CONF_RAW in config: + cg.add(var.set_output_raw(config[CONF_RAW])) + 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/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h index c6fb383ed8..bb160cd8eb 100644 --- a/esphome/components/ade7953/ade7953.h +++ b/esphome/components/ade7953/ade7953.h @@ -76,9 +76,9 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { return err; *value = 0; *value |= ((uint32_t) recv[0]) << 24; - *value |= ((uint32_t) recv[1]) << 24; - *value |= ((uint32_t) recv[2]) << 24; - *value |= ((uint32_t) recv[3]) << 24; + *value |= ((uint32_t) recv[1]) << 16; + *value |= ((uint32_t) recv[2]) << 8; + *value |= ((uint32_t) recv[3]); return i2c::ERROR_OK; } diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 713199212c..3c690c39b5 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -73,13 +73,6 @@ void AHT10Component::update() { bool success = false; for (int i = 0; i < AHT10_ATTEMPTS; ++i) { ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis()); - delay_microseconds_accurate(4); - - uint8_t reg = 0; - if (this->write(®, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); - continue; - } delay(delay_ms); if (this->read(data, 6) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); @@ -117,12 +110,12 @@ 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) { diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index b53eb69464..c06a2a34d7 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -38,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) diff --git a/esphome/components/am43/sensor.py b/esphome/components/am43/sensor.py index c88e529a0c..68c85d0e9c 100644 --- a/esphome/components/am43/sensor.py +++ b/esphome/components/am43/sensor.py @@ -4,7 +4,8 @@ from esphome.components import sensor, ble_client from esphome.const import ( CONF_ID, CONF_BATTERY_LEVEL, - ICON_BATTERY, + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_PERCENT, @@ -20,10 +21,15 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(Am43), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, ICON_BATTERY, 0 + 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_PERCENT, ICON_BRIGHTNESS_5, 0 + unit_of_measurement=UNIT_PERCENT, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=0, ), } ) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 3f03e5c185..7c9ff07f97 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -44,8 +44,9 @@ async def to_code(config): width, height = image.size frames = image.n_frames if CONF_RESIZE in config: - image.thumbnail(config[CONF_RESIZE]) - width, height = image.size + 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( @@ -59,7 +60,13 @@ async def to_code(config): 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 @@ -70,7 +77,13 @@ async def to_code(config): 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 @@ -85,6 +98,8 @@ async def to_code(config): 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)): diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 2e6910f326..4f8f0d0ee2 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -30,7 +30,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_heat_mode(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); diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index 811a34a27a..ce4febbe37 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -73,51 +73,46 @@ AnovaPacket *AnovaCodec::get_stop_request() { } void AnovaCodec::decode(const uint8_t *data, uint16_t length) { - memset(this->buf_, 0, 32); - strncpy(this->buf_, (char *) data, 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(this->buf_, "stopped", 7)) { + if (!strncmp(buf, "stopped", 7)) { this->has_running_ = true; this->running_ = false; } - if (!strncmp(this->buf_, "running", 7)) { + if (!strncmp(buf, "running", 7)) { this->has_running_ = true; this->running_ = true; } break; } case START: { - if (!strncmp(this->buf_, "start", 5)) { + if (!strncmp(buf, "start", 5)) { this->has_running_ = true; this->running_ = true; } break; } case STOP: { - if (!strncmp(this->buf_, "stop", 4)) { + if (!strncmp(buf, "stop", 4)) { this->has_running_ = true; this->running_ = false; } break; } - case READ_TARGET_TEMPERATURE: { - this->target_temp_ = strtof(this->buf_, nullptr); - if (this->fahrenheit_) - this->target_temp_ = ftoc(this->target_temp_); - this->has_target_temp_ = true; - break; - } + case READ_TARGET_TEMPERATURE: case SET_TARGET_TEMPERATURE: { - this->target_temp_ = strtof(this->buf_, nullptr); + 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_ = strtof(this->buf_, nullptr); + 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; @@ -125,8 +120,8 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { } case SET_UNIT: case READ_UNIT: { - this->unit_ = this->buf_[0]; - this->fahrenheit_ = this->buf_[0] == 'f'; + this->unit_ = buf[0]; + this->fahrenheit_ = buf[0] == 'f'; this->has_unit_ = true; break; } diff --git a/esphome/components/anova/anova_base.h b/esphome/components/anova/anova_base.h index 7c1383512d..b831157849 100644 --- a/esphome/components/anova/anova_base.h +++ b/esphome/components/anova/anova_base.h @@ -70,7 +70,6 @@ class AnovaCodec { bool has_current_temp_; bool has_unit_; bool has_running_; - char buf_[32]; bool fahrenheit_; CurrentQuery current_query_; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index b0608a69dd..6b2e7fd06b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -121,7 +121,7 @@ async def to_code(config): decoded = base64.b64decode(conf[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.3") + cg.add_library("esphome/noise-c", "0.1.4") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 5a6eba004c..dca722dca5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -40,6 +40,7 @@ service APIConnection { rpc climate_command (ClimateCommandRequest) returns (void) {} rpc number_command (NumberCommandRequest) returns (void) {} rpc select_command (SelectCommandRequest) returns (void) {} + rpc button_command (ButtonCommandRequest) returns (void) {} } @@ -182,6 +183,8 @@ message DeviceInfoResponse { // The esphome project details if set string project_name = 8; string project_version = 9; + + uint32 webserver_port = 10; } message ListEntitiesRequest { @@ -201,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; @@ -216,6 +227,7 @@ message ListEntitiesBinarySensorResponse { bool is_status_binary_sensor = 6; bool disabled_by_default = 7; string icon = 8; + EntityCategory entity_category = 9; } message BinarySensorStateResponse { option (id) = 21; @@ -247,6 +259,7 @@ message ListEntitiesCoverResponse { string device_class = 8; bool disabled_by_default = 9; string icon = 10; + EntityCategory entity_category = 11; } enum LegacyCoverState { @@ -316,6 +329,7 @@ message ListEntitiesFanResponse { int32 supported_speed_count = 8; bool disabled_by_default = 9; string icon = 10; + EntityCategory entity_category = 11; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -392,6 +406,7 @@ message ListEntitiesLightResponse { repeated string effects = 11; bool disabled_by_default = 13; string icon = 14; + EntityCategory entity_category = 15; } message LightStateResponse { option (id) = 24; @@ -480,6 +495,7 @@ message ListEntitiesSensorResponse { // 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; @@ -508,6 +524,7 @@ message ListEntitiesSwitchResponse { string icon = 5; bool assumed_state = 6; bool disabled_by_default = 7; + EntityCategory entity_category = 8; } message SwitchStateResponse { option (id) = 26; @@ -541,6 +558,7 @@ message ListEntitiesTextSensorResponse { string icon = 5; bool disabled_by_default = 6; + EntityCategory entity_category = 7; } message TextSensorStateResponse { option (id) = 27; @@ -701,6 +719,8 @@ message ListEntitiesCameraResponse { string name = 3; string unique_id = 4; bool disabled_by_default = 5; + string icon = 6; + EntityCategory entity_category = 7; } message CameraImageResponse { @@ -795,6 +815,7 @@ message ListEntitiesClimateResponse { repeated string supported_custom_presets = 17; bool disabled_by_default = 18; string icon = 19; + EntityCategory entity_category = 20; } message ClimateStateResponse { option (id) = 47; @@ -848,6 +869,11 @@ message ClimateCommandRequest { } // ==================== NUMBER ==================== +enum NumberMode { + NUMBER_MODE_AUTO = 0; + NUMBER_MODE_BOX = 1; + NUMBER_MODE_SLIDER = 2; +} message ListEntitiesNumberResponse { option (id) = 49; option (source) = SOURCE_SERVER; @@ -863,6 +889,9 @@ message ListEntitiesNumberResponse { 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; @@ -900,6 +929,7 @@ message ListEntitiesSelectResponse { string icon = 5; repeated string options = 6; bool disabled_by_default = 7; + EntityCategory entity_category = 8; } message SelectStateResponse { option (id) = 53; @@ -922,3 +952,28 @@ message SelectCommandRequest { 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 47171ba50f..1f629c2c85 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -20,6 +20,7 @@ namespace esphome { namespace api { static const char *const TAG = "api.connection"; +static const int ESP32_CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { @@ -78,6 +79,8 @@ void APIConnection::loop() { 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); } @@ -130,7 +133,7 @@ void APIConnection::loop() { if (state_subs_at_ != -1) { const auto &subs = this->parent_->get_state_subs(); - if (state_subs_at_ >= subs.size()) { + if (state_subs_at_ >= (int) subs.size()) { state_subs_at_ = -1; } else { auto &it = subs[state_subs_at_]; @@ -182,6 +185,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ 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 @@ -215,6 +219,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { 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) { @@ -281,6 +286,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { 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) { @@ -344,6 +350,7 @@ bool APIConnection::send_light_info(light::LightState *light) { 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)); @@ -430,7 +437,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { 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 @@ -454,6 +461,7 @@ bool APIConnection::send_switch_info(switch_::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) { @@ -489,6 +497,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) 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 @@ -535,6 +544,7 @@ bool APIConnection::send_climate_info(climate::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(); @@ -609,6 +619,9 @@ bool APIConnection::send_number_info(number::Number *number) { 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(); @@ -646,6 +659,7 @@ bool APIConnection::send_select_info(select::Select *select) { 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); @@ -663,13 +677,37 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { } #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(std::move(image)); + if (image->was_requested_by(esphome::esp32_camera::API_REQUESTER) || + image->was_requested_by(esphome::esp32_camera::IDLE)) + this->image_reader_.set_image(std::move(image)); } bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { ListEntitiesCameraResponse msg; @@ -678,6 +716,8 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { 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) { @@ -685,9 +725,14 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { return; if (msg.single) - esp32_camera::global_esp32_camera->request_image(); - if (msg.stream) - esp32_camera::global_esp32_camera->request_stream(); + esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::API_REQUESTER); + if (msg.stream) { + esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::API_REQUESTER); + + App.scheduler.set_timeout(this->parent_, "api_esp32_camera_stop_stream", ESP32_CAMERA_STOP_STREAM, []() { + esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::API_REQUESTER); + }); + } } #endif @@ -756,6 +801,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #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; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a1f1769a19..72697b5911 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -73,6 +73,10 @@ class APIConnection : public APIServerConnection { 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) { diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 4971272f41..d9eadb2aaa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,6 +1,7 @@ #include "api_frame_helper.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "proto.h" #include @@ -10,7 +11,7 @@ namespace api { static const char *const TAG = "api.socket"; -/// Is the given return value (from read/write syscalls) a wouldblock error? +/// 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; @@ -64,6 +65,8 @@ const char *api_error_to_str(APIError err) { 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"; } @@ -172,9 +175,6 @@ APIError APINoiseFrameHelper::loop() { * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. */ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { - int err; - APIError aerr; - if (frame == nullptr) { HELPER_LOG("Bad argument for try_read_frame_"); return APIError::BAD_ARG; @@ -185,15 +185,20 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // no header information yet size_t to_read = 3 - rx_header_buf_len_; ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + 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 (received != to_read) { + if ((size_t) received != to_read) { // not a full read return APIError::WOULD_BLOCK; } @@ -227,15 +232,20 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read size_t to_read = msg_size - rx_buf_len_; ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + 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 (received != to_read) { + if ((size_t) received != to_read) { // not all read return APIError::WOULD_BLOCK; } @@ -243,7 +253,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // uncomment for even more debugging #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); #endif frame->msg = std::move(rx_buf_); // consume msg @@ -532,13 +542,13 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() { APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { if (iovcnt == 0) return APIError::OK; - int err; APIError aerr; size_t total_write_len = 0; for (int i = 0; i < iovcnt; i++) { #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); + 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; } @@ -572,7 +582,7 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", errno); return APIError::SOCKET_WRITE_FAILED; - } else if (sent != total_write_len) { + } 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++) { @@ -712,7 +722,12 @@ APIError APINoiseFrameHelper::shutdown(int how) { } 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); } +void noise_rand_bytes(void *output, size_t len) { + if (!esphome::random_bytes(reinterpret_cast(output), len)) { + ESP_LOGE(TAG, "Failed to acquire random bytes, rebooting!"); + arch_restart(); + } +} } #endif // USE_API_NOISE @@ -766,9 +781,6 @@ APIError APIPlaintextFrameHelper::loop() { * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. */ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { - int err; - APIError aerr; - if (frame == nullptr) { HELPER_LOG("Bad argument for try_read_frame_"); return APIError::BAD_ARG; @@ -778,12 +790,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { while (!rx_header_parsed_) { uint8_t data; ssize_t received = socket_->read(&data, 1); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + 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); @@ -824,15 +841,20 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read size_t to_read = rx_header_parsed_len_ - rx_buf_len_; ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + 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 (received != to_read) { + if ((size_t) received != to_read) { // not all read return APIError::WOULD_BLOCK; } @@ -840,7 +862,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // uncomment for even more debugging #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); #endif frame->msg = std::move(rx_buf_); // consume msg @@ -852,7 +874,6 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { } APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - int err; APIError aerr; if (state_ != State::DATA) { @@ -872,9 +893,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { } bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { - int err; - APIError aerr; - if (state_ != State::DATA) { return APIError::BAD_STATE; } @@ -918,13 +936,13 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() { APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { if (iovcnt == 0) return APIError::OK; - int err; APIError aerr; size_t total_write_len = 0; for (int i = 0; i < iovcnt; i++) { #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); + 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; } @@ -958,7 +976,7 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", errno); return APIError::SOCKET_WRITE_FAILED; - } else if (sent != total_write_len) { + } 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++) { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 7fdb26fd40..57e3c961d5 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -53,6 +53,7 @@ enum class APIError : int { HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SPLIT_FAILED = 1020, BAD_HANDSHAKE_ERROR_BYTE = 1021, + CONNECTION_CLOSED = 1022, }; const char *api_error_to_str(APIError err); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6a87238186..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: @@ -254,6 +266,18 @@ template<> const char *proto_enum_to_string(enums::Climate 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: { @@ -267,7 +291,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); } #ifdef HAS_PROTO_MESSAGE_DUMP void HelloRequest::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("HelloRequest {\n"); out.append(" client_info: "); out.append("'").append(this->client_info).append("'"); @@ -306,7 +330,7 @@ void HelloResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -337,7 +361,7 @@ bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); } #ifdef HAS_PROTO_MESSAGE_DUMP void ConnectRequest::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ConnectRequest {\n"); out.append(" password: "); out.append("'").append(this->password).append("'"); @@ -358,7 +382,7 @@ bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } #ifdef HAS_PROTO_MESSAGE_DUMP void ConnectResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ConnectResponse {\n"); out.append(" invalid_password: "); out.append(YESNO(this->invalid_password)); @@ -396,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; } @@ -444,10 +472,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { 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)); @@ -484,6 +513,11 @@ void DeviceInfoResponse::dump_to(std::string &out) const { 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 @@ -509,6 +543,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar this->disabled_by_default = value.as_bool(); return true; } + case 9: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -558,10 +596,11 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -595,6 +634,10 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { 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 @@ -629,7 +672,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -664,6 +707,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->disabled_by_default = value.as_bool(); return true; } + case 11: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -715,10 +762,11 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -760,6 +808,10 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { 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 @@ -804,7 +856,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -887,7 +939,7 @@ void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { } #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); @@ -948,6 +1000,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->disabled_by_default = value.as_bool(); return true; } + case 11: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -995,10 +1051,11 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -1041,6 +1098,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { 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 @@ -1090,7 +1151,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -1191,7 +1252,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { } #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); @@ -1267,6 +1328,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->disabled_by_default = value.as_bool(); return true; } + case 15: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -1334,10 +1399,11 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } 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("'"); @@ -1401,6 +1467,10 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { 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 @@ -1491,7 +1561,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -1714,7 +1784,7 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { } #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); @@ -1860,6 +1930,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 13: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -1917,10 +1991,11 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -1971,6 +2046,10 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { 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 @@ -2005,7 +2084,7 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -2033,6 +2112,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 8: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2077,10 +2160,11 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -2110,6 +2194,10 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { 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 @@ -2139,7 +2227,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -2178,7 +2266,7 @@ void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const { } #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); @@ -2197,6 +2285,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn this->disabled_by_default = value.as_bool(); return true; } + case 7: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2240,10 +2332,11 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -2269,6 +2362,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { 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 @@ -2309,7 +2406,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -2346,7 +2443,7 @@ void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { } #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)); @@ -2389,7 +2486,7 @@ void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { } #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)); @@ -2431,7 +2528,7 @@ void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { } #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("'"); @@ -2490,7 +2587,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { } #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("'"); @@ -2546,7 +2643,7 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const } #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("'"); @@ -2583,7 +2680,7 @@ void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { } #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("'"); @@ -2616,7 +2713,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } #ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("GetTimeResponse {\n"); out.append(" epoch_seconds: "); sprintf(buffer, "%u", this->epoch_seconds); @@ -2651,7 +2748,7 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { } #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("'"); @@ -2696,7 +2793,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { } #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("'"); @@ -2790,7 +2887,7 @@ void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { } #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_)); @@ -2871,7 +2968,7 @@ void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const { } #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); @@ -2892,6 +2989,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 7: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2910,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; } @@ -2930,10 +3035,12 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { 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("'"); @@ -2955,6 +3062,14 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { 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 @@ -2995,7 +3110,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -3032,7 +3147,7 @@ void CameraImageRequest::encode(ProtoWriteBuffer buffer) const { } #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)); @@ -3082,6 +3197,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->disabled_by_default = value.as_bool(); return true; } + case 20: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -3170,10 +3289,11 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } 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("'"); @@ -3266,6 +3386,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { 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 @@ -3356,7 +3480,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { } #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); @@ -3544,7 +3668,7 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { } #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); @@ -3642,6 +3766,14 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va 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; } @@ -3664,6 +3796,10 @@ bool ListEntitiesNumberResponse::decode_length(uint32_t field_id, ProtoLengthDel this->icon = value.as_string(); return true; } + case 11: { + this->unit_of_measurement = value.as_string(); + return true; + } default: return false; } @@ -3700,10 +3836,13 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { 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 { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesNumberResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -3744,6 +3883,18 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { 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 @@ -3778,7 +3929,7 @@ void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { } #ifdef HAS_PROTO_MESSAGE_DUMP void NumberStateResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("NumberStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -3816,7 +3967,7 @@ void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const { } #ifdef HAS_PROTO_MESSAGE_DUMP void NumberCommandRequest::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("NumberCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -3836,6 +3987,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 8: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -3886,10 +4041,11 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { 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 { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSelectResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -3921,6 +4077,10 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { 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 @@ -3961,7 +4121,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { } #ifdef HAS_PROTO_MESSAGE_DUMP void SelectStateResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("SelectStateResponse {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -4004,7 +4164,7 @@ void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const { } #ifdef HAS_PROTO_MESSAGE_DUMP void SelectCommandRequest::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("SelectCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); @@ -4017,6 +4177,127 @@ void SelectCommandRequest::dump_to(std::string &out) const { 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 13a21c4772..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, @@ -118,6 +123,11 @@ enum ClimatePreset : uint32_t { 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 @@ -224,6 +234,7 @@ class DeviceInfoResponse : public ProtoMessage { 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; @@ -270,6 +281,7 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { 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; @@ -306,6 +318,7 @@ class ListEntitiesCoverResponse : public ProtoMessage { 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; @@ -363,6 +376,7 @@ class ListEntitiesFanResponse : public ProtoMessage { 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; @@ -428,6 +442,7 @@ class ListEntitiesLightResponse : public ProtoMessage { 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; @@ -516,6 +531,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { 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; @@ -549,6 +565,7 @@ class ListEntitiesSwitchResponse : public ProtoMessage { 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; @@ -593,6 +610,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { 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; @@ -803,6 +821,8 @@ class ListEntitiesCameraResponse : public ProtoMessage { 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; @@ -861,6 +881,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { 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; @@ -940,6 +961,9 @@ class ListEntitiesNumberResponse : public ProtoMessage { 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; @@ -985,6 +1009,7 @@ class ListEntitiesSelectResponse : public ProtoMessage { 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; @@ -1023,6 +1048,37 @@ class SelectCommandRequest : public ProtoMessage { 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 ad2413ea57..567fbf02c9 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -282,6 +282,16 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon #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: { @@ -513,6 +523,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, 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; } @@ -737,6 +758,19 @@ void APIServerConnection::on_select_command_request(const SelectCommandRequest & 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 1b8d990b05..50b08d3ec4 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -129,6 +129,12 @@ class APIServerConnectionBase : public ProtoService { #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; @@ -171,6 +177,9 @@ class APIServerConnection : public APIServerConnectionBase { #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; @@ -209,6 +218,9 @@ class APIServerConnection : public APIServerConnectionBase { #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 4e2899d94f..25081a809a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -77,7 +77,7 @@ void APIServer::setup() { this->last_connected_ = millis(); #ifdef USE_ESP32_CAMERA - if (esp32_camera::global_esp32_camera != nullptr) { + 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_) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 4a3944d33e..b2920f239b 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -23,7 +23,6 @@ async def async_run_logs(config, address): _LOGGER.info("Starting log output from %s using esphome API", address) zc = zeroconf.Zeroconf() cli = APIClient( - asyncio.get_event_loop(), address, port, password, diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 8a72765195..90cfe751b6 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)); } @@ -58,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 745dd92c89..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); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index c728fb0a97..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 diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index beb9b947d4..d3f2d3aa45 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -31,6 +31,9 @@ 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 diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index 5085994607..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()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index e404a95619..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 @@ -78,6 +81,9 @@ class ComponentIterator { #ifdef USE_SWITCH SWITCH, #endif +#ifdef USE_BUTTON + BUTTON, +#endif #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index 42c30598ad..9d550fcf8c 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -45,6 +45,8 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device this->battery_voltage_->publish_state(*res->battery_voltage); success = true; } + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(device.get_rssi()); return success; } diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index ca079bf8c1..9398c02bcf 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -28,6 +28,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice 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; } + void set_signal_strength(sensor::Sensor *signal_strength) { signal_strength_ = signal_strength; } protected: uint64_t address_; @@ -35,6 +36,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *humidity_{nullptr}; sensor::Sensor *battery_level_{nullptr}; sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index 0f6cc1abcb..7baab51944 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -6,14 +6,18 @@ from esphome.const import ( CONF_BATTERY_VOLTAGE, CONF_MAC_ADDRESS, CONF_HUMIDITY, + CONF_SIGNAL_STRENGTH, CONF_TEMPERATURE, CONF_ID, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, UNIT_PERCENT, UNIT_VOLT, ) @@ -49,12 +53,21 @@ CONFIG_SCHEMA = ( 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, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) @@ -82,3 +95,6 @@ async def to_code(config): if CONF_BATTERY_VOLTAGE in config: sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) cg.add(var.set_battery_voltage(sens)) + if CONF_SIGNAL_STRENGTH in config: + sens = await sensor.new_sensor(config[CONF_SIGNAL_STRENGTH]) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 05e5250d89..9c876bb62c 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -17,6 +17,7 @@ from esphome.const import ( DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, ICON_LIGHTBULB, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -125,6 +126,7 @@ CONFIG_SCHEMA = ( 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( diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index d51c48c602..201685adc4 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_LUX, @@ -51,6 +52,7 @@ CONFIG_SCHEMA = ( 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, diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 5645f46f1c..4a95f8c339 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -80,21 +80,23 @@ void BangBangClimate::compute_state_() { climate::ClimateAction target_action; if (too_cold) { - // too cold -> enable heating if possible, else idle - if (this->supports_heat_) + // too cold -> enable heating if possible and enabled, else idle + if (this->supports_heat_ && + (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_HEAT)) target_action = climate::CLIMATE_ACTION_HEATING; else target_action = climate::CLIMATE_ACTION_IDLE; } else if (too_hot) { - // too hot -> enable cooling if possible, else idle - if (this->supports_cool_) + // too hot -> enable cooling if possible and enabled, else idle + if (this->supports_cool_ && + (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_COOL)) target_action = climate::CLIMATE_ACTION_COOLING; else target_action = climate::CLIMATE_ACTION_IDLE; } else { // neither too hot nor too cold -> in range - if (this->supports_cool_ && this->supports_heat_) { - // if supports both ends, go to idle action + if (this->supports_cool_ && this->supports_heat_ && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { + // if supports both ends and both cooling and heating enabled, go to idle action target_action = climate::CLIMATE_ACTION_IDLE; } else { // else use current mode and don't change (hysteresis) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 951fe3670c..4e6bb3c563 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -79,6 +79,9 @@ void BH1750Sensor::read_data_() { 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(); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index ec199cc5fa..1eab76d54e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -44,9 +44,11 @@ from esphome.const import ( 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, @@ -76,9 +78,11 @@ DEVICE_CLASSES = [ 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, @@ -313,6 +317,7 @@ def validate_multi_click_timing(value): device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(BinarySensor), diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 41da83aa3e..71422609d7 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -48,7 +48,10 @@ void BinarySensor::set_device_class(const std::string &device_class) { this->dev std::string BinarySensor::get_device_class() { if (this->device_class_.has_value()) return *this->device_class_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->device_class(); +#pragma GCC diagnostic pop } void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 9c0d43fa98..591f444387 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -74,7 +74,10 @@ class BinarySensor : public EntityBase { // ========== OVERRIDE METHODS ========== // (You'll only need this when creating your own custom binary sensor) - /// Get the default device class for this sensor, or empty string for no default. + /** Override this to set the default device class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string device_class(); protected: diff --git a/esphome/components/bl0940/__init__.py b/esphome/components/bl0940/__init__.py new file mode 100644 index 0000000000..087626a4e7 --- /dev/null +++ b/esphome/components/bl0940/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@tobias-"] diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp new file mode 100644 index 0000000000..19672e98d0 --- /dev/null +++ b/esphome/components/bl0940/bl0940.cpp @@ -0,0 +1,137 @@ +#include "bl0940.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940"; + +static const uint8_t BL0940_READ_COMMAND = 0x50; // 0x58 according to documentation +static const uint8_t BL0940_FULL_PACKET = 0xAA; +static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to documentation + +static const uint8_t BL0940_WRITE_COMMAND = 0xA0; // 0xA8 according to documentation +static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; +static const uint8_t BL0940_REG_MODE = 0x18; +static const uint8_t BL0940_REG_SOFT_RESET = 0x19; +static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; +static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; + +const uint8_t BL0940_INIT[5][6] = { + // Reset to default + {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, + // Enable User Operation Write + {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, + // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS + {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, + // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS + {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; + +void BL0940::loop() { + DataPacket buffer; + if (!this->available()) { + return; + } + if (read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (validate_checksum(&buffer)) { + received_package_(&buffer); + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + } +} + +bool BL0940::validate_checksum(const DataPacket *data) { + uint8_t checksum = BL0940_READ_COMMAND; + // Whole package but checksum + for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { + checksum += data->raw[i]; + } + checksum ^= 0xFF; + if (checksum != data->checksum) { + ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + } + return checksum == data->checksum; +} + +void BL0940::update() { + this->flush(); + this->write_byte(BL0940_READ_COMMAND); + this->write_byte(BL0940_FULL_PACKET); +} + +void BL0940::setup() { + for (auto i : BL0940_INIT) { + this->write_array(i, 6); + delay(1); + } + this->flush(); +} + +float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { + auto tb = (float) (temperature.h << 8 | temperature.l); + float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; + if (sensor != nullptr) { + if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { + ESP_LOGD("bl0940", "Invalid temperature change. Sensor: '%s', Old temperature: %f, New temperature: %f", + sensor->get_name().c_str(), sensor->get_state(), converted_temp); + return 0.0f; + } + sensor->publish_state(converted_temp); + } + return converted_temp; +} + +void BL0940::received_package_(const DataPacket *data) const { + // Bad header + if (data->frame_header != BL0940_PACKET_HEADER) { + ESP_LOGI("bl0940", "Invalid data. Header mismatch: %d", data->frame_header); + return; + } + + float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; + float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; + float watt = (float) to_int32_t(data->watt) / power_reference_; + uint32_t cf_cnt = to_uint32_t(data->cf_cnt); + float total_energy_consumption = (float) cf_cnt / energy_reference_; + + float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); + float tps2 = update_temp_(external_temperature_sensor_, data->tps2); + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(v_rms); + } + if (current_sensor_ != nullptr) { + current_sensor_->publish_state(i_rms); + } + if (power_sensor_ != nullptr) { + power_sensor_->publish_state(watt); + } + if (energy_sensor_ != nullptr) { + energy_sensor_->publish_state(total_energy_consumption); + } + + ESP_LOGV("bl0940", "BL0940: U %fV, I %fA, P %fW, Cnt %d, ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, + total_energy_consumption, tps1, tps2); +} + +void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexity) + ESP_LOGCONFIG(TAG, "BL0940:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Energy", this->energy_sensor_); + LOG_SENSOR("", "Internal temperature", this->internal_temperature_sensor_); + LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); +} + +uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h new file mode 100644 index 0000000000..49c8e50595 --- /dev/null +++ b/esphome/components/bl0940/bl0940.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bl0940 { + +static const float BL0940_PREF = 1430; +static const float BL0940_UREF = 33000; +static const float BL0940_IREF = 275000; // 2750 from tasmota. Seems to generate values 100 times too high + +// Measured to 297J per click according to power consumption of 5 minutes +// Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 +static const float BL0940_EREF = 3.6e6 / 297; + +struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + uint8_t h; +} __attribute__((packed)); + +struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t h; +} __attribute__((packed)); + +struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + int8_t h; +} __attribute__((packed)); + +// Caveat: All these values are big endian (low - middle - high) + +union DataPacket { // NOLINT(altera-struct-pack-align) + uint8_t raw[35]; + struct { + uint8_t frame_header; // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. + ube24_t i_fast_rms; // 0x00 + ube24_t i_rms; // 0x04 + ube24_t RESERVED0; // reserved + ube24_t v_rms; // 0x06 + ube24_t RESERVED1; // reserved + sbe24_t watt; // 0x08 + ube24_t RESERVED2; // reserved + ube24_t cf_cnt; // 0x0A + ube24_t RESERVED3; // reserved + ube16_t tps1; // 0x0c + uint8_t RESERVED4; // value of 0x00 + ube16_t tps2; // 0x0c + uint8_t RESERVED5; // value of 0x00 + uint8_t checksum; // checksum + }; +} __attribute__((packed)); + +class BL0940 : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { + internal_temperature_sensor_ = internal_temperature_sensor; + } + void set_external_temperature_sensor(sensor::Sensor *external_temperature_sensor) { + external_temperature_sensor_ = external_temperature_sensor; + } + + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + // NB This may be negative as the circuits is seemingly able to measure + // power in both directions + sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; + sensor::Sensor *internal_temperature_sensor_; + sensor::Sensor *external_temperature_sensor_; + + // Max difference between two measurements of the temperature. Used to avoid noise. + float max_temperature_diff_{0}; + // Divide by this to turn into Watt + float power_reference_ = BL0940_PREF; + // Divide by this to turn into Volt + float voltage_reference_ = BL0940_UREF; + // Divide by this to turn into Ampere + float current_reference_ = BL0940_IREF; + // Divide by this to turn into kWh + float energy_reference_ = BL0940_EREF; + + float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; + + static uint32_t to_uint32_t(ube24_t input); + + static int32_t to_int32_t(sbe24_t input); + + static bool validate_checksum(const DataPacket *data); + + void received_package_(const DataPacket *data) const; +}; +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py new file mode 100644 index 0000000000..ce630b7408 --- /dev/null +++ b/esphome/components/bl0940/sensor.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +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, + DEVICE_CLASS_TEMPERATURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, +) + +DEPENDENCIES = ["uart"] + +CONF_INTERNAL_TEMPERATURE = "internal_temperature" +CONF_EXTERNAL_TEMPERATURE = "external_temperature" + +bl0940_ns = cg.esphome_ns.namespace("bl0940") +BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0940), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + UNIT_AMPERE, + ICON_EMPTY, + 2, + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 0, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_EMPTY, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_EMPTY, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_NONE, + ), + } + ) + .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_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)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) + if CONF_INTERNAL_TEMPERATURE in config: + conf = config[CONF_INTERNAL_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_internal_temperature_sensor(sens)) + if CONF_EXTERNAL_TEMPERATURE in config: + conf = config[CONF_EXTERNAL_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_external_temperature_sensor(sens)) diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 8ff516d735..407f1a1d17 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -11,6 +11,8 @@ 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) { @@ -386,6 +388,15 @@ 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 diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 4a17ccb79b..5680b69f72 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -59,7 +59,7 @@ class BLECharacteristic { 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; }; @@ -81,6 +81,7 @@ class BLEClient : public espbt::ESPBTClient, public Component { 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; 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 index efe4bf0e9a..4aa6a92ba5 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -67,7 +67,7 @@ async def to_code(config): 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 len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): @@ -87,7 +87,9 @@ async def to_code(config): elif len(config[CONF_CHARACTERISTIC_UUID]) == len( esp32_ble_tracker.bt_uuid128_format ): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) + 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: @@ -108,7 +110,9 @@ async def to_code(config): elif len(config[CONF_DESCRIPTOR_UUID]) == len( esp32_ble_tracker.bt_uuid128_format ): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) cg.add(var.set_descr_uuid128(uuid128)) if CONF_LAMBDA in config: diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index 18386430a2..d3a228328b 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -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; @@ -178,23 +179,29 @@ void BME280Component::update() { 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); + 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/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index d258819aa4..83e519f8aa 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -44,7 +44,8 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_STATE_SAVE_INTERVAL, default="6hours" ): cv.positive_time_period_minutes, - } + }, + cv.only_with_arduino, ).extend(i2c.i2c_device_schema(0x76)) @@ -60,5 +61,8 @@ async def to_code(config): 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("BSEC Software Library", "1.6.1480") + cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bmp3xx/__init__.py b/esphome/components/bmp3xx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp3xx/bmp3xx.cpp b/esphome/components/bmp3xx/bmp3xx.cpp new file mode 100644 index 0000000000..410b7a3173 --- /dev/null +++ b/esphome/components/bmp3xx/bmp3xx.cpp @@ -0,0 +1,388 @@ +/* + based on BMP388_DEV by Martin Lindupp + under MIT License (MIT) + Copyright (C) Martin Lindupp 2020 + http://github.com/MartinL1/BMP388_DEV +*/ + +#include "bmp3xx.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace bmp3xx { + +static const char *const TAG = "bmp3xx.sensor"; + +static const LogString *chip_type_to_str(uint8_t chip_type) { + switch (chip_type) { + case BMP388_ID: + return LOG_STR("BMP 388"); + case BMP390_ID: + return LOG_STR("BMP 390"); + default: + return LOG_STR("Unknown Chip Type"); + } +} + +static const LogString *oversampling_to_str(Oversampling oversampling) { + switch (oversampling) { + case Oversampling::OVERSAMPLING_NONE: + return LOG_STR("None"); + case Oversampling::OVERSAMPLING_X2: + return LOG_STR("2x"); + case Oversampling::OVERSAMPLING_X4: + return LOG_STR("4x"); + case Oversampling::OVERSAMPLING_X8: + return LOG_STR("8x"); + case Oversampling::OVERSAMPLING_X16: + return LOG_STR("16x"); + case Oversampling::OVERSAMPLING_X32: + return LOG_STR("32x"); + default: + return LOG_STR(""); + } +} + +static const LogString *iir_filter_to_str(IIRFilter filter) { + switch (filter) { + case IIRFilter::IIR_FILTER_OFF: + return LOG_STR("OFF"); + case IIRFilter::IIR_FILTER_2: + return LOG_STR("2x"); + case IIRFilter::IIR_FILTER_4: + return LOG_STR("4x"); + case IIRFilter::IIR_FILTER_8: + return LOG_STR("8x"); + case IIRFilter::IIR_FILTER_16: + return LOG_STR("16x"); + case IIRFilter::IIR_FILTER_32: + return LOG_STR("32x"); + case IIRFilter::IIR_FILTER_64: + return LOG_STR("64x"); + case IIRFilter::IIR_FILTER_128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +void BMP3XXComponent::setup() { + this->error_code_ = NONE; + ESP_LOGCONFIG(TAG, "Setting up BMP3XX..."); + // Call the Device base class "initialise" function + if (!reset()) { + ESP_LOGE(TAG, "Failed to reset BMP3XX..."); + this->error_code_ = ERROR_SENSOR_RESET; + this->mark_failed(); + } + + if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) { + ESP_LOGE(TAG, "Can't read chip id"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); + + if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) { + ESP_LOGE(TAG, "Unknown chip id - is this really a BMP388 or BMP390?"); + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + return; + } + // set sensor in sleep mode + stop_conversion(); + // Read the calibration parameters into the params structure + if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) { + ESP_LOGE(TAG, "Can't read calibration data"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + compensation_float_params_.param_T1 = + (float) compensation_params_.param_T1 / powf(2.0f, -8.0f); // Calculate the floating point trim parameters + compensation_float_params_.param_T2 = (float) compensation_params_.param_T2 / powf(2.0f, 30.0f); + compensation_float_params_.param_T3 = (float) compensation_params_.param_T3 / powf(2.0f, 48.0f); + compensation_float_params_.param_P1 = ((float) compensation_params_.param_P1 - powf(2.0f, 14.0f)) / powf(2.0f, 20.0f); + compensation_float_params_.param_P2 = ((float) compensation_params_.param_P2 - powf(2.0f, 14.0f)) / powf(2.0f, 29.0f); + compensation_float_params_.param_P3 = (float) compensation_params_.param_P3 / powf(2.0f, 32.0f); + compensation_float_params_.param_P4 = (float) compensation_params_.param_P4 / powf(2.0f, 37.0f); + compensation_float_params_.param_P5 = (float) compensation_params_.param_P5 / powf(2.0f, -3.0f); + compensation_float_params_.param_P6 = (float) compensation_params_.param_P6 / powf(2.0f, 6.0f); + compensation_float_params_.param_P7 = (float) compensation_params_.param_P7 / powf(2.0f, 8.0f); + compensation_float_params_.param_P8 = (float) compensation_params_.param_P8 / powf(2.0f, 15.0f); + compensation_float_params_.param_P9 = (float) compensation_params_.param_P9 / powf(2.0f, 48.0f); + compensation_float_params_.param_P10 = (float) compensation_params_.param_P10 / powf(2.0f, 48.0f); + compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f); + + // Initialise the BMP388 IIR filter register + if (!set_iir_filter(this->iir_filter_)) { + ESP_LOGE(TAG, "Failed to set IIR filter"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + // Set power control registers + pwr_ctrl_.bit.press_en = 1; + pwr_ctrl_.bit.temp_en = 1; + // Disable pressure if no sensor defined + // keep temperature enabled since it's needed for compensation + if (this->pressure_sensor_ == nullptr) { + pwr_ctrl_.bit.press_en = 0; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + } + // just disable oeversampling for temp if not used + if (this->temperature_sensor_ == nullptr) { + this->temperature_oversampling_ = OVERSAMPLING_NONE; + } + // Initialise the BMP388 oversampling register + if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) { + ESP_LOGE(TAG, "Failed to set oversampling register"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } +} + +void BMP3XXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BMP3XX:"); + ESP_LOGCONFIG(TAG, " Type: %s (0x%X)", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with BMP3XX failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE( + TAG, + "BMP3XX has wrong chip ID (reported id: 0x%X) - please check if you are really using a BMP 388 or BMP 390", + this->chip_id_.reg); + break; + case ERROR_SENSOR_RESET: + ESP_LOGE(TAG, "BMP3XX failed to reset"); + break; + default: + ESP_LOGE(TAG, "BMP3XX error code %d", (int) this->error_code_); + break; + } + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_))); + LOG_UPDATE_INTERVAL(this); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); + } +} +float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } + +inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } + +void BMP3XXComponent::update() { + // Enable sensor + ESP_LOGV(TAG, "Sending conversion request..."); + float meas_time = 1.0f; + // Ref: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp390-ds002.pdf 3.9.2 + meas_time += 2.02f * oversampling_to_time(this->temperature_oversampling_) + 0.163f; + meas_time += 2.02f * oversampling_to_time(this->pressure_oversampling_) + 0.392f; + meas_time += 0.234f; + if (!set_mode(FORCED_MODE)) { + ESP_LOGE(TAG, "Failed start forced mode"); + this->mark_failed(); + return; + } + + ESP_LOGVV(TAG, "measurement time %d", uint32_t(ceilf(meas_time))); + this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + float temperature = 0.0f; + float pressure = 0.0f; + if (this->pressure_sensor_ != nullptr) { + if (!get_measurements(temperature, pressure)) { + ESP_LOGW(TAG, "Failed to read pressure and temperature - skipping update"); + this->status_set_warning(); + return; + } + ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); + } else { + if (!get_temperature(temperature)) { + ESP_LOGW(TAG, "Failed to read temperature - skipping update"); + this->status_set_warning(); + return; + } + ESP_LOGD(TAG, "Got temperature=%.1f°C", temperature); + } + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + set_mode(SLEEP_MODE); + }); +} + +// Reset the BMP3XX +uint8_t BMP3XXComponent::reset() { + write_byte(BMP388_CMD, RESET_CODE); // Write the reset code to the command register + // Wait for 10ms + delay(10); + this->read_byte(BMP388_EVENT, &event_.reg); // Read the BMP388's event register + return event_.bit.por_detected; // Return if device reset is complete +} + +// Start a one shot measurement in FORCED_MODE +bool BMP3XXComponent::start_forced_conversion() { + // Only set FORCED_MODE if we're already in SLEEP_MODE + if (pwr_ctrl_.bit.mode == SLEEP_MODE) { + return set_mode(FORCED_MODE); + } + return true; +} + +// Stop the conversion and return to SLEEP_MODE +bool BMP3XXComponent::stop_conversion() { return set_mode(SLEEP_MODE); } + +// Set the pressure oversampling rate +bool BMP3XXComponent::set_pressure_oversampling(Oversampling oversampling) { + osr_.bit.osr_p = oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Set the temperature oversampling rate +bool BMP3XXComponent::set_temperature_oversampling(Oversampling oversampling) { + osr_.bit.osr_t = oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Set the IIR filter setting +bool BMP3XXComponent::set_iir_filter(IIRFilter iir_filter) { + config_.bit.iir_filter = iir_filter; + return this->write_byte(BMP388_CONFIG, config_.reg); +} + +// Get temperature +bool BMP3XXComponent::get_temperature(float &temperature) { + // Check if a measurement is ready + if (!data_ready()) { + return false; + } + uint8_t data[3]; + // Read the temperature + if (!this->read_bytes(BMP388_DATA_3, &data[0], 3)) { + ESP_LOGE(TAG, "Failed to read temperature"); + return false; + } + // Copy the temperature data into the adc variables + int32_t adc_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + // Temperature compensation (function from BMP388 datasheet) + temperature = bmp388_compensate_temperature_((float) adc_temp); + return true; +} + +// Get the pressure +bool BMP3XXComponent::get_pressure(float &pressure) { + float temperature; + return get_measurements(temperature, pressure); +} + +// Get temperature and pressure +bool BMP3XXComponent::get_measurements(float &temperature, float &pressure) { + // Check if a measurement is ready + if (!data_ready()) { + ESP_LOGD(TAG, "BMP3XX Get measurement - data not ready skipping update"); + return false; + } + + uint8_t data[6]; + // Read the temperature and pressure data + if (!this->read_bytes(BMP388_DATA_0, &data[0], 6)) { + ESP_LOGE(TAG, "Failed to read measurements"); + return false; + } + // Copy the temperature and pressure data into the adc variables + int32_t adc_pres = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t adc_temp = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3]; + + // Temperature compensation (function from BMP388 datasheet) + temperature = bmp388_compensate_temperature_((float) adc_temp); + // Pressure compensation (function from BMP388 datasheet) + pressure = bmp388_compensate_pressure_((float) adc_pres, temperature); + // Calculate the pressure in millibar/hPa + pressure /= 100.0f; + return true; +} + +// Set the BMP388's mode in the power control register +bool BMP3XXComponent::set_mode(OperationMode mode) { + pwr_ctrl_.bit.mode = mode; + return this->write_byte(BMP388_PWR_CTRL, pwr_ctrl_.reg); +} + +// Set the BMP388 oversampling register +bool BMP3XXComponent::set_oversampling_register(Oversampling pressure_oversampling, + Oversampling temperature_oversampling) { + osr_.reg = temperature_oversampling << 3 | pressure_oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Check if measurement data is ready +bool BMP3XXComponent::data_ready() { + // If we're in SLEEP_MODE return immediately + if (pwr_ctrl_.bit.mode == SLEEP_MODE) { + ESP_LOGD(TAG, "Not ready - sensor is in sleep mode"); + return false; + } + // Read the interrupt status register + uint8_t status; + if (!this->read_byte(BMP388_INT_STATUS, &status)) { + ESP_LOGE(TAG, "Failed to read status register"); + return false; + } + int_status_.reg = status; + ESP_LOGVV(TAG, "data ready status %d", status); + // If we're in FORCED_MODE switch back to SLEEP_MODE + if (int_status_.bit.drdy) { + if (pwr_ctrl_.bit.mode == FORCED_MODE) { + pwr_ctrl_.bit.mode = SLEEP_MODE; + } + return true; // The measurement is ready + } + return false; // The measurement is still pending +} + +//////////////////////////////////////////////////////////////////////////////// +// Bosch BMP3XXComponent (Private) Member Functions +//////////////////////////////////////////////////////////////////////////////// + +float BMP3XXComponent::bmp388_compensate_temperature_(float uncomp_temp) { + float partial_data1 = uncomp_temp - compensation_float_params_.param_T1; + float partial_data2 = partial_data1 * compensation_float_params_.param_T2; + return partial_data2 + partial_data1 * partial_data1 * compensation_float_params_.param_T3; +} + +float BMP3XXComponent::bmp388_compensate_pressure_(float uncomp_press, float t_lin) { + float partial_data1 = compensation_float_params_.param_P6 * t_lin; + float partial_data2 = compensation_float_params_.param_P7 * t_lin * t_lin; + float partial_data3 = compensation_float_params_.param_P8 * t_lin * t_lin * t_lin; + float partial_out1 = compensation_float_params_.param_P5 + partial_data1 + partial_data2 + partial_data3; + partial_data1 = compensation_float_params_.param_P2 * t_lin; + partial_data2 = compensation_float_params_.param_P3 * t_lin * t_lin; + partial_data3 = compensation_float_params_.param_P4 * t_lin * t_lin * t_lin; + float partial_out2 = + uncomp_press * (compensation_float_params_.param_P1 + partial_data1 + partial_data2 + partial_data3); + partial_data1 = uncomp_press * uncomp_press; + partial_data2 = compensation_float_params_.param_P9 + compensation_float_params_.param_P10 * t_lin; + partial_data3 = partial_data1 * partial_data2; + float partial_data4 = + partial_data3 + uncomp_press * uncomp_press * uncomp_press * compensation_float_params_.param_P11; + return partial_out1 + partial_out2 + partial_data4; +} + +} // namespace bmp3xx +} // namespace esphome diff --git a/esphome/components/bmp3xx/bmp3xx.h b/esphome/components/bmp3xx/bmp3xx.h new file mode 100644 index 0000000000..ab20abfe9b --- /dev/null +++ b/esphome/components/bmp3xx/bmp3xx.h @@ -0,0 +1,237 @@ +/* + based on BMP388_DEV by Martin Lindupp + under MIT License (MIT) + Copyright (C) Martin Lindupp 2020 + http://github.com/MartinL1/BMP388_DEV +*/ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bmp3xx { + +static const uint8_t BMP388_ID = 0x50; // The BMP388 device ID +static const uint8_t BMP390_ID = 0x60; // The BMP390 device ID +static const uint8_t RESET_CODE = 0xB6; // The BMP388 reset code + +/// BMP388_DEV Registers +enum { + BMP388_CHIP_ID = 0x00, // Chip ID register sub-address + BMP388_ERR_REG = 0x02, // Error register sub-address + BMP388_STATUS = 0x03, // Status register sub-address + BMP388_DATA_0 = 0x04, // Pressure eXtended Least Significant Byte (XLSB) register sub-address + BMP388_DATA_1 = 0x05, // Pressure Least Significant Byte (LSB) register sub-address + BMP388_DATA_2 = 0x06, // Pressure Most Significant Byte (MSB) register sub-address + BMP388_DATA_3 = 0x07, // Temperature eXtended Least Significant Byte (XLSB) register sub-address + BMP388_DATA_4 = 0x08, // Temperature Least Significant Byte (LSB) register sub-address + BMP388_DATA_5 = 0x09, // Temperature Most Significant Byte (MSB) register sub-address + BMP388_SENSORTIME_0 = 0x0C, // Sensor time register 0 sub-address + BMP388_SENSORTIME_1 = 0x0D, // Sensor time register 1 sub-address + BMP388_SENSORTIME_2 = 0x0E, // Sensor time register 2 sub-address + BMP388_EVENT = 0x10, // Event register sub-address + BMP388_INT_STATUS = 0x11, // Interrupt Status register sub-address + BMP388_INT_CTRL = 0x19, // Interrupt Control register sub-address + BMP388_IF_CONFIG = 0x1A, // Interface Configuration register sub-address + BMP388_PWR_CTRL = 0x1B, // Power Control register sub-address + BMP388_OSR = 0x1C, // Oversampling register sub-address + BMP388_ODR = 0x1D, // Output Data Rate register sub-address + BMP388_CONFIG = 0x1F, // Configuration register sub-address + BMP388_TRIM_PARAMS = 0x31, // Trim parameter registers' base sub-address + BMP388_CMD = 0x7E // Command register sub-address +}; + +/// Device mode bitfield in the control and measurement register +enum OperationMode { SLEEP_MODE = 0x00, FORCED_MODE = 0x01, NORMAL_MODE = 0x03 }; + +/// Oversampling bit fields in the control and measurement register +enum Oversampling { + OVERSAMPLING_NONE = 0x00, + OVERSAMPLING_X2 = 0x01, + OVERSAMPLING_X4 = 0x02, + OVERSAMPLING_X8 = 0x03, + OVERSAMPLING_X16 = 0x04, + OVERSAMPLING_X32 = 0x05 +}; + +/// Infinite Impulse Response (IIR) filter bit field in the configuration register +enum IIRFilter { + IIR_FILTER_OFF = 0x00, + IIR_FILTER_2 = 0x01, + IIR_FILTER_4 = 0x02, + IIR_FILTER_8 = 0x03, + IIR_FILTER_16 = 0x04, + IIR_FILTER_32 = 0x05, + IIR_FILTER_64 = 0x06, + IIR_FILTER_128 = 0x07 +}; + +/// This class implements support for the BMP3XX Temperature+Pressure i2c sensor. +class BMP3XXComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + /// Set the oversampling value for the temperature sensor. Default is 16x. + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + } + /// Set the oversampling value for the pressure sensor. Default is 16x. + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + } + /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. + void set_iir_filter_config(IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } + + /// Soft reset the sensor + uint8_t reset(); + /// Start continuous measurement in NORMAL_MODE + bool start_normal_conversion(); + /// Start a one shot measurement in FORCED_MODE + bool start_forced_conversion(); + /// Stop the conversion and return to SLEEP_MODE + bool stop_conversion(); + /// Set the pressure oversampling: OFF, X1, X2, X4, X8, X16, X32 + bool set_pressure_oversampling(Oversampling pressure_oversampling); + /// Set the temperature oversampling: OFF, X1, X2, X4, X8, X16, X32 + bool set_temperature_oversampling(Oversampling temperature_oversampling); + /// Set the IIR filter setting: OFF, 2, 3, 8, 16, 32 + bool set_iir_filter(IIRFilter iir_filter); + /// Get a temperature measurement + bool get_temperature(float &temperature); + /// Get a pressure measurement + bool get_pressure(float &pressure); + /// Get a temperature and pressure measurement + bool get_measurements(float &temperature, float &pressure); + /// Get a temperature and pressure measurement + bool get_measurement(); + /// Set the barometer mode + bool set_mode(OperationMode mode); + /// Set the BMP388 oversampling register + bool set_oversampling_register(Oversampling pressure_oversampling, Oversampling temperature_oversampling); + /// Checks if a measurement is ready + bool data_ready(); + + protected: + Oversampling temperature_oversampling_{OVERSAMPLING_X16}; + Oversampling pressure_oversampling_{OVERSAMPLING_X16}; + IIRFilter iir_filter_{IIR_FILTER_OFF}; + OperationMode operation_mode_{FORCED_MODE}; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + ERROR_SENSOR_STATUS, + ERROR_SENSOR_RESET, + } error_code_{NONE}; + + struct { // The BMP388 compensation trim parameters (coefficients) + uint16_t param_T1; + uint16_t param_T2; + int8_t param_T3; + int16_t param_P1; + int16_t param_P2; + int8_t param_P3; + int8_t param_P4; + uint16_t param_P5; + uint16_t param_P6; + int8_t param_P7; + int8_t param_P8; + int16_t param_P9; + int8_t param_P10; + int8_t param_P11; + } __attribute__((packed)) compensation_params_; + + struct FloatParams { // The BMP388 float point compensation trim parameters + float param_T1; + float param_T2; + float param_T3; + float param_P1; + float param_P2; + float param_P3; + float param_P4; + float param_P5; + float param_P6; + float param_P7; + float param_P8; + float param_P9; + float param_P10; + float param_P11; + } compensation_float_params_; + + union { // Copy of the BMP388's chip id register + struct { + uint8_t chip_id_nvm : 4; + uint8_t chip_id_fixed : 4; + } bit; + uint8_t reg; + } chip_id_ = {.reg = 0}; + + union { // Copy of the BMP388's event register + struct { + uint8_t por_detected : 1; + } bit; + uint8_t reg; + } event_ = {.reg = 0}; + + union { // Copy of the BMP388's interrupt status register + struct { + uint8_t fwm_int : 1; + uint8_t ffull_int : 1; + uint8_t : 1; + uint8_t drdy : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; + + union { // Copy of the BMP388's power control register + struct { + uint8_t press_en : 1; + uint8_t temp_en : 1; + uint8_t : 2; + uint8_t mode : 2; + } bit; + uint8_t reg; + } pwr_ctrl_ = {.reg = 0}; + + union { // Copy of the BMP388's oversampling register + struct { + uint8_t osr_p : 3; + uint8_t osr_t : 3; + } bit; + uint8_t reg; + } osr_ = {.reg = 0}; + + union { // Copy of the BMP388's output data rate register + struct { + uint8_t odr_sel : 5; + } bit; + uint8_t reg; + } odr_ = {.reg = 0}; + + union { // Copy of the BMP388's configuration register + struct { + uint8_t : 1; + uint8_t iir_filter : 3; + } bit; + uint8_t reg; + } config_ = {.reg = 0}; + + // Bosch temperature compensation function + float bmp388_compensate_temperature_(float uncomp_temp); + // Bosch pressure compensation function + float bmp388_compensate_pressure_(float uncomp_press, float t_lin); +}; + +} // namespace bmp3xx +} // namespace esphome diff --git a/esphome/components/bmp3xx/sensor.py b/esphome/components/bmp3xx/sensor.py new file mode 100644 index 0000000000..736e6df3d8 --- /dev/null +++ b/esphome/components/bmp3xx/sensor.py @@ -0,0 +1,100 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@martgras"] +DEPENDENCIES = ["i2c"] + +bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx") +Oversampling = bmp3xx_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32x": Oversampling.OVERSAMPLING_X32, +} + +IIRFilter = bmp3xx_ns.enum("IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": IIRFilter.IIR_FILTER_OFF, + "2X": IIRFilter.IIR_FILTER_2, + "4X": IIRFilter.IIR_FILTER_4, + "8X": IIRFilter.IIR_FILTER_8, + "16X": IIRFilter.IIR_FILTER_16, + "32X": IIRFilter.IIR_FILTER_32, + "64X": IIRFilter.IIR_FILTER_64, + "128X": IIRFilter.IIR_FILTER_128, +} + +BMP3XXComponent = bmp3xx_ns.class_( + "BMP3XXComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP3XXComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="2X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add(var.set_iir_filter_config(config[CONF_IIR_FILTER])) + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling_config(conf[CONF_OVERSAMPLING])) + + if CONF_PRESSURE in config: + conf = config[CONF_PRESSURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(conf[CONF_OVERSAMPLING])) diff --git a/esphome/components/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/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 384a3f23a0..f024c94b01 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -36,3 +36,5 @@ async def to_code(config): 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 9e00adae3d..d4e37f62f2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -67,6 +67,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->start_scanning(); request->redirect("/?save=true"); } @@ -84,14 +85,7 @@ void CaptivePortal::start() { 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().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { - 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; } diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 11a66f5100..f8cee79c55 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -52,7 +52,7 @@ void CCS811Component::setup() { 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); diff --git a/esphome/components/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py new file mode 100644 index 0000000000..f8efdf4b2a --- /dev/null +++ b/esphome/components/cd74hc4067/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +from esphome import pins +import esphome.config_validation as cv +from esphome.const import ( + CONF_DELAY, + CONF_ID, +) + +CODEOWNERS = ["@asoehlke"] +AUTO_LOAD = ["sensor", "voltage_sampler"] + +cd74hc4067_ns = cg.esphome_ns.namespace("cd74hc4067") + +CD74HC4067Component = cd74hc4067_ns.class_( + "CD74HC4067Component", cg.Component, cg.PollingComponent +) + +CONF_PIN_S0 = "pin_s0" +CONF_PIN_S1 = "pin_s1" +CONF_PIN_S2 = "pin_s2" +CONF_PIN_S3 = "pin_s3" + +DEFAULT_DELAY = "2ms" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CD74HC4067Component), + cv.Required(CONF_PIN_S0): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S1): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S2): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S3): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DELAY, default=DEFAULT_DELAY + ): 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) + + pin_s0 = await cg.gpio_pin_expression(config[CONF_PIN_S0]) + cg.add(var.set_pin_s0(pin_s0)) + pin_s1 = await cg.gpio_pin_expression(config[CONF_PIN_S1]) + cg.add(var.set_pin_s1(pin_s1)) + pin_s2 = await cg.gpio_pin_expression(config[CONF_PIN_S2]) + cg.add(var.set_pin_s2(pin_s2)) + pin_s3 = await cg.gpio_pin_expression(config[CONF_PIN_S3]) + cg.add(var.set_pin_s3(pin_s3)) + + cg.add(var.set_switch_delay(config[CONF_DELAY])) diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp new file mode 100644 index 0000000000..ea789c2d8c --- /dev/null +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -0,0 +1,86 @@ +#include "cd74hc4067.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cd74hc4067 { + +static const char *const TAG = "cd74hc4067"; + +float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } + +void CD74HC4067Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up CD74HC4067..."); + + this->pin_s0_->setup(); + this->pin_s1_->setup(); + this->pin_s2_->setup(); + this->pin_s3_->setup(); + + // set other pin, so that activate_pin will really switch + this->active_pin_ = 1; + this->activate_pin(0); +} + +void CD74HC4067Component::dump_config() { + ESP_LOGCONFIG(TAG, "CD74HC4067 Multiplexer:"); + LOG_PIN(" S0 Pin: ", this->pin_s0_); + LOG_PIN(" S1 Pin: ", this->pin_s1_); + LOG_PIN(" S2 Pin: ", this->pin_s2_); + LOG_PIN(" S3 Pin: ", this->pin_s3_); + ESP_LOGCONFIG(TAG, "switch delay: %d", this->switch_delay_); +} + +void CD74HC4067Component::activate_pin(uint8_t pin) { + if (this->active_pin_ != pin) { + ESP_LOGD(TAG, "switch to input %d", pin); + + static int mux_channel[16][4] = { + {0, 0, 0, 0}, // channel 0 + {1, 0, 0, 0}, // channel 1 + {0, 1, 0, 0}, // channel 2 + {1, 1, 0, 0}, // channel 3 + {0, 0, 1, 0}, // channel 4 + {1, 0, 1, 0}, // channel 5 + {0, 1, 1, 0}, // channel 6 + {1, 1, 1, 0}, // channel 7 + {0, 0, 0, 1}, // channel 8 + {1, 0, 0, 1}, // channel 9 + {0, 1, 0, 1}, // channel 10 + {1, 1, 0, 1}, // channel 11 + {0, 0, 1, 1}, // channel 12 + {1, 0, 1, 1}, // channel 13 + {0, 1, 1, 1}, // channel 14 + {1, 1, 1, 1} // channel 15 + }; + this->pin_s0_->digital_write(mux_channel[pin][0]); + this->pin_s1_->digital_write(mux_channel[pin][1]); + this->pin_s2_->digital_write(mux_channel[pin][2]); + this->pin_s3_->digital_write(mux_channel[pin][3]); + // small delay is needed to let the multiplexer switch + delay(this->switch_delay_); + this->active_pin_ = pin; + } +} + +CD74HC4067Sensor::CD74HC4067Sensor(CD74HC4067Component *parent) : parent_(parent) {} + +void CD74HC4067Sensor::update() { + float value_v = this->sample(); + this->publish_state(value_v); +} + +float CD74HC4067Sensor::get_setup_priority() const { return this->parent_->get_setup_priority() - 1.0f; } + +float CD74HC4067Sensor::sample() { + this->parent_->activate_pin(this->pin_); + return this->source_->sample(); +} + +void CD74HC4067Sensor::dump_config() { + LOG_SENSOR(TAG, "CD74HC4067 Sensor", this); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace cd74hc4067 +} // namespace esphome diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h new file mode 100644 index 0000000000..4a5c2e4e35 --- /dev/null +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -0,0 +1,65 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +namespace esphome { +namespace cd74hc4067 { + +class CD74HC4067Component : public Component { + public: + /// Set up the internal sensor array. + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + /// setting pin active by setting the right combination of the four multiplexer input pins + void activate_pin(uint8_t pin); + + /// set the pin connected to multiplexer control pin 0 + void set_pin_s0(InternalGPIOPin *pin) { this->pin_s0_ = pin; } + /// set the pin connected to multiplexer control pin 1 + void set_pin_s1(InternalGPIOPin *pin) { this->pin_s1_ = pin; } + /// set the pin connected to multiplexer control pin 2 + void set_pin_s2(InternalGPIOPin *pin) { this->pin_s2_ = pin; } + /// set the pin connected to multiplexer control pin 3 + void set_pin_s3(InternalGPIOPin *pin) { this->pin_s3_ = pin; } + + /// set the delay needed after an input switch + void set_switch_delay(uint32_t switch_delay) { this->switch_delay_ = switch_delay; } + + private: + InternalGPIOPin *pin_s0_; + InternalGPIOPin *pin_s1_; + InternalGPIOPin *pin_s2_; + InternalGPIOPin *pin_s3_; + /// the currently active pin + uint8_t active_pin_; + uint32_t switch_delay_; +}; + +class CD74HC4067Sensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { + public: + CD74HC4067Sensor(CD74HC4067Component *parent); + + void update() override; + + 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_source(voltage_sampler::VoltageSampler *source) { this->source_ = source; } + + float sample() override; + + protected: + CD74HC4067Component *parent_; + /// The sampling source to read values from. + voltage_sampler::VoltageSampler *source_; + + uint8_t pin_; +}; +} // namespace cd74hc4067 +} // namespace esphome diff --git a/esphome/components/cd74hc4067/sensor.py b/esphome/components/cd74hc4067/sensor.py new file mode 100644 index 0000000000..7c7cf9ccb7 --- /dev/null +++ b/esphome/components/cd74hc4067/sensor.py @@ -0,0 +1,55 @@ +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_SENSOR, + CONF_NUMBER, + ICON_FLASH, + UNIT_VOLT, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_VOLTAGE, +) +from . import cd74hc4067_ns, CD74HC4067Component + +DEPENDENCIES = ["cd74hc4067"] + +CD74HC4067Sensor = cd74hc4067_ns.class_( + "CD74HC4067Sensor", + sensor.Sensor, + cg.PollingComponent, + voltage_sampler.VoltageSampler, +) + +CONF_CD74HC4067_ID = "cd74hc4067_id" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_FLASH, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(CD74HC4067Sensor), + cv.GenerateID(CONF_CD74HC4067_ID): cv.use_id(CD74HC4067Component), + cv.Required(CONF_NUMBER): cv.int_range(0, 15), + cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_CD74HC4067_ID]) + + var = cg.new_Pvariable(config[CONF_ID], parent) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + cg.add(var.set_pin(config[CONF_NUMBER])) + + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_source(sens)) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 7ff769e5cb..87b9a4b3e2 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( CONF_MODE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_ON_STATE, CONF_PRESET, CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, @@ -34,6 +35,7 @@ from esphome.const import ( CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC, CONF_TEMPERATURE_STEP, + CONF_TRIGGER_ID, CONF_VISUAL, CONF_MQTT_ID, ) @@ -101,6 +103,7 @@ validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) +StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -161,6 +164,11 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). 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), + } + ), } ) @@ -205,7 +213,7 @@ async def setup_climate_core_(var, config): if CONF_MODE_COMMAND_TOPIC in config: cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC])) if CONF_MODE_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_state_topic(config[CONF_MODE_STATE_TOPIC])) + cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC])) if CONF_SWING_MODE_COMMAND_TOPIC in config: cg.add( @@ -256,6 +264,10 @@ async def setup_climate_core_(var, config): ) ) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + async def register_climate(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 49a87027f2..3145358dab 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -42,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 34e6328d8a..ebea20ed1f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -440,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; } +#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); } diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index b47d9b0141..76adfb42bb 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -10,21 +10,22 @@ climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); - if (supports_cool_) + if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); - if (supports_heat_) + if (this->supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - if (supports_dry_) + if (this->supports_dry_) traits.add_supported_mode(climate::CLIMATE_MODE_DRY); - if (supports_fan_only_) + if (this->supports_fan_only_) traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); traits.set_supports_two_point_target_temperature(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_); - traits.set_supported_fan_modes(fan_modes_); - traits.set_supported_swing_modes(swing_modes_); + traits.set_supported_fan_modes(this->fan_modes_); + traits.set_supported_swing_modes(this->swing_modes_); + traits.set_supported_presets(this->presets_); return traits; } @@ -50,6 +51,7 @@ void ClimateIR::setup() { 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; + this->preset = climate::CLIMATE_PRESET_NONE; } // Never send nan to HA if (std::isnan(this->target_temperature)) @@ -65,6 +67,8 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->fan_mode = *call.get_fan_mode(); if (call.get_swing_mode().has_value()) this->swing_mode = *call.get_swing_mode(); + if (call.get_preset().has_value()) + this->preset = *call.get_preset(); this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 677021da29..5be4fc06f5 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -22,7 +22,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}) { + std::set swing_modes = {}, std::set presets = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; @@ -30,6 +30,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: this->supports_fan_only_ = supports_fan_only; this->fan_modes_ = std::move(fan_modes); this->swing_modes_ = std::move(swing_modes); + this->presets_ = std::move(presets); } void setup() override; @@ -61,6 +62,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_fan_only_{false}; std::set fan_modes_ = {}; std::set swing_modes_ = {}; + std::set presets_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index c9145e4ecf..76ec1627c2 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -1,4 +1,5 @@ #include "coolix.h" +#include "esphome/components/remote_base/coolix_protocol.h" #include "esphome/core/log.h" namespace esphome { @@ -6,29 +7,29 @@ namespace coolix { static const char *const TAG = "coolix.climate"; -const uint32_t COOLIX_OFF = 0xB27BE0; -const uint32_t COOLIX_SWING = 0xB26BE0; -const uint32_t COOLIX_LED = 0xB5F5A5; -const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; +static const uint32_t COOLIX_OFF = 0xB27BE0; +static const uint32_t COOLIX_SWING = 0xB26BE0; +static const uint32_t COOLIX_LED = 0xB5F5A5; +static const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -const uint8_t COOLIX_COOL = 0b0000; -const uint8_t COOLIX_DRY_FAN = 0b0100; -const uint8_t COOLIX_AUTO = 0b1000; -const uint8_t COOLIX_HEAT = 0b1100; -const uint32_t COOLIX_MODE_MASK = 0b1100; -const uint32_t COOLIX_FAN_MASK = 0xF000; -const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; -const uint32_t COOLIX_FAN_AUTO = 0xB000; -const uint32_t COOLIX_FAN_MIN = 0x9000; -const uint32_t COOLIX_FAN_MED = 0x5000; -const uint32_t COOLIX_FAN_MAX = 0x3000; +static const uint8_t COOLIX_COOL = 0b0000; +static const uint8_t COOLIX_DRY_FAN = 0b0100; +static const uint8_t COOLIX_AUTO = 0b1000; +static const uint8_t COOLIX_HEAT = 0b1100; +static const uint32_t COOLIX_MODE_MASK = 0b1100; +static const uint32_t COOLIX_FAN_MASK = 0xF000; +static const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; +static const uint32_t COOLIX_FAN_AUTO = 0xB000; +static const uint32_t COOLIX_FAN_MIN = 0x9000; +static const uint32_t COOLIX_FAN_MED = 0x5000; +static const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature -const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; -const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. -const uint32_t COOLIX_TEMP_MASK = 0b11110000; -const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { +static const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; +static const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. +static const uint32_t COOLIX_TEMP_MASK = 0b11110000; +static const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { 0b00000000, // 17C 0b00010000, // 18c 0b00110000, // 19C @@ -45,17 +46,6 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { 0b10110000 // 30C }; -// Constants -static const uint32_t BIT_MARK_US = 660; -static const uint32_t HEADER_MARK_US = 560 * 8; -static const uint32_t HEADER_SPACE_US = 560 * 8; -static const uint32_t BIT_ONE_SPACE_US = 1500; -static const uint32_t BIT_ZERO_SPACE_US = 450; -static const uint32_t FOOTER_MARK_US = BIT_MARK_US; -static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; - -const uint16_t COOLIX_BITS = 24; - void CoolixClimate::transmit_state() { uint32_t remote_state = 0xB20F00; @@ -111,119 +101,60 @@ void CoolixClimate::transmit_state() { } } } - ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); + ESP_LOGV(TAG, "Sending coolix code: 0x%06X", remote_state); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); - - data->set_carrier_frequency(38000); - uint16_t repeat = 1; - for (uint16_t r = 0; r <= repeat; r++) { - // Header - data->mark(HEADER_MARK_US); - data->space(HEADER_SPACE_US); - // Data - // Break data into bytes, starting at the Most Significant - // Byte. Each byte then being sent normal, then followed inverted. - for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) { - // Grab a bytes worth of data. - uint8_t byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF; - // Normal - for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(BIT_MARK_US); - data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); - } - // Inverted - for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(BIT_MARK_US); - data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); - } - } - // Footer - data->mark(BIT_MARK_US); - data->space(FOOTER_SPACE_US); // Pause before repeating - } - + remote_base::CoolixProtocol().encode(data, remote_state); transmit.perform(); } -bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { +bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { + auto decoded = remote_base::CoolixProtocol().decode(data); + if (!decoded.has_value()) + return false; // Decoded remote state y 3 bytes long code. - uint32_t remote_state = 0; - // The protocol sends the data twice, read here - uint32_t loop_read; - for (uint16_t loop = 1; loop <= 2; loop++) { - if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) - return false; - loop_read = 0; - for (uint8_t a_byte = 0; a_byte < 3; a_byte++) { - uint8_t byte = 0; - for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { - if (data.expect_item(BIT_MARK_US, BIT_ONE_SPACE_US)) - byte |= 1 << a_bit; - else if (!data.expect_item(BIT_MARK_US, BIT_ZERO_SPACE_US)) - return false; - } - // Need to see this segment inverted - for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { - bool bit = byte & (1 << a_bit); - if (!data.expect_item(BIT_MARK_US, bit ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) - return false; - } - // Receiving MSB first: reorder bytes - loop_read |= byte << ((2 - a_byte) * 8); - } - // Footer Mark - if (!data.expect_mark(BIT_MARK_US)) - return false; - if (loop == 1) { - // Back up state on first loop - remote_state = loop_read; - if (!data.expect_space(FOOTER_SPACE_US)) - return false; - } - } - - ESP_LOGV(TAG, "Decoded 0x%02X", remote_state); - if (remote_state != loop_read || (remote_state & 0xFF0000) != 0xB20000) + uint32_t remote_state = *decoded; + ESP_LOGV(TAG, "Decoded 0x%06X", remote_state); + if ((remote_state & 0xFF0000) != 0xB20000) return false; if (remote_state == COOLIX_OFF) { - this->mode = climate::CLIMATE_MODE_OFF; + parent->mode = climate::CLIMATE_MODE_OFF; } else if (remote_state == COOLIX_SWING) { - this->swing_mode = - this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + parent->swing_mode = + parent->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) - this->mode = climate::CLIMATE_MODE_HEAT; + parent->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + parent->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; + parent->mode = climate::CLIMATE_MODE_DRY; else - this->mode = climate::CLIMATE_MODE_FAN_ONLY; + parent->mode = climate::CLIMATE_MODE_FAN_ONLY; } else - this->mode = climate::CLIMATE_MODE_COOL; + parent->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - 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; + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL || + parent->mode == climate::CLIMATE_MODE_DRY) + parent->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) - this->fan_mode = climate::CLIMATE_FAN_LOW; + parent->fan_mode = climate::CLIMATE_FAN_LOW; else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) - this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + parent->fan_mode = climate::CLIMATE_FAN_MEDIUM; else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) - this->fan_mode = climate::CLIMATE_FAN_HIGH; + parent->fan_mode = climate::CLIMATE_FAN_HIGH; // Temperature uint8_t temperature_code = remote_state & COOLIX_TEMP_MASK; for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) if (COOLIX_TEMP_MAP[i] == temperature_code) - this->target_temperature = i + COOLIX_TEMP_MIN; + parent->target_temperature = i + COOLIX_TEMP_MIN; } - this->publish_state(); + parent->publish_state(); return true; } diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index caf93f7621..3419795875 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -26,11 +26,15 @@ class CoolixClimate : public climate_ir::ClimateIR { climate_ir::ClimateIR::control(call); } + /// This static method can be used in other climate components that accept the Coolix protocol. See midea_ir for + /// example. + static bool on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data); + 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 on_receive(remote_base::RemoteReceiveData data) override { return CoolixClimate::on_coolix(this, data); } bool send_swing_cmd_{false}; }; diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index a8d3d691a4..21f35f14de 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -210,7 +210,10 @@ Cover::Cover() : Cover("") {} std::string Cover::get_device_class() { if (this->device_class_override_.has_value()) return *this->device_class_override_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->device_class(); +#pragma GCC diagnostic pop } bool Cover::is_fully_open() const { return this->position == COVER_OPEN; } bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index a67f8d2393..1b5d3a8fa1 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -169,6 +169,11 @@ class Cover : public EntityBase { friend CoverCall; virtual void control(const CoverCall &call) = 0; + + /** Override this to set the default device class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string device_class(); optional restore_state_(); diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index a172bcdf56..b0c0531936 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -102,8 +102,6 @@ void CS5460AComponent::hw_init_() { /* Doesn't reset the register values etc., just restarts the "computation cycle" */ void CS5460AComponent::restart_() { - int cnt; - this->enable(); /* Stop running conversion, wake up if needed */ this->write_byte(CMD_POWER_UP); 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 87bc4c4bdf..25d75da3e6 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -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 1c8efc4f72..2f48aff0aa 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -3,16 +3,20 @@ import esphome.config_validation as cv from esphome.components import sensor, uart 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"] @@ -44,6 +48,12 @@ CONFIG_SCHEMA = ( 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")) @@ -71,3 +81,7 @@ async def to_code(config): conf = config[CONF_POWER] 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/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index 5f8d0288e2..83d0253691 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -231,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/dallas/__init__.py b/esphome/components/dallas/__init__.py index 2dbc69b8e2..0f71399a7c 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -6,26 +6,20 @@ from esphome.const import CONF_ID, CONF_PIN MULTI_CONF = True 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") -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(DallasComponent), - cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - } - ).extend(cv.polling_component_schema("60s")), - # pin_mode call logs in esp-idf, but InterruptLock is active -> crash - cv.only_with_arduino, -) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DallasComponent), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("60s")) async def to_code(config): - pin = await 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) + 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 0fc4108687..3610e79447 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -31,18 +31,16 @@ 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); auto *address8 = reinterpret_cast(&address); if (crc8(address8, 7) != address8[7]) { - ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", s.c_str()); + ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str()); continue; } if (address8[0] != DALLAS_MODEL_DS18S20 && address8[0] != DALLAS_MODEL_DS1822 && @@ -70,7 +68,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()) { @@ -78,8 +76,7 @@ void DallasComponent::dump_config() { } else { ESP_LOGD(TAG, " Found sensors:"); for (auto &address : this->found_sensors_) { - std::string s = uint64_to_string(address); - ESP_LOGD(TAG, " 0x%s", s.c_str()); + ESP_LOGD(TAG, " 0x%s", format_hex(address).c_str()); } } @@ -102,15 +99,12 @@ 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) { @@ -121,11 +115,7 @@ 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' - Resetting bus for read failed!", sensor->get_name().c_str()); @@ -146,7 +136,6 @@ void DallasComponent::update() { }); } } -DallasComponent::DallasComponent(ESPOneWire *one_wire) : one_wire_(one_wire) {} void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; } uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; } @@ -156,13 +145,13 @@ void DallasTemperatureSensor::set_index(uint8_t index) { this->index_ = index; } uint8_t *DallasTemperatureSensor::get_address8() { return reinterpret_cast(&this->address_); } const std::string &DallasTemperatureSensor::get_address_name() { if (this->address_name_.empty()) { - this->address_name_ = std::string("0x") + uint64_to_string(this->address_); + this->address_name_ = std::string("0x") + format_hex(this->address_); } return this->address_name_; } bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { - ESPOneWire *wire = this->parent_->one_wire_; + auto *wire = this->parent_->one_wire_; if (!wire->reset()) { return false; } @@ -176,11 +165,7 @@ bool IRAM_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"); @@ -214,21 +199,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 @@ -253,7 +235,7 @@ float DallasTemperatureSensor::get_temp_c() { return temp / 128.0f; } -std::string DallasTemperatureSensor::unique_id() { return "dallas-" + uint64_to_string(this->address_); } +std::string DallasTemperatureSensor::unique_id() { return "dallas-" + str_upper_case(format_hex(this->address_)); } } // namespace dallas } // namespace esphome diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index 8d405f6eab..37c098283a 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -11,8 +11,7 @@ class DallasTemperatureSensor; class DallasComponent : public PollingComponent { public: - explicit DallasComponent(ESPOneWire *one_wire); - + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void register_sensor(DallasTemperatureSensor *sensor); void setup() override; @@ -24,6 +23,7 @@ class DallasComponent : public PollingComponent { protected: friend DallasTemperatureSensor; + InternalGPIOPin *pin_; ESPOneWire *one_wire_; std::vector sensors_; std::vector found_sensors_; diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 9278b83f7f..a0ab10f8a4 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -10,115 +10,123 @@ 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 IRAM_ATTR ESPOneWire::reset() { - uint8_t retries = 125; + // See reset here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; - // Wait for communication to clear - this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + // Wait for communication to clear (delay G) + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + uint8_t retries = 125; do { if (--retries == 0) return false; delayMicroseconds(2); - } while (!this->pin_->digital_read()); + } while (!pin_.digital_read()); - // Send 480µs LOW TX reset pulse - this->pin_->pin_mode(gpio::FLAG_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(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - // after 15µs-60µs wait time, responder pulls low for 60µs-240µs - // let's have 70µs just in case + // 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 IRAM_ATTR ESPOneWire::write_bit(bool bit) { - // Initiate write/read by pulling low. - this->pin_->pin_mode(gpio::FLAG_OUTPUT); - this->pin_->digital_write(false); + // 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 IRAM_ATTR ESPOneWire::read_bit() { - // Initiate read slot by pulling LOW for at least 1µs - this->pin_->pin_mode(gpio::FLAG_OUTPUT); - this->pin_->digital_write(false); + // 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(gpio::FLAG_INPUT | gpio::FLAG_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 IRAM_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 IRAM_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 IRAM_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 IRAM_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 IRAM_ATTR ESPOneWire::select(uint64_t address) { +void ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void IRAM_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 IRAM_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 IRAM_ATTR ESPOneWire::search() { return this->rom_number_; } -std::vector IRAM_ATTR ESPOneWire::search_vec() { +std::vector ESPOneWire::search_vec() { std::vector res; this->reset_search(); @@ -206,10 +214,9 @@ std::vector IRAM_ATTR ESPOneWire::search_vec() { return res; } -void IRAM_ATTR ESPOneWire::skip() { +void ESPOneWire::skip() { this->write8(0xCC); // skip ROM } -GPIOPin *ESPOneWire::get_pin() { return this->pin_; } uint8_t IRAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast(&this->rom_number_); } diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h index 728fa127d3..ef6f079f02 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/hal.h" #include @@ -12,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. * @@ -55,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/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 2b884d3b9a..6194a55205 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -3,6 +3,7 @@ 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.wizard import wizard_file from esphome.yaml_util import dump @@ -29,6 +30,13 @@ CONFIG_SCHEMA = cv.Schema( } ) +WIFI_CONFIG = """ + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password +""" + async def to_code(config): cg.add_define("USE_DASHBOARD_IMPORT") @@ -41,5 +49,24 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N if p.exists(): raise FileExistsError - config = {"substitutions": {"name": name}, "packages": {project_name: import_url}} - p.write_text(dump(config), encoding="utf8") + if project_name == "esphome.web": + p.write_text( + wizard_file( + name=name, + platform="ESP32" if "esp32" in import_url else "ESP8266", + board="esp32dev" if "esp32" in import_url else "esp01_1m", + ssid="!secret wifi_ssid", + psk="!secret wifi_password", + ), + encoding="utf8", + ) + else: + 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/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index b856733121..f3d0bded13 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -4,20 +4,23 @@ #include "esphome/core/defines.h" #include "esphome/core/version.h" +#ifdef USE_ESP_IDF +#include +#include +#endif + #ifdef USE_ESP32 +#if ESP_IDF_VERSION_MAJOR >= 4 +#include +#else #include -#include +#endif #endif #ifdef USE_ARDUINO #include #endif -#ifdef USE_ESP_IDF -#include -#include -#endif - namespace esphome { namespace debug { @@ -98,7 +101,7 @@ void DebugComponent::dump_config() { info.features &= ~CHIP_FEATURE_BT; } if (info.features) - features += "Other:" + uint64_to_string(info.features); + features += "Other:" + format_hex(info.features); ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores, info.revision); diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index f47888b8eb..ba4c2c0d7e 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -41,15 +41,30 @@ 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, } +WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration") 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" + +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, + } +) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(DeepSleepComponent), - cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_milliseconds, + 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 @@ -85,7 +100,28 @@ async def to_code(config): 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] diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index e4b1edfb7b..7774014d3d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -13,12 +13,35 @@ static const char *const TAG = "deep_sleep"; bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +optional DeepSleepComponent::get_run_duration_() const { +#ifdef USE_ESP32 + if (this->wakeup_cause_to_run_duration_.has_value()) { + esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); + switch (wakeup_cause) { + case ESP_SLEEP_WAKEUP_EXT0: + case ESP_SLEEP_WAKEUP_EXT1: + 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..."); @@ -33,6 +56,11 @@ void DeepSleepComponent::dump_config() { 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 } void DeepSleepComponent::loop() { @@ -49,6 +77,9 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode 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) { @@ -77,9 +108,10 @@ void DeepSleepComponent::begin_sleep(bool manual) { if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { - bool level = this->wakeup_pin_->is_inverted(); - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) + bool level = !this->wakeup_pin_->is_inverted(); + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; + } esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); } if (this->ext1_wakeup_.has_value()) { diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index d7969ba999..59df199a9f 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -32,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; @@ -59,6 +68,11 @@ class DeepSleepComponent : public Component { 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); @@ -75,12 +89,17 @@ 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 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}; diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index 1334f0270c..cd1886728e 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -33,7 +33,7 @@ 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.Required(CONF_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 947b09a258..0d403f99f0 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, CONF_ID, CONF_LAMBDA, CONF_PAGES, @@ -79,6 +80,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_TO): cv.use_id(DisplayPage), } ), + cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, } ) @@ -86,6 +88,10 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( 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]: diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 2ee06e379f..b97fb4ae23 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -5,6 +5,7 @@ #include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" namespace esphome { namespace display { @@ -15,7 +16,8 @@ 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 (std::nothrow) uint8_t[buffer_length]; // NOLINT + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_ = allocator.allocate(buffer_length); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); return; @@ -336,7 +338,9 @@ void DisplayBuffer::show_page(DisplayPage *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()) { @@ -494,7 +498,7 @@ bool Animation::get_pixel(int x, int y) const { 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 >= this->width_ * this->height_ * this->animation_frame_count_) + 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)); @@ -503,7 +507,7 @@ 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 >= this->width_ * this->height_ * this->animation_frame_count_) + 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) | @@ -515,7 +519,7 @@ 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 >= this->width_ * this->height_ * this->animation_frame_count_) + 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); diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 54488f18f7..c803180a2d 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -333,6 +333,9 @@ class DisplayBuffer { /// Internal method to set the display rotation with. void set_rotation(DisplayRotation rotation); + // Internal method to set display auto clearing. + void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } + protected: void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); @@ -352,6 +355,7 @@ class DisplayBuffer { DisplayPage *page_{nullptr}; DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; + bool auto_clear_enabled_{true}; }; class DisplayPage { diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 8fc3b0adb9..202de912de 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -42,7 +42,7 @@ class ColorUtil { ? 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) & 0xFF), ((1 << third_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; diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index dd6e6051aa..7a7681082e 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -1,9 +1,11 @@ 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"] @@ -11,10 +13,13 @@ CODEOWNERS = ["@glmnet", "@zuidwijk"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor", "text_sensor"] -CONF_DSMR_ID = "dsmr_id" -CONF_DECRYPTION_KEY = "decryption_key" CONF_CRC_CHECK = "crc_check" +CONF_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") @@ -46,6 +51,14 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DECRYPTION_KEY): _validate_key, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, + cv.Optional(CONF_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, @@ -55,10 +68,17 @@ CONFIG_SCHEMA = cv.All( 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 diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index b798fe5d44..7b339e5fe0 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -12,156 +12,275 @@ 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->decryption_key_.empty()) - this->receive_telegram_(); - else - this->receive_encrypted_(); + 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_() { - int count = MAX_BYTES_PER_LOOP; - while (available() && count-- > 0) { - const char c = read(); + 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 found"); - header_found_ = true; - footer_found_ = false; - telegram_len_ = 0; + ESP_LOGV(TAG, "Header of telegram found"); + this->reset_telegram_(); + this->header_found_ = true; } - if (!header_found_) + if (!this->header_found_) continue; // Check for buffer overflow. - if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { - header_found_ = false; - footer_found_ = false; - ESP_LOGE(TAG, "Error: Message larger than buffer"); + 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 - while (c == '(' && (telegram_[telegram_len_ - 1] == '\n' || telegram_[telegram_len_ - 1] == '\r')) - telegram_len_--; + // 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. - telegram_[telegram_len_] = c; - telegram_len_++; + 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 found"); - footer_found_ = true; + 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 (footer_found_ && c == '\n') { - header_found_ = false; + if (this->footer_found_ && c == '\n') { // Parse the telegram and publish sensor values. - if (parse_telegram()) - return; + this->parse_telegram(); + this->reset_telegram_(); + return; } } } -void Dsmr::receive_encrypted_() { - // Encrypted buffer - uint8_t buffer[MAX_TELEGRAM_LENGTH]; - size_t buffer_length = 0; +void Dsmr::receive_encrypted_telegram_() { + while (this->available_within_timeout_()) { + const char c = this->read(); - size_t packet_size = 0; - while (available()) { - const char c = read(); - - if (!header_found_) { - if ((uint8_t) c == 0xdb) { - ESP_LOGV(TAG, "Start byte 0xDB found"); - header_found_ = true; + // 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; } - // Sanity check - if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { - if (buffer_length == 0) { - ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); - } else { - ESP_LOGW(TAG, "Unexpected data"); - } - this->status_momentary_warning("unexpected_data"); - this->flush(); - while (available()) - read(); + // 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; } - buffer[buffer_length++] = c; + // Store the byte in the buffer. + this->crypt_telegram_[this->crypt_bytes_read_] = c; + this->crypt_bytes_read_++; - if (packet_size == 0 && buffer_length > 20) { - // Complete header + a few bytes of data - packet_size = buffer[11] << 8 | buffer[12]; - } - if (buffer_length == packet_size + 13 && packet_size > 0) { - ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); - - GCM *gcmaes128{new GCM()}; - gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); - // the iv is 8 bytes of the system title + 4 bytes frame counter - // system title is at byte 2 and frame counter at byte 15 - for (int i = 10; i < 14; i++) - buffer[i] = buffer[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&buffer[2], iv_size); - gcmaes128->decrypt(reinterpret_cast(this->telegram_), - // the ciphertext start at byte 18 - &buffer[18], - // cipher size - buffer_length - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - - telegram_len_ = strnlen(this->telegram_, sizeof(this->telegram_)); - ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); - ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); - - parse_telegram(); - telegram_len_ = 0; - return; + // 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_); } - if (!available()) { - // baud rate is 115200 for encrypted data, this means a few byte should arrive every time - // program runs faster than buffer loading then available() might return false in the middle - delay(4); // Wait for data + // Check for the end of the encrypted telegram. + if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { + continue; } - } - if (buffer_length > 0) { - ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); + 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"); + ESP_LOGV(TAG, "Trying to parse telegram"); + this->stop_requesting_data_(); ::dsmr::ParseResult res = - ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, false, + ::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(telegram_, telegram_ + telegram_len_); + 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(); - publish_sensors(data); + this->publish_sensors(data); return true; } } void Dsmr::dump_config() { - ESP_LOGCONFIG(TAG, "dsmr:"); + 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, ) @@ -174,23 +293,31 @@ 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."); + ESP_LOGE(TAG, "Error, decryption key must be 32 character long"); return; } this->decryption_key_.clear(); - ESP_LOGI(TAG, "Decryption key is set."); + ESP_LOGI(TAG, "Decryption key is set"); // Verbose level prints decryption key ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(decryption_key.c_str()[i * 2]), 2); - decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); + 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 } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 4f9a66b3d0..76f79ee55c 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -16,10 +16,6 @@ namespace esphome { namespace dsmr { -static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; -static constexpr uint32_t MAX_BYTES_PER_LOOP = 50; -static constexpr uint32_t POLL_TIMEOUT = 1000; - using namespace ::dsmr::fields; // DSMR_**_LIST generated by ESPHome and written in esphome/core/defines @@ -53,6 +49,7 @@ 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(); @@ -72,6 +69,10 @@ class Dsmr : public Component, public uart::UARTDevice { 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) \ @@ -84,13 +85,40 @@ class Dsmr : public Component, public uart::UARTDevice { protected: void receive_telegram_(); - void receive_encrypted_(); + void receive_encrypted_telegram_(); + void reset_telegram_(); - // Telegram buffer - char telegram_[MAX_TELEGRAM_LENGTH]; - int telegram_len_{0}; + /// 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_(); - // Serial parser + // 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}; diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 761009c766..d809d0d105 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -75,14 +75,14 @@ CONFIG_SCHEMA = cv.Schema( UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 3, - DEVICE_CLASS_ENERGY, + DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 3, - DEVICE_CLASS_ENERGY, + DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, ), cv.Optional("power_delivered"): sensor.sensor_schema( @@ -166,42 +166,42 @@ CONFIG_SCHEMA = cv.Schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l1"): sensor.sensor_schema( diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 3d7f731d5d..9a881c81f0 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -12,7 +12,6 @@ void DutyCycleSensor::setup() { 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_, gpio::INTERRUPT_ANY_EDGE); @@ -24,21 +23,24 @@ 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; } diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index 22d3588fb7..ffb1802e14 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -30,7 +30,7 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { InternalGPIOPin *pin_; DutyCycleSensorStore store_{}; - uint32_t last_update_; + uint32_t last_update_{0}; }; } // namespace duty_cycle diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 704f9bb3e8..8214886f8c 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2,11 +2,13 @@ from dataclasses import dataclass from typing import Union from pathlib import Path import logging +import os -from esphome.helpers import write_file_if_changed +from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_SOURCE, CONF_TYPE, CONF_VARIANT, CONF_VERSION, @@ -21,14 +23,16 @@ from esphome.core import CORE, HexInt import esphome.config_validation as cv import esphome.codegen as cg -from .const import ( +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 @@ -49,7 +53,7 @@ def set_core_data(config): elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( - config[CONF_FRAMEWORK][CONF_VERSION_HINT] + config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] @@ -90,6 +94,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: 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 @@ -119,119 +130,123 @@ ESP_IDF_PLATFORM_VERSION = cv.Version(3, 3, 2) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": ("https://github.com/espressif/arduino-esp32.git", cv.Version(2, 0, 0)), - "latest": ("", cv.Version(1, 0, 3)), - "recommended": ( - _format_framework_arduino_version(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION), - RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, - ), + "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), } - ver_value = value[CONF_VERSION] - default_ver_hint = None - if ver_value.lower() in lookups: - default_ver_hint = str(lookups[ver_value.lower()][1]) - ver_value = lookups[ver_value.lower()][0] + + 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: - with cv.suppress_invalid(): - ver = cv.Version.parse(cv.version_number(value)) - if ver <= cv.Version(1, 0, 3): - ver_value = f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - else: - ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - default_ver_hint = str(ver) - value[CONF_VERSION] = ver_value + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) - if CONF_VERSION_HINT not in value and default_ver_hint is None: - raise cv.Invalid("Needs a version hint to understand the framework version") + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) - value[CONF_VERSION_HINT] = ver_hint_s - plat_ver = value.get(CONF_PLATFORM_VERSION, ARDUINO_PLATFORM_VERSION) - value[CONF_PLATFORM_VERSION] = str(plat_ver) + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) + ) - if cv.Version.parse(ver_hint_s) != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( - "The selected arduino framework version is not the recommended one" - ) - _LOGGER.warning( - "If there are connectivity or build issues please remove the manual version" + "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 _format_framework_espidf_version(ver: cv.Version) -> str: - # format the given arduino (https://github.com/espressif/esp-idf/releases) version to - # a PIO platformio/framework-espidf value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf - return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - - def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": ("https://github.com/espressif/esp-idf.git", cv.Version(4, 3, 1)), - "latest": ("", cv.Version(4, 3, 0)), - "recommended": ( - _format_framework_espidf_version(RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION), - RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, - ), + "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), } - ver_value = value[CONF_VERSION] - default_ver_hint = None - if ver_value.lower() in lookups: - default_ver_hint = str(lookups[ver_value.lower()][1]) - ver_value = lookups[ver_value.lower()][0] + + 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: - with cv.suppress_invalid(): - ver = cv.Version.parse(cv.version_number(value)) - ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - default_ver_hint = str(ver) - value[CONF_VERSION] = ver_value + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) - if CONF_VERSION_HINT not in value and default_ver_hint is None: - raise cv.Invalid("Needs a version hint to understand the framework version") + if version < cv.Version(4, 0, 0): + raise cv.Invalid("Only ESP-IDF 4.0+ is supported.") - ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) - value[CONF_VERSION_HINT] = ver_hint_s - if cv.Version.parse(ver_hint_s) < cv.Version(4, 0, 0): - raise cv.Invalid("Only ESP-IDF 4.0+ is supported") - if cv.Version.parse(ver_hint_s) != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + 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" + "The selected ESP-IDF framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." ) - _LOGGER.warning( - "If there are connectivity or build issues please remove the manual version" - ) - - plat_ver = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) - value[CONF_PLATFORM_VERSION] = str(plat_ver) return value -CONF_VERSION_HINT = "version_hint" +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_VERSION_HINT): cv.version_number, - cv.Optional(CONF_PLATFORM_VERSION): 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_VERSION_HINT): cv.version_number, + 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_PLATFORM_VERSION): cv.string_strict, cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, @@ -260,12 +275,11 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, - cv.Optional(CONF_VARIANT, default="ESP32"): cv.one_of( - *VARIANTS, upper=True - ), + cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } ), + _detect_variant, set_core_data, ) @@ -275,21 +289,23 @@ async def to_code(config): 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]) + + cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: - cg.add_platformio_option( - "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" - ) cg.add_platformio_option("framework", "espidf") cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-Wno-nonnull-compare") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-espidf @ {conf[CONF_VERSION]}"], + [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) @@ -299,6 +315,15 @@ async def to_code(config): 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(): @@ -311,15 +336,12 @@ async def to_code(config): ) elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: - cg.add_platformio_option( - "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" - ) cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-arduinoespressif32 @ {conf[CONF_VERSION]}"], + [f"platformio/framework-arduinoespressif32 @ {conf[CONF_SOURCE]}"], ) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -393,3 +415,10 @@ def copy_files(): CORE.relative_build_path("partitions.csv"), IDF_PARTITIONS_CSV, ) + + dir = os.path.dirname(__file__) + post_build_file = os.path.join(dir, "post_build.py.script") + copy_file_if_changed( + post_build_file, + CORE.relative_build_path("post_build.py"), + ) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index ddf4bf2026..56fd4932b4 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1,3 +1,5 @@ +from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3 + ESP32_BASE_PINS = { "TX": 1, "RX": 3, @@ -259,6 +261,37 @@ ESP32_BOARD_PINS = { "SS": 33, "TX": 17, }, + "featheresp32-s2": { + "SDA": 3, + "SCL": 4, + "SS": 42, + "MOSI": 35, + "SCK": 36, + "MISO": 37, + "A0": 18, + "A1": 17, + "A10": 27, + "A11": 12, + "A12": 13, + "A13": 35, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "LED": 13, + "TX": 39, + "RX": 38, + "T5": 5, + "T8": 8, + "T9": 9, + "T10": 10, + "T11": 11, + "T12": 12, + "T13": 13, + "T14": 14, + "DAC1": 17, + "DAC2": 18, + }, "firebeetle32": {"LED": 2}, "fm-devkit": { "D0": 34, @@ -925,3 +958,125 @@ ESP32_BOARD_PINS = { }, "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 index b82f03bf68..d92b449ee9 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -18,4 +18,12 @@ VARIANTS = [ 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 index 96047df535..6123d83a34 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -6,12 +6,17 @@ #include #include #include +#include #include #if ESP_IDF_VERSION_MAJOR >= 4 #include #endif +#ifdef USE_ARDUINO +#include +#endif + void setup(); void loop(); @@ -21,11 +26,7 @@ void IRAM_ATTR HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t)(esp_timer_get_time() / 1000ULL); } void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { - auto start = (uint64_t) esp_timer_get_time(); - while (((uint64_t) esp_timer_get_time()) - start < us) - ; -} +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { esp_restart(); // restart() doesn't always end execution @@ -33,24 +34,24 @@ void arch_restart() { yield(); } } -void IRAM_ATTR HOT arch_feed_wdt() { -#ifdef USE_ARDUINO -#if CONFIG_ARDUINO_RUNNING_CORE == 0 -#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 - // ESP32 uses "Task Watchdog" which is hooked to the FreeRTOS idle task. - // To cause the Watchdog to be triggered we need to put the current task - // to sleep to get the idle task scheduled. - delay(1); -#endif -#endif -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF -#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 - delay(1); +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 -#endif // USE_ESP_IDF } +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() { diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 93ab17db22..5819943f37 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,4 +1,5 @@ -import logging +from dataclasses import dataclass +from typing import Any from esphome.const import ( CONF_ID, @@ -17,10 +18,24 @@ import esphome.config_validation as cv import esphome.codegen as cg from . import boards -from .const import KEY_BOARD, KEY_ESP32, esp32_ns +from .const import ( + KEY_BOARD, + KEY_ESP32, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32H2, + esp32_ns, +) -_LOGGER = logging.getLogger(__name__) +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) @@ -59,65 +74,61 @@ def _translate_pin(value): return _lookup_pin(value) -_ESP_SDIO_PINS = { - 6: "Flash Clock", - 7: "Flash Data 0", - 8: "Flash Data 1", - 11: "Flash Command", +@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) - if value < 0 or value > 39: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") - if value in _ESP_SDIO_PINS: - raise cv.Invalid( - f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" - ) - if 9 <= value <= 10: - _LOGGER.warning( - "Pin %s (9-10) might already be used by the " - "flash interface in QUAD IO flash mode.", - value, - ) - if value in (20, 24, 28, 29, 30, 31): - # These pins are not exposed in GPIO mux (reason unknown) - # but they're missing from IO_MUX list in datasheet - raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") - return value + 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): - 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] + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in _esp32_validations: + raise cv.Invalid("Unsupported ESP32 variant {variant}") - if is_input: - # All ESP32 pins support input mode - pass - if is_output and 34 <= num <= 39: - raise cv.Invalid( - f"GPIO{num} (34-39) does not support output pin mode.", - [CONF_MODE, CONF_OUTPUT], - ) if is_open_drain and not is_output: raise cv.Invalid( "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] ) - if is_pullup and 34 <= num <= 39: - raise cv.Invalid( - f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP] - ) - if is_pulldown and 34 <= num <= 39: - raise cv.Invalid( - f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] - ) + value = _esp32_validations[variant].usage_validation(value) if CORE.using_arduino: # (input, output, open_drain, pullup, pulldown) supported_modes = { @@ -138,7 +149,6 @@ def validate_supports(value): "This pin mode is not supported on ESP32 for arduino frameworks", [CONF_MODE], ) - return value diff --git a/esphome/components/esp32/gpio_arduino.cpp b/esphome/components/esp32/gpio_arduino.cpp index c4bb21a0aa..ba92894f97 100644 --- a/esphome/components/esp32/gpio_arduino.cpp +++ b/esphome/components/esp32/gpio_arduino.cpp @@ -9,6 +9,22 @@ 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; @@ -43,22 +59,9 @@ void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, g attachInterruptArg(pin_, func, arg, arduino_mode); } + void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { - uint8_t mode; - if (flags == gpio::FLAG_INPUT) { - mode = INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { - mode = OUTPUT; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { - mode = INPUT_PULLUP; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { - mode = INPUT_PULLDOWN; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - mode = OUTPUT_OPEN_DRAIN; - } else { - return; - } - pinMode(pin_, mode); // NOLINT + pinMode(pin_, flags_to_mode(flags)); // NOLINT } std::string ArduinoInternalGPIOPin::dump_summary() const { @@ -101,6 +104,10 @@ void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { } #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 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 index d1853e1f8b..498843ebff 100644 --- a/esphome/components/esp32/gpio_idf.cpp +++ b/esphome/components/esp32/gpio_idf.cpp @@ -10,38 +10,7 @@ static const char *const TAG = "esp32"; bool IDFInternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -struct ISRPinArg { - gpio_num_t pin; - bool inverted; -}; - -ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { - auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; - arg->inverted = inverted_; - return ISRInternalGPIOPin((void *) arg); -} - -void IDFInternalGPIOPin::setup() { - pin_mode(flags_); - gpio_set_drive_capability(pin_, drive_strength_); -} - -void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { - gpio_config_t conf{}; - conf.pin_bit_mask = 1ULL << static_cast(pin_); - conf.mode = flags_to_mode(flags); - conf.pull_up_en = flags & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - conf.pull_down_en = flags & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; - conf.intr_type = GPIO_INTR_DISABLE; - gpio_config(&conf); -} - -bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } - -void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } - -gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { +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; @@ -61,6 +30,18 @@ gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { } } +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) { @@ -99,6 +80,35 @@ std::string IDFInternalGPIOPin::dump_summary() const { 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; @@ -114,6 +124,19 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { 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 diff --git a/esphome/components/esp32/gpio_idf.h b/esphome/components/esp32/gpio_idf.h index a99571cc46..a07d11378a 100644 --- a/esphome/components/esp32/gpio_idf.h +++ b/esphome/components/esp32/gpio_idf.h @@ -18,13 +18,12 @@ class IDFInternalGPIOPin : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; std::string dump_summary() const override; - void detach_interrupt() const override { gpio_intr_disable(pin_); } + 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: - static gpio_mode_t flags_to_mode(gpio::Flags flags); void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; gpio_num_t pin_; diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script new file mode 100644 index 0000000000..7feaf9e8e5 --- /dev/null +++ b/esphome/components/esp32/post_build.py.script @@ -0,0 +1,43 @@ +# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 + +import esptool + +# pylint: disable=E0602 +Import("env") # noqa + + +def esp32_create_combined_bin(source, target, env): + print("Generating combined binary for serial flashing") + app_offset = 0x10000 + + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") + sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + chip = env.get("BOARD_MCU") + flash_size = env.BoardConfig().get("upload.flash_size") + cmd = [ + "--chip", + chip, + "merge_bin", + "-o", + new_file_name, + "--flash_size", + flash_size, + ] + print(" Offset | File") + for section in sections: + sect_adr, sect_file = section.split(" ", 1) + print(f" - {sect_adr} | {sect_file}") + cmd += [sect_adr, sect_file] + + print(f" - {hex(app_offset)} | {firmware_name}") + cmd += [hex(app_offset), firmware_name] + + print() + print(f"Using esptool.py arguments: {' '.join(cmd)}") + print() + esptool.main(cmd) + + +# pylint: disable=E0602 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 96b7e7809e..8c2b67a942 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -76,6 +76,7 @@ class ESP32Preferences : public ESPPreferences { 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; diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f6bab8e6df..955bc8595f 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -57,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(); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 00adc88060..5c4b5bf165 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -95,19 +95,20 @@ bool BLEServer::create_device_characteristics_() { return true; } -BLEService *BLEServer::create_service(const uint8_t *uuid, bool advertise) { +std::shared_ptr BLEServer::create_service(const uint8_t *uuid, bool advertise) { return this->create_service(ESPBTUUID::from_raw(uuid), advertise); } -BLEService *BLEServer::create_service(uint16_t uuid, bool advertise) { +std::shared_ptr BLEServer::create_service(uint16_t uuid, bool advertise) { return this->create_service(ESPBTUUID::from_uint16(uuid), advertise); } -BLEService *BLEServer::create_service(const std::string &uuid, bool advertise) { +std::shared_ptr BLEServer::create_service(const std::string &uuid, bool advertise) { return this->create_service(ESPBTUUID::from_raw(uuid), advertise); } -BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) { +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()); - BLEService *service = new BLEService(uuid, num_handles, inst_id); // NOLINT(cppcoreguidelines-owning-memory) - this->services_.push_back(service); + 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); } @@ -146,12 +147,12 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga break; } - for (auto *service : this->services_) { + for (const auto &service : this->services_) { service->gatts_event_handler(event, gatts_if, param); } } -float BLEServer::get_setup_priority() const { return setup_priority::BLUETOOTH - 10; } +float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 9f7e8b8fc0..d275eeab01 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -11,6 +11,7 @@ #include "esphome/core/preferences.h" #include +#include #ifdef USE_ESP32 @@ -43,10 +44,11 @@ class BLEServer : public Component { void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; } void set_model(const std::string &model) { this->model_ = model; } - BLEService *create_service(const uint8_t *uuid, bool advertise = false); - BLEService *create_service(uint16_t uuid, bool advertise = false); - BLEService *create_service(const std::string &uuid, bool advertise = false); - BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); + 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_; } @@ -74,8 +76,8 @@ class BLEServer : public Component { uint32_t connected_clients_{0}; std::map clients_; - std::vector services_; - BLEService *device_information_service_; + std::vector> services_; + std::shared_ptr device_information_service_; std::vector service_components_; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index e3d52f345a..e647b74a8f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -18,7 +18,6 @@ from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["xiaomi_ble", "ruuvi_ble"] CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 65749f5124..084dab4c84 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -40,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(); @@ -481,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; @@ -522,7 +525,7 @@ 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:"); @@ -535,10 +538,10 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e 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 1308119df5..9ff2a5a861 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -97,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); @@ -121,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; @@ -171,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; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 7f3aebe238..d42d4f5de3 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,10 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( - CONF_DISABLED_BY_DEFAULT, CONF_FREQUENCY, CONF_ID, - CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, @@ -17,8 +15,11 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.cpp_helpers import setup_entity -DEPENDENCIES = ["esp32", "api"] +DEPENDENCIES = ["esp32"] + +AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) @@ -58,16 +59,17 @@ 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( +CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ESP32Camera), - cv.Required(CONF_NAME): cv.string, - cv.Optional(CONF_DISABLED_BY_DEFAULT, default=False): cv.boolean, cv.Required(CONF_DATA_PINS): cv.All( [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) ), @@ -105,6 +107,9 @@ CONFIG_SCHEMA = cv.Schema( 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) @@ -119,6 +124,9 @@ SETTERS = { 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", @@ -127,8 +135,8 @@ SETTERS = { async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) - cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + var = cg.new_Pvariable(config[CONF_ID]) + await setup_entity(var, config) await cg.register_component(var, config) for key, setter in SETTERS.items(): @@ -147,9 +155,7 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) 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 babfda4113..7d11f98d09 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -26,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_); @@ -45,9 +48,7 @@ void ESP32Camera::dump_config() { auto conf = this->config_; ESP_LOGCONFIG(TAG, "ESP32 Camera:"); ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); -#ifdef USE_ARDUINO - ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound())); -#endif // USE_ARDUINO + ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_)); 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); @@ -110,9 +111,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); @@ -132,6 +133,13 @@ void ESP32Camera::loop() { this->current_image_.reset(); } + // request idle image every idle_update_interval + const uint32_t now = millis(); + if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { + this->last_idle_request_ = now; + this->request_image(IDLE); + } + // Check if we should fetch a new image if (!this->has_requested_image_()) return; @@ -139,7 +147,6 @@ void ESP32Camera::loop() { // image is still in use return; } - const uint32_t now = millis(); if (now - this->last_update_ <= this->max_update_interval_) return; @@ -156,12 +163,12 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } - this->current_image_ = std::make_shared(fb); + this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); ESP_LOGD(TAG, "Got Image: len=%u", fb->len); this->new_image_callback_.call(this->current_image_); this->last_update_ = now; - this->single_requester_ = false; + this->single_requesters_ = 0; } void ESP32Camera::framebuffer_task(void *pv) { while (true) { @@ -185,6 +192,7 @@ ESP32Camera::ESP32Camera(const std::string &name) : EntityBase(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]; @@ -248,29 +256,18 @@ 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; } float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } uint32_t ESP32Camera::hash_base() { return 3010542557UL; } -void ESP32Camera::request_image() { this->single_requester_ = true; } -void ESP32Camera::request_stream() { this->last_stream_request_ = millis(); } -bool ESP32Camera::has_requested_image_() const { - if (this->single_requester_) - // single request - return true; - - uint32_t now = millis(); - if (now - this->last_stream_request_ < 5000) - // stream request - return true; - - if (this->idle_update_interval_ != 0 && now - this->last_update_ > this->idle_update_interval_) - // idle update - return true; - - return false; -} +void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= 1 << requester; } +void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= 1 << requester; } +void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1 << requester); } +bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; } bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { this->max_update_interval_ = max_update_interval; @@ -299,7 +296,10 @@ uint8_t *CameraImageReader::peek_data_buffer() { return this->image_->get_data_b camera_fb_t *CameraImage::get_raw_buffer() { return this->buffer_; } uint8_t *CameraImage::get_data_buffer() { return this->buffer_->buf; } size_t CameraImage::get_data_length() { return this->buffer_->len; } -CameraImage::CameraImage(camera_fb_t *buffer) : buffer_(buffer) {} +bool CameraImage::was_requested_by(CameraRequester requester) const { + return (this->requesters_ & (1 << requester)) != 0; +} +CameraImage::CameraImage(camera_fb_t *buffer, uint8_t requesters) : buffer_(buffer), requesters_(requesters) {} } // namespace esp32_camera } // namespace esphome diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index d0445607a4..b2670078f3 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -14,15 +14,19 @@ namespace esp32_camera { class ESP32Camera; +enum CameraRequester { IDLE, API_REQUESTER, WEB_REQUESTER }; + class CameraImage { public: - CameraImage(camera_fb_t *buffer); + CameraImage(camera_fb_t *buffer, uint8_t requester); camera_fb_t *get_raw_buffer(); uint8_t *get_data_buffer(); size_t get_data_length(); + bool was_requested_by(CameraRequester requester) const; protected: camera_fb_t *buffer_; + uint8_t requesters_; }; class CameraImageReader { @@ -54,6 +58,7 @@ enum ESP32CameraFrameSize { 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); @@ -66,6 +71,9 @@ class ESP32Camera : public Component, public EntityBase { 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); @@ -77,8 +85,9 @@ class ESP32Camera : public Component, public EntityBase { void dump_config() override; void add_image_callback(std::function)> &&f); float get_setup_priority() const override; - void request_stream(); - void request_image(); + void start_stream(CameraRequester requester); + void stop_stream(CameraRequester requester); + void request_image(CameraRequester requester); protected: uint32_t hash_base() override; @@ -90,6 +99,9 @@ class ESP32Camera : public Component, public EntityBase { 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}; @@ -97,13 +109,14 @@ class ESP32Camera : public Component, public EntityBase { esp_err_t init_error_{ESP_OK}; std::shared_ptr current_image_; - uint32_t last_stream_request_{0}; - bool single_requester_{false}; + uint8_t single_requesters_{0}; + uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; CallbackManager)> new_image_callback_; uint32_t max_update_interval_{1000}; uint32_t idle_update_interval_{15000}; + uint32_t last_idle_request_{0}; uint32_t last_update_{0}; }; 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..39b110bc85 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -0,0 +1,243 @@ +#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 = 5000; +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_ && image->was_requested_by(esp32_camera::WEB_REQUESTER)) { + 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; + + esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::WEB_REQUESTER); + + while (res == ESP_OK && this->running_) { + 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)); + } + + esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER); + + 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; + + esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::WEB_REQUESTER); + + 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_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_can/__init__.py b/esphome/components/esp32_can/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py new file mode 100644 index 0000000000..7761418c6a --- /dev/null +++ b/esphome/components/esp32_can/canbus.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import canbus +from esphome.const import CONF_ID, CONF_RX_PIN, CONF_TX_PIN +from esphome.components.canbus import CanbusComponent, CanSpeed, CONF_BIT_RATE + +CODEOWNERS = ["@Sympatron"] +DEPENDENCIES = ["esp32"] + +esp32_can_ns = cg.esphome_ns.namespace("esp32_can") +esp32_can = esp32_can_ns.class_("ESP32Can", CanbusComponent) + +# Currently the driver only supports a subset of the bit rates defined in canbus +CAN_SPEEDS = { + "50KBPS": CanSpeed.CAN_50KBPS, + "100KBPS": CanSpeed.CAN_100KBPS, + "125KBPS": CanSpeed.CAN_125KBPS, + "250KBPS": CanSpeed.CAN_250KBPS, + "500KBPS": CanSpeed.CAN_500KBPS, + "1000KBPS": CanSpeed.CAN_1000KBPS, +} + +CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(esp32_can), + cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True), + cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await canbus.register_canbus(var, config) + + cg.add(var.set_rx(config[CONF_RX_PIN])) + cg.add(var.set_tx(config[CONF_TX_PIN])) diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp new file mode 100644 index 0000000000..baae683988 --- /dev/null +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -0,0 +1,123 @@ +#ifdef USE_ESP32 +#include "esp32_can.h" +#include "esphome/core/log.h" + +#include + +// WORKAROUND, because CAN_IO_UNUSED is just defined as (-1) in this version +// of the framework which does not work with -fpermissive +#undef CAN_IO_UNUSED +#define CAN_IO_UNUSED ((gpio_num_t) -1) + +namespace esphome { +namespace esp32_can { + +static const char *const TAG = "esp32_can"; + +static bool get_bitrate(canbus::CanSpeed bitrate, can_timing_config_t *t_config) { + switch (bitrate) { + case canbus::CAN_50KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_50KBITS(); + return true; + case canbus::CAN_100KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_100KBITS(); + return true; + case canbus::CAN_125KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_125KBITS(); + return true; + case canbus::CAN_250KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_250KBITS(); + return true; + case canbus::CAN_500KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_500KBITS(); + return true; + case canbus::CAN_1000KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_1MBITS(); + return true; + default: + return false; + } +} + +bool ESP32Can::setup_internal() { + can_general_config_t g_config = + CAN_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, CAN_MODE_NORMAL); + can_filter_config_t f_config = CAN_FILTER_CONFIG_ACCEPT_ALL(); + can_timing_config_t t_config; + + if (!get_bitrate(this->bit_rate_, &t_config)) { + // invalid bit rate + this->mark_failed(); + return false; + } + + // Install CAN driver + if (can_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + // Failed to install driver + this->mark_failed(); + return false; + } + + // Start CAN driver + if (can_start() != ESP_OK) { + // Failed to start driver + this->mark_failed(); + return false; + } + return true; +} + +canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAILTX; + } + + uint32_t flags = CAN_MSG_FLAG_NONE; + if (frame->use_extended_id) { + flags |= CAN_MSG_FLAG_EXTD; + } + if (frame->remote_transmission_request) { + flags |= CAN_MSG_FLAG_RTR; + } + + can_message_t message = { + .flags = flags, + .identifier = frame->can_id, + .data_length_code = frame->can_data_length_code, + }; + if (!frame->remote_transmission_request) { + memcpy(message.data, frame->data, frame->can_data_length_code); + } + + if (can_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) { + return canbus::ERROR_OK; + } else { + return canbus::ERROR_ALLTXBUSY; + } +} + +canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { + can_message_t message; + + if (can_receive(&message, 0) != ESP_OK) { + return canbus::ERROR_NOMSG; + } + + frame->can_id = message.identifier; + frame->use_extended_id = message.flags & CAN_MSG_FLAG_EXTD; + frame->remote_transmission_request = message.flags & CAN_MSG_FLAG_RTR; + frame->can_data_length_code = message.data_length_code; + + if (!frame->remote_transmission_request) { + size_t dlc = + message.data_length_code < canbus::CAN_MAX_DATA_LENGTH ? message.data_length_code : canbus::CAN_MAX_DATA_LENGTH; + memcpy(frame->data, message.data, dlc); + } + + return canbus::ERROR_OK; +} + +} // namespace esp32_can +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h new file mode 100644 index 0000000000..a428834f65 --- /dev/null +++ b/esphome/components/esp32_can/esp32_can.h @@ -0,0 +1,29 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/canbus/canbus.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace esp32_can { + +class ESP32Can : public canbus::Canbus { + public: + void set_rx(int rx) { rx_ = rx; } + void set_tx(int tx) { tx_ = tx; } + ESP32Can(){}; + + protected: + bool setup_internal() override; + canbus::Error send_message(struct canbus::CanFrame *frame) override; + canbus::Error read_message(struct canbus::CanFrame *frame) override; + + int rx_{-1}; + int tx_{-1}; +}; + +} // namespace esp32_can +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index e8429790ea..978de4fee5 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -4,7 +4,7 @@ from esphome.components import binary_sensor, output, esp32_ble_server from esphome.const import CONF_BLE_SERVER_ID, CONF_ID -AUTO_LOAD = ["binary_sensor", "output", "improv", "esp32_ble_server"] +AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] @@ -55,6 +55,7 @@ async def to_code(config): 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])) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index faa9ab7df6..788e7a9460 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -11,6 +11,7 @@ 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; } @@ -124,8 +125,13 @@ void ESP32ImprovComponent::loop() { this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); - std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; - std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url}); + 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(); @@ -213,7 +219,7 @@ void ESP32ImprovComponent::dump_config() { void ESP32ImprovComponent::process_incoming_data_() { uint8_t length = this->incoming_data_[1]; - ESP_LOGD(TAG, "Processing bytes - %s", hexencode(this->incoming_data_).c_str()); + 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_); diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 53cda5f399..45639f2f63 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -1,9 +1,8 @@ #pragma once #include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/esp32_ble_server/ble_server.h" #include "esphome/components/esp32_ble_server/ble_characteristic.h" -#include "esphome/components/improv/improv.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" @@ -12,6 +11,8 @@ #ifdef USE_ESP32 +#include + namespace esphome { namespace esp32_improv { @@ -48,7 +49,7 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { std::vector incoming_data_; wifi::WiFiAP connecting_sta_; - BLEService *service_; + std::shared_ptr service_; BLECharacteristic *status_; BLECharacteristic *error_; BLECharacteristic *rpc_; diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index cb72820900..b225ae1a8a 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "esp32_touch.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -93,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"); } @@ -125,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) { diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 93a461ba1f..7182042770 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,9 +1,11 @@ import logging +import os from esphome.const import ( CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_FRAMEWORK, + CONF_SOURCE, CONF_VERSION, KEY_CORE, KEY_FRAMEWORK_VERSION, @@ -13,6 +15,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv import esphome.codegen as cg +from esphome.helpers import copy_file_if_changed from .const import CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, esp8266_ns from .boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS @@ -31,7 +34,7 @@ def set_core_data(config): CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "esp8266" CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( - config[CONF_FRAMEWORK][CONF_VERSION_HINT] + config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP8266][KEY_BOARD] = config[CONF_BOARD] return config @@ -62,75 +65,68 @@ RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 7, 4) # The platformio/espressif8266 version to use for arduino 2 framework versions # - https://github.com/platformio/platform-espressif8266/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 -ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 2) +ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3) # for arduino 3 framework versions -ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 0, 2) +ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": ("https://github.com/esp8266/Arduino.git", cv.Version(3, 0, 2)), - "latest": ("", cv.Version(3, 0, 2)), - "recommended": ( - _format_framework_arduino_version(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION), - RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, - ), + "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), } - ver_value = value[CONF_VERSION] - default_ver_hint = None - if ver_value.lower() in lookups: - default_ver_hint = str(lookups[ver_value.lower()][1]) - ver_value = lookups[ver_value.lower()][0] + + 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: - with cv.suppress_invalid(): - ver = cv.Version.parse(cv.version_number(value)) - if ver <= cv.Version(2, 4, 1): - ver_value = f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - elif ver <= cv.Version(2, 6, 2): - ver_value = f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - else: - ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - default_ver_hint = str(ver) + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) - value[CONF_VERSION] = ver_value + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - if CONF_VERSION_HINT not in value and default_ver_hint is None: - raise cv.Invalid("Needs a version hint to understand the framework version") - - ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) - value[CONF_VERSION_HINT] = ver_hint_s - plat_ver = value.get(CONF_PLATFORM_VERSION) - - if plat_ver is None: - ver_hint = cv.Version.parse(ver_hint_s) - if ver_hint >= cv.Version(3, 0, 0): - plat_ver = ARDUINO_3_PLATFORM_VERSION - elif ver_hint >= cv.Version(2, 5, 0): - plat_ver = ARDUINO_2_PLATFORM_VERSION + 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: - plat_ver = cv.Version(1, 8, 0) - value[CONF_PLATFORM_VERSION] = str(plat_ver) + platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) + value[CONF_PLATFORM_VERSION] = platform_version - if cv.Version.parse(ver_hint_s) != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( - "The selected arduino framework version is not the recommended one" - ) - _LOGGER.warning( - "If there are connectivity or build issues please remove the manual version" + "The selected Arduino framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." ) return value -CONF_VERSION_HINT = "version_hint" +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_VERSION_HINT): cv.version_number, - cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, } ), _arduino_check_versions, @@ -157,20 +153,23 @@ CONFIG_SCHEMA = cv.All( 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") + + cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") 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_VERSION]}"], - ) - cg.add_platformio_option( - "platform", f"platformio/espressif8266 @ {conf[CONF_PLATFORM_VERSION]}" + [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], ) # Default for platformio is LWIP2_LOW_MEMORY with: @@ -215,3 +214,14 @@ async def to_code(config): if ld_script is not None: cg.add_platformio_option("board_build.ldscript", ld_script) + + +# Called by writer.py +def copy_files(): + + dir = os.path.dirname(__file__) + post_build_file = os.path.join(dir, "post_build.py.script") + copy_file_if_changed( + post_build_file, + CORE.relative_build_path("post_build.py"), + ) diff --git a/esphome/components/esp8266/boards.py b/esphome/components/esp8266/boards.py index c49aae4ffa..410e934615 100644 --- a/esphome/components/esp8266/boards.py +++ b/esphome/components/esp8266/boards.py @@ -206,61 +206,3 @@ ESP8266_BOARD_PINS = { "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, } - -FLASH_SIZE_1_MB = 2 ** 20 -FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 -FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB -FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB -FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB - -ESP8266_FLASH_SIZES = { - "d1": FLASH_SIZE_4_MB, - "d1_mini": FLASH_SIZE_4_MB, - "d1_mini_lite": FLASH_SIZE_1_MB, - "d1_mini_pro": FLASH_SIZE_16_MB, - "esp01": FLASH_SIZE_512_KB, - "esp01_1m": FLASH_SIZE_1_MB, - "esp07": FLASH_SIZE_4_MB, - "esp12e": FLASH_SIZE_4_MB, - "esp210": FLASH_SIZE_4_MB, - "esp8285": FLASH_SIZE_1_MB, - "esp_wroom_02": FLASH_SIZE_2_MB, - "espduino": FLASH_SIZE_4_MB, - "espectro": FLASH_SIZE_4_MB, - "espino": FLASH_SIZE_4_MB, - "espinotee": FLASH_SIZE_4_MB, - "espmxdevkit": FLASH_SIZE_1_MB, - "espresso_lite_v1": FLASH_SIZE_4_MB, - "espresso_lite_v2": FLASH_SIZE_4_MB, - "gen4iod": FLASH_SIZE_512_KB, - "heltec_wifi_kit_8": FLASH_SIZE_4_MB, - "huzzah": FLASH_SIZE_4_MB, - "inventone": FLASH_SIZE_4_MB, - "modwifi": FLASH_SIZE_2_MB, - "nodemcu": FLASH_SIZE_4_MB, - "nodemcuv2": FLASH_SIZE_4_MB, - "oak": FLASH_SIZE_4_MB, - "phoenix_v1": FLASH_SIZE_4_MB, - "phoenix_v2": FLASH_SIZE_4_MB, - "sonoff_basic": FLASH_SIZE_1_MB, - "sonoff_s20": FLASH_SIZE_1_MB, - "sonoff_sv": FLASH_SIZE_1_MB, - "sonoff_th": FLASH_SIZE_1_MB, - "sparkfunBlynk": FLASH_SIZE_4_MB, - "thing": FLASH_SIZE_512_KB, - "thingdev": FLASH_SIZE_512_KB, - "wifi_slot": FLASH_SIZE_1_MB, - "wifiduino": FLASH_SIZE_4_MB, - "wifinfo": FLASH_SIZE_1_MB, - "wio_link": FLASH_SIZE_4_MB, - "wio_node": FLASH_SIZE_4_MB, - "xinabox_cw01": FLASH_SIZE_4_MB, -} - -ESP8266_LD_SCRIPTS = { - FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), - FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), - FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), - FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), - FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), -} diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index b78600e7a3..828d71a3bd 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -12,7 +12,7 @@ void IRAM_ATTR HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } +void 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 @@ -20,6 +20,7 @@ void arch_restart() { yield(); } } +void arch_init() {} void IRAM_ATTR HOT arch_feed_wdt() { ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance) } @@ -27,7 +28,7 @@ void IRAM_ATTR HOT arch_feed_wdt() { uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } -uint32_t arch_get_cpu_cycle_count() { +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; } diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index cb703c18e1..a24f217756 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -8,6 +8,29 @@ 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; @@ -43,21 +66,7 @@ void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Int attachInterruptArg(pin_, func, arg, arduino_mode); } void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { - uint8_t mode; - if (flags == gpio::FLAG_INPUT) { - mode = INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { - mode = OUTPUT; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { - mode = INPUT_PULLUP; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { - mode = INPUT_PULLDOWN_16; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - mode = OUTPUT_OPEN_DRAIN; - } else { - return; - } - pinMode(pin_, mode); // NOLINT + pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } std::string ESP8266GPIOPin::dump_summary() const { @@ -90,6 +99,10 @@ 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 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 0ebfbd6f69..fa5c94dff5 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -107,9 +107,9 @@ def validate_supports(value): raise cv.Invalid( "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] ) - if is_pullup and num == 0: + if is_pullup and num == 16: raise cv.Invalid( - "GPIO Pin 0 does not support pullup pin mode. " + "GPIO Pin 16 does not support pullup pin mode. " "Please choose another pin.", [CONF_MODE, CONF_PULLUP], ) diff --git a/esphome/components/esp8266/post_build.py.script b/esphome/components/esp8266/post_build.py.script new file mode 100644 index 0000000000..4dab1cbd27 --- /dev/null +++ b/esphome/components/esp8266/post_build.py.script @@ -0,0 +1,15 @@ +import shutil + +# pylint: disable=E0602 +Import("env") # noqa + + +def esp8266_copy_factory_bin(source, target, env): + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") + + shutil.copyfile(firmware_name, new_file_name) + + +# pylint: disable=E0602 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_factory_bin) # noqa diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 041736943b..a8f8bd0d41 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -55,7 +55,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { extern "C" uint32_t _SPIFFS_end; // NOLINT -static const uint32_t get_esp8266_flash_sector() { +static uint32_t get_esp8266_flash_sector() { union { uint32_t *ptr; uint32_t uint; @@ -63,7 +63,7 @@ static const uint32_t get_esp8266_flash_sector() { 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; } +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; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d55db0a7d8..384a31ed2f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -168,7 +168,9 @@ void EthernetComponent::start_connect_() { esp_err_t err; err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); - ESPHL_ERROR_CHECK(err, "ETH set hostname error"); + if (err != ERR_OK) { + ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); + } tcpip_adapter_ip_info_t info; if (this->manual_ip_.has_value()) { @@ -182,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"); diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 110a8d95ed..d0153f6104 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( CONF_TYPE, CONF_EXTERNAL_COMPONENTS, CONF_PATH, + CONF_USERNAME, + CONF_PASSWORD, ) from esphome.core import CORE from esphome import git, loader @@ -27,6 +29,8 @@ 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, @@ -43,19 +47,27 @@ def validate_source_shorthand(value): # 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\-_.\./]+))?", + 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 or in expected github://username/name[@branch-or-tag] format!" + "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" ) - 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) + 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) @@ -91,6 +103,8 @@ def _process_git_config(config: dict, refresh) -> str: 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(): diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 81597f3466..2ee5782ff6 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -32,7 +32,7 @@ void EZOSensor::update() { } void EZOSensor::loop() { - uint8_t buf[20]; + 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_); @@ -74,7 +74,12 @@ void EZOSensor::loop() { if (buf[0] != 1) return; - float val = strtof((char *) &buf[1], nullptr); + // 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); } diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index f359a10348..4ae670743d 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_LAST_FINGER_ID, CONF_SECURITY_LEVEL, CONF_STATUS, + ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT, ICON_ACCOUNT_CHECK, ICON_DATABASE, @@ -26,30 +27,36 @@ CONFIG_SCHEMA = cv.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, ), } ) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index a9daad4ab9..daff89e0a6 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -86,7 +86,7 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo // Look back in trace data to best-fit into local range float mx = NAN; float mn = NAN; - for (int16_t i = 0; i < this->width_; i++) { + for (uint32_t i = 0; i < this->width_; i++) { for (auto *trace : traces_) { float v = trace->get_tracedata()->get_value(i); if (!std::isnan(v)) { @@ -132,7 +132,7 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo if (!std::isnan(this->gridspacing_y_)) { for (int y = yn; y <= ym; y++) { int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn))); - for (int x = 0; x < this->width_; x += 2) { + for (uint32_t x = 0; x < this->width_; x += 2) { buff->draw_pixel_at(x_offset + x, y_offset + py, color); } } @@ -147,7 +147,7 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo ESP_LOGW(TAG, "Graphing reducing x-scale to prevent too many gridlines"); } for (int i = 0; i <= n; i++) { - for (int y = 0; y < this->height_; y += 2) { + 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); } } @@ -158,14 +158,14 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo for (auto *trace : traces_) { Color c = trace->get_line_color(); uint16_t thick = trace->get_line_thickness(); - for (int16_t i = 0; i < this->width_; i++) { + 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 (int16_t t = 0; t < thick; t++) { + for (uint16_t t = 0; t < thick; t++) { buff->draw_pixel_at(x_offset + x, y_offset + y + t, c); } } @@ -179,8 +179,8 @@ void GraphLegend::init(Graph *g) { parent_ = g; // Determine maximum expected text and value width / height - int txtw = 0, txtos = 0, txtbl = 0, txth = 0; - int valw = 0, valos = 0, valbl = 0, valh = 0; + int txtw = 0, txth = 0; + int valw = 0, valh = 0; int lt = 0; for (auto *trace : g->traces_) { std::string txtstr = trace->get_name(); @@ -320,7 +320,7 @@ void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_ if (legend_->lines_) { uint16_t thick = trace->get_line_thickness(); - for (int16_t i = 0; i < legend_->x0_ * 4 / 3; i++) { + 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, 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/sensor.py b/esphome/components/havells_solar/sensor.py index 3ec12d5b83..d7c8d544f9 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -93,13 +93,12 @@ PV_SENSORS = { CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, + 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, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), } @@ -135,7 +134,6 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 60e8943e67..7186578a22 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -21,7 +21,9 @@ void HDC1080Component::setup() { }; if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { - this->mark_failed(); + // as instruction is same as powerup defaults (for now), interpret as warning if this fails + ESP_LOGW(TAG, "HDC1080 initial config instruction error"); + this->status_set_warning(); return; } } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 36e56aa5da..744ef5e527 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -30,6 +30,7 @@ PROTOCOLS = { "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, @@ -108,7 +109,9 @@ def to_code(config): cg.add(var.set_protocol(config[CONF_PROTOCOL])) cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) - cg.add(var.set_max_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add(var.set_min_temperature(config[CONF_MAX_TEMPERATURE])) + cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) + cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.15") + # 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 index 8d9fc962c0..ad3731b955 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -25,6 +25,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {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 @@ -61,6 +62,19 @@ void HeatpumpIRClimate::setup() { } 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() { @@ -171,8 +185,7 @@ void HeatpumpIRClimate::transmit_state() { temperature_cmd = (uint8_t) clamp(this->target_temperature, this->min_temperature_, this->max_temperature_); - IRSenderESPHome esp_sender(0, this->transmitter_); - + 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); } diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index e2d2b45dc4..18d9b5040f 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -25,6 +25,7 @@ enum Protocol { PROTOCOL_GREE, PROTOCOL_GREEYAA, PROTOCOL_GREEYAN, + PROTOCOL_GREEYAC, PROTOCOL_HISENSE_AUD, PROTOCOL_HITACHI, PROTOCOL_HYUNDAI, diff --git a/esphome/components/heatpumpir/ir_sender_esphome.h b/esphome/components/heatpumpir/ir_sender_esphome.h index 24e8ba9883..7546d990ea 100644 --- a/esphome/components/heatpumpir/ir_sender_esphome.h +++ b/esphome/components/heatpumpir/ir_sender_esphome.h @@ -11,8 +11,8 @@ namespace heatpumpir { class IRSenderESPHome : public IRSender { public: - IRSenderESPHome(uint8_t pin, remote_transmitter::RemoteTransmitterComponent *transmitter) - : IRSender(pin), transmit_(transmitter->transmit()){}; + 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; diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 067ea39d07..7702baf312 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -299,9 +299,7 @@ bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) { 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 & 0x7) == 0x0) { - this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - } else if ((swing_modeh & 0x3) == 0x3) { + if ((swing_modeh & 0x3) == 0x3) { this->swing_mode = climate::CLIMATE_SWING_OFF; } else { this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 2e5423a37a..713bc0be25 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -300,9 +300,7 @@ bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) { HITACHI_AC424_SWINGH_SIZE); ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC424_SWINGH_BYTE], swing_modeh); - if ((swing_modeh & 0x7) == 0x0) { - this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - } else if ((swing_modeh & 0x3) == 0x3) { + if ((swing_modeh & 0x3) == 0x3) { this->swing_mode = climate::CLIMATE_SWING_OFF; } else { this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h index fb41b921d9..42d900a262 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -1,6 +1,5 @@ #pragma once -#ifdef USE_ARDUINO #include namespace esphome { @@ -13,5 +12,3 @@ class AbstractAQICalculator { } // namespace hm3301 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 1410eac72b..08d1dc2921 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "abstract_aqi_calculator.h" namespace esphome { @@ -33,7 +31,7 @@ class AQICalculator : public AbstractAQICalculator { int conc_lo = array[grid_index][0]; int conc_hi = array[grid_index][1]; - return ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + 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]) { @@ -48,5 +46,3 @@ class AQICalculator : public AbstractAQICalculator { } // namespace hm3301 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h index 3c6f9709b6..55608b6e51 100644 --- a/esphome/components/hm3301/aqi_calculator_factory.h +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "caqi_calculator.h" #include "aqi_calculator.h" @@ -29,5 +27,3 @@ class AQICalculatorFactory { } // namespace hm3301 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h index 51158454d0..1ec61f2416 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/hm3301/caqi_calculator.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/log.h" #include "abstract_aqi_calculator.h" @@ -37,9 +35,7 @@ class CAQICalculator : public AbstractAQICalculator { int conc_lo = array[grid_index][0]; int conc_hi = array[grid_index][1]; - int aqi = ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; - - return aqi; + 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]) { @@ -54,5 +50,3 @@ class CAQICalculator : public AbstractAQICalculator { } // namespace hm3301 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 759157f330..a2bef2a01d 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "esphome/core/log.h" #include "hm3301.h" @@ -14,9 +12,8 @@ static const uint8_t PM_10_0_VALUE_INDEX = 7; void HM3301Component::setup() { ESP_LOGCONFIG(TAG, "Setting up HM3301..."); - hm3301_ = make_unique(); - 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; } @@ -38,7 +35,7 @@ void HM3301Component::dump_config() { 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; @@ -87,8 +84,6 @@ void HM3301Component::update() { 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++) { @@ -104,5 +99,3 @@ uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) { } // namespace hm3301 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 61bbf7e4ab..e13ffa466e 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -1,17 +1,15 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" #include "aqi_calculator_factory.h" -#include - namespace esphome { namespace hm3301 { +static const uint8_t SELECT_COMM_CMD = 0X88; + class HM3301Component : public PollingComponent, public i2c::I2CDevice { public: HM3301Component() = default; @@ -29,9 +27,12 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - std::unique_ptr 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]; @@ -43,12 +44,9 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { 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); }; } // namespace hm3301 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 976a0488e1..8e9ee4c6fb 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -84,7 +84,6 @@ CONFIG_SCHEMA = cv.All( .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x40)), _validate, - cv.only_with_arduino, ) @@ -109,6 +108,3 @@ async def to_code(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])) - - # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 - cg.add_library("seeed-studio/Grove - Laser PM2.5 Sensor HM3301", "1.0.3") diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 73e7472dcf..9d8701079e 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -114,8 +114,8 @@ CONFIG_SCHEMA = ( 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] diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index b6f2acdbe4..f5e73c8854 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "homeassistant.sensor"; void HomeassistantSensor::setup() { api::global_api_server->subscribe_home_assistant_state( this->entity_id_, this->attribute_, [this](const std::string &state) { - auto val = parse_float(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); diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index cf6c9eea65..bd1c82c96b 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -44,7 +44,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() { if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && this->buffer_.back() == static_cast(ASCII_CR)) { - int millimeters = strtol(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2).c_str(), nullptr, 10); + 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); diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 6e249c4247..e044e5fece 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -96,6 +96,8 @@ async def to_code(config): 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) @@ -170,7 +172,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): if CONF_JSON in config: json_ = config[CONF_JSON] if isinstance(json_, Lambda): - args_ = args + [(cg.JsonObjectRef, "root")] + args_ = args + [(cg.JsonObject, "root")] lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 309977a915..a80d095835 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -115,8 +115,11 @@ void HttpRequestComponent::close() { } const char *HttpRequestComponent::get_string() { - static const String STR = this->client_.getString(); - return STR.c_str(); + // The static variable is here because HTTPClient::getString() returns a String on ESP32, and we need something to + // to keep a buffer alive. + static std::string str; + str = this->client_.getString().c_str(); + return str.c_str(); } } // namespace http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 9cc027b58d..a38bdf9c95 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -78,7 +78,7 @@ template class HttpRequestSendAction : public Action { void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } - void set_json(std::function json_func) { this->json_func_ = json_func; } + void set_json(std::function json_func) { this->json_func_ = json_func; } void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } @@ -118,17 +118,17 @@ template class HttpRequestSendAction : public Action { } protected: - void encode_json_(Ts... x, JsonObject &root) { + void encode_json_(Ts... x, JsonObject root) { for (const auto &item : this->json_) { auto val = item.second; root[item.first] = val.value(x...); } } - void encode_json_func_(Ts... x, JsonObject &root) { this->json_func_(x..., root); } + void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; std::map> headers_{}; std::map> json_{}; - std::function json_func_{nullptr}; + std::function json_func_{nullptr}; std::vector response_triggers_; }; diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index b53284ae3f..a38ec73019 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -9,8 +9,8 @@ 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() { diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 7ee4cdd811..50a0b3ae50 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,6 +1,7 @@ #pragma once #include "i2c_bus.h" +#include "esphome/core/helpers.h" #include "esphome/core/optional.h" #include #include @@ -32,16 +33,8 @@ class I2CRegister { // like ntohs/htons but without including networking headers. // ("i2c" byte order is big-endian) -inline uint16_t i2ctohs(uint16_t i2cshort) { - union { - uint16_t x; - uint8_t y[2]; - } conv; - conv.x = i2cshort; - return ((uint16_t) conv.y[0] << 8) | ((uint16_t) conv.y[1] << 0); -} - -inline uint16_t htoi2cs(uint16_t hostshort) { return i2ctohs(hostshort); } +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: diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index cb00260f43..71f6b1d15b 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include namespace esphome { namespace i2c { @@ -40,6 +42,20 @@ class I2CBus { 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 diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 4afabbfa53..b605928692 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -2,6 +2,7 @@ #include "i2c_bus_arduino.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include #include @@ -24,9 +25,13 @@ void ArduinoI2CBus::setup() { wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer) #endif - wire_->begin(sda_pin_, scl_pin_); + wire_->begin(static_cast(sda_pin_), static_cast(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:"); @@ -45,22 +50,20 @@ void ArduinoI2CBus::dump_config() { break; } if (this->scan_) { - ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); - uint8_t found = 0; - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - if (err == ERROR_OK) { - ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); - found++; - } else if (err == ERROR_UNKNOWN) { - ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); - } - } - if (found == 0) { + ESP_LOGI(TAG, "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 diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 82f043ef7d..f4151e4f37 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -34,7 +34,6 @@ class ArduinoI2CBus : public I2CBus, public Component { protected: TwoWire *wire_; - bool scan_; uint8_t sda_pin_; uint8_t scl_pin_; uint32_t frequency_; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index f7ecfe5f7c..109c3f890d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -3,6 +3,7 @@ #include "i2c_bus_esp_idf.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include namespace esphome { @@ -37,6 +38,10 @@ void IDFI2CBus::setup() { 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:"); @@ -55,23 +60,20 @@ void IDFI2CBus::dump_config() { break; } if (this->scan_) { - ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); - uint8_t found = 0; - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - - if (err == ERROR_OK) { - ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); - found++; - } else if (err == ERROR_UNKNOWN) { - ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); - } - } - if (found == 0) { + ESP_LOGI(TAG, "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 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 13d996dbd8..d4b0626467 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -36,7 +36,6 @@ class IDFI2CBus : public I2CBus, public Component { protected: i2c_port_t port_; - bool scan_; uint8_t sda_pin_; bool sda_pullup_enabled_; uint8_t scl_pin_; diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index ab5586fa28..a24f0bbb64 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -86,8 +86,8 @@ void ILI9341Display::update() { void ILI9341Display::display_() { // we will only update the changed window to the display - int w = this->x_high_ - this->x_low_ + 1; - int h = this->y_high_ - this->y_low_ + 1; + 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_(); diff --git a/esphome/components/improv/__init__.py b/esphome/components/improv/__init__.py deleted file mode 100644 index b1de57df8f..0000000000 --- a/esphome/components/improv/__init__.py +++ /dev/null @@ -1 +0,0 @@ -CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/improv/improv.cpp b/esphome/components/improv/improv.cpp deleted file mode 100644 index 4f6ed7702d..0000000000 --- a/esphome/components/improv/improv.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "improv.h" - -namespace improv { - -ImprovCommand parse_improv_data(const std::vector &data) { - return parse_improv_data(data.data(), data.size()); -} - -ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { - Command command = (Command) data[0]; - uint8_t data_length = data[1]; - - if (data_length != length - 3) { - return {.command = UNKNOWN}; - } - - uint8_t checksum = data[length - 1]; - - uint32_t calculated_checksum = 0; - for (uint8_t i = 0; i < length - 1; i++) { - calculated_checksum += data[i]; - } - - if ((uint8_t) calculated_checksum != checksum) { - return {.command = BAD_CHECKSUM}; - } - - if (command == WIFI_SETTINGS) { - uint8_t ssid_length = data[2]; - uint8_t ssid_start = 3; - size_t ssid_end = ssid_start + ssid_length; - - uint8_t pass_length = data[ssid_end]; - size_t pass_start = ssid_end + 1; - size_t pass_end = pass_start + pass_length; - - std::string ssid(data + ssid_start, data + ssid_end); - std::string password(data + pass_start, data + pass_end); - return {.command = command, .ssid = ssid, .password = password}; - } - - return { - .command = command, - }; -} - -std::vector build_rpc_response(Command command, const std::vector &datum) { - std::vector out; - uint32_t length = 0; - out.push_back(command); - for (auto str : datum) { - uint8_t len = str.length(); - length += len; - out.push_back(len); - out.insert(out.end(), str.begin(), str.end()); - } - out.insert(out.begin() + 1, length); - - uint32_t calculated_checksum = 0; - - for (uint8_t byte : out) { - calculated_checksum += byte; - } - out.push_back(calculated_checksum); - return out; -} - -#ifdef USE_ARDUINO -std::vector build_rpc_response(Command command, const std::vector &datum) { - std::vector out; - uint32_t length = 0; - out.push_back(command); - for (auto str : datum) { - uint8_t len = str.length(); - length += len; - out.push_back(len); - out.insert(out.end(), str.begin(), str.end()); - } - out.insert(out.begin() + 1, length); - - uint32_t calculated_checksum = 0; - - for (uint8_t byte : out) { - calculated_checksum += byte; - } - out.push_back(calculated_checksum); - return out; -} -#endif // USE_ARDUINO - -} // namespace improv diff --git a/esphome/components/improv/improv.h b/esphome/components/improv/improv.h deleted file mode 100644 index 0ead80e2cf..0000000000 --- a/esphome/components/improv/improv.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#ifdef USE_ARDUINO -#include "WString.h" -#endif // USE_ARDUINO - -#include -#include -#include - -namespace improv { - -static const char *const SERVICE_UUID = "00467768-6228-2272-4663-277478268000"; -static const char *const STATUS_UUID = "00467768-6228-2272-4663-277478268001"; -static const char *const ERROR_UUID = "00467768-6228-2272-4663-277478268002"; -static const char *const RPC_COMMAND_UUID = "00467768-6228-2272-4663-277478268003"; -static const char *const RPC_RESULT_UUID = "00467768-6228-2272-4663-277478268004"; -static const char *const CAPABILITIES_UUID = "00467768-6228-2272-4663-277478268005"; - -enum Error : uint8_t { - ERROR_NONE = 0x00, - ERROR_INVALID_RPC = 0x01, - ERROR_UNKNOWN_RPC = 0x02, - ERROR_UNABLE_TO_CONNECT = 0x03, - ERROR_NOT_AUTHORIZED = 0x04, - ERROR_UNKNOWN = 0xFF, -}; - -enum State : uint8_t { - STATE_STOPPED = 0x00, - STATE_AWAITING_AUTHORIZATION = 0x01, - STATE_AUTHORIZED = 0x02, - STATE_PROVISIONING = 0x03, - STATE_PROVISIONED = 0x04, -}; - -enum Command : uint8_t { - UNKNOWN = 0x00, - WIFI_SETTINGS = 0x01, - IDENTIFY = 0x02, - BAD_CHECKSUM = 0xFF, -}; - -static const uint8_t CAPABILITY_IDENTIFY = 0x01; - -struct ImprovCommand { - Command command; - std::string ssid; - std::string password; -}; - -ImprovCommand parse_improv_data(const std::vector &data); -ImprovCommand parse_improv_data(const uint8_t *data, size_t length); - -std::vector build_rpc_response(Command command, const std::vector &datum); -#ifdef USE_ARDUINO -std::vector build_rpc_response(Command command, const std::vector &datum); -#endif // USE_ARDUINO - -} // namespace improv diff --git a/esphome/components/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/ina260/__init__.py b/esphome/components/ina260/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ina260/ina260.cpp b/esphome/components/ina260/ina260.cpp new file mode 100644 index 0000000000..2f220e6a11 --- /dev/null +++ b/esphome/components/ina260/ina260.cpp @@ -0,0 +1,128 @@ +#include "ina260.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ina260 { + +static const char *const TAG = "ina260"; + +// | A0 | A1 | Address | +// | GND | GND | 0x40 | +// | GND | V_S+ | 0x41 | +// | GND | SDA | 0x42 | +// | GND | SCL | 0x43 | +// | V_S+ | GND | 0x44 | +// | V_S+ | V_S+ | 0x45 | +// | V_S+ | SDA | 0x46 | +// | V_S+ | SCL | 0x47 | +// | SDA | GND | 0x48 | +// | SDA | V_S+ | 0x49 | +// | SDA | SDA | 0x4A | +// | SDA | SCL | 0x4B | +// | SCL | GND | 0x4C | +// | SCL | V_S+ | 0x4D | +// | SCL | SDA | 0x4E | +// | SCL | SCL | 0x4F | + +static const uint8_t INA260_REGISTER_CONFIG = 0x00; +static const uint8_t INA260_REGISTER_CURRENT = 0x01; +static const uint8_t INA260_REGISTER_BUS_VOLTAGE = 0x02; +static const uint8_t INA260_REGISTER_POWER = 0x03; +static const uint8_t INA260_REGISTER_MASK_ENABLE = 0x06; +static const uint8_t INA260_REGISTER_ALERT_LIMIT = 0x07; +static const uint8_t INA260_REGISTER_MANUFACTURE_ID = 0xFE; +static const uint8_t INA260_REGISTER_DEVICE_ID = 0xFF; + +void INA260Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up INA260..."); + + // Reset device on setup + if (!this->write_byte_16(INA260_REGISTER_CONFIG, 0x8000)) { + this->error_code_ = DEVICE_RESET_FAILED; + this->mark_failed(); + return; + } + + delay(2); + + this->read_byte_16(INA260_REGISTER_MANUFACTURE_ID, &this->manufacture_id_); + this->read_byte_16(INA260_REGISTER_DEVICE_ID, &this->device_id_); + + if (this->manufacture_id_ != (uint16_t) 0x5449 || this->device_id_ != (uint16_t) 0x2270) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + if (!this->write_byte_16(INA260_REGISTER_CONFIG, (uint16_t) 0b0000001100000111)) { + this->error_code_ = FAILED_TO_UPDATE_CONFIGURATION; + this->mark_failed(); + return; + } +} + +void INA260Component::dump_config() { + ESP_LOGCONFIG(TAG, "INA260:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " Manufacture ID: 0x%x", this->manufacture_id_); + ESP_LOGCONFIG(TAG, " Device ID: 0x%x", this->device_id_); + + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Connected device does not match a known INA260 sensor"); + break; + case DEVICE_RESET_FAILED: + ESP_LOGE(TAG, "Device reset failed - Is the device connected?"); + break; + case FAILED_TO_UPDATE_CONFIGURATION: + ESP_LOGE(TAG, "Failed to update device configuration"); + break; + case NONE: + default: + break; + } +} + +void INA260Component::update() { + if (this->bus_voltage_sensor_ != nullptr) { + uint16_t raw_bus_voltage; + if (!this->read_byte_16(INA260_REGISTER_BUS_VOLTAGE, &raw_bus_voltage)) { + this->status_set_warning(); + return; + } + float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f; + this->bus_voltage_sensor_->publish_state(bus_voltage_v); + } + + if (this->current_sensor_ != nullptr) { + uint16_t raw_current; + if (!this->read_byte_16(INA260_REGISTER_CURRENT, &raw_current)) { + this->status_set_warning(); + return; + } + float current_a = int16_t(raw_current) * 0.00125f; + this->current_sensor_->publish_state(current_a); + } + + if (this->power_sensor_ != nullptr) { + uint16_t raw_power; + if (!this->read_byte_16(INA260_REGISTER_POWER, &raw_power)) { + this->status_set_warning(); + return; + } + float power_w = ((int16_t(raw_power) * 10.0f) / 1000.0f); + this->power_sensor_->publish_state(power_w); + } + + this->status_clear_warning(); +} + +} // namespace ina260 +} // namespace esphome diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h new file mode 100644 index 0000000000..8bad1cba6d --- /dev/null +++ b/esphome/components/ina260/ina260.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ina260 { + +class INA260Component : 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_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { this->bus_voltage_sensor_ = bus_voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + + protected: + uint16_t manufacture_id_{0}; + uint16_t device_id_{0}; + + sensor::Sensor *bus_voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + + enum ErrorCode { + NONE, + COMMUNICATION_FAILED, + DEVICE_RESET_FAILED, + FAILED_TO_UPDATE_CONFIGURATION, + } error_code_{NONE}; +}; + +} // namespace ina260 +} // namespace esphome diff --git a/esphome/components/ina260/sensor.py b/esphome/components/ina260/sensor.py new file mode 100644 index 0000000000..048e713afa --- /dev/null +++ b/esphome/components/ina260/sensor.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@MrEditor97"] + +ina260_ns = cg.esphome_ns.namespace("ina260") +INA260Component = ina260_ns.class_( + "INA260Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(INA260Component), + 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_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, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_BUS_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + cg.add(var.set_bus_voltage_sensor(sens)) + + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index 0ab9f8b3e0..aa11fb3172 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -53,6 +54,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index e4c71ea717..dca764c6ed 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -6,11 +6,13 @@ from esphome.const import ( CONF_FULL_UPDATE_EVERY, CONF_ID, CONF_LAMBDA, + CONF_MODEL, CONF_PAGES, CONF_WAKEUP_PIN, ) DEPENDENCIES = ["i2c", "esp32"] +AUTO_LOAD = ["psram"] CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" @@ -40,6 +42,13 @@ Inkplate6 = inkplate6_ns.class_( "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer ) +InkplateModel = inkplate6_ns.enum("InkplateModel") + +MODELS = { + "inkplate_6": InkplateModel.INKPLATE_6, + "inkplate_10": InkplateModel.INKPLATE_10, +} + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { @@ -47,6 +56,9 @@ CONFIG_SCHEMA = cv.All( 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, + cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( + MODELS, lower=True, space="_" + ), # Control pins cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, @@ -110,6 +122,8 @@ async def to_code(config): cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + cg.add(var.set_model(config[CONF_MODEL])) + ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) cg.add(var.set_ckv_pin(ckv)) @@ -166,5 +180,3 @@ async def to_code(config): 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 index 8a05836db9..e62e594a49 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -42,32 +42,32 @@ void Inkplate6::setup() { this->display(); } void Inkplate6::initialize_() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint32_t buffer_size = this->get_buffer_length_(); + if (buffer_size == 0) + return; - 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 - } + if (this->partial_buffer_ != nullptr) + allocator.deallocate(this->partial_buffer_, buffer_size); + if (this->partial_buffer_2_ != nullptr) + allocator.deallocate(this->partial_buffer_2_, buffer_size * 2); + if (this->buffer_ != nullptr) + allocator.deallocate(this->buffer_, buffer_size); - this->buffer_ = (uint8_t *) ps_malloc(buffer_size); + this->buffer_ = allocator.allocate(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); + this->partial_buffer_ = allocator.allocate(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); + this->partial_buffer_2_ = allocator.allocate(buffer_size * 2); if (this->partial_buffer_2_ == nullptr) { ESP_LOGE(TAG, "Could not allocate partial buffer 2 for display!"); this->mark_failed(); diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 56e95e95bb..2dac12a0c4 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -10,6 +10,11 @@ namespace esphome { namespace inkplate6 { +enum InkplateModel : uint8_t { + INKPLATE_6 = 0, + INKPLATE_10 = 1, +}; + class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { public: const uint8_t LUT2[16] = {0b10101010, 0b10101001, 0b10100110, 0b10100101, 0b10011010, 0b10011001, @@ -43,6 +48,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public void set_partial_updating(bool partial_updating) { this->partial_updating_ = partial_updating; } void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } + void set_model(InkplateModel model) { this->model_ = model; } + 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; } @@ -101,9 +108,21 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public void pins_z_state_(); void pins_as_outputs_(); - int get_width_internal() override { return 800; } + int get_width_internal() override { + if (this->model_ == INKPLATE_6) + return 800; + else if (this->model_ == INKPLATE_10) + return 1200; + return 0; + } - int get_height_internal() override { return 600; } + int get_height_internal() override { + if (this->model_ == INKPLATE_6) + return 600; + else if (this->model_ == INKPLATE_10) + return 825; + return 0; + } size_t get_buffer_length_(); @@ -133,6 +152,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public bool greyscale_; bool partial_updating_; + InkplateModel model_; + InternalGPIOPin *display_data_0_pin_; InternalGPIOPin *display_data_1_pin_; InternalGPIOPin *display_data_2_pin_; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 2a398e5240..642116152c 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -23,31 +23,6 @@ void IntegrationSensor::setup() { this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); }); } void IntegrationSensor::dump_config() { LOG_SENSOR("", "Integration Sensor", this); } -std::string IntegrationSensor::unit_of_measurement() { - std::string suffix; - switch (this->time_) { - case INTEGRATION_SENSOR_TIME_MILLISECOND: - suffix = "ms"; - break; - case INTEGRATION_SENSOR_TIME_SECOND: - suffix = "s"; - break; - case INTEGRATION_SENSOR_TIME_MINUTE: - suffix = "min"; - break; - case INTEGRATION_SENSOR_TIME_HOUR: - suffix = "h"; - break; - case INTEGRATION_SENSOR_TIME_DAY: - suffix = "d"; - break; - } - std::string base = this->sensor_->get_unit_of_measurement(); - if (str_endswith(base, "/" + suffix)) { - return base.substr(0, base.size() - suffix.size() - 1); - } - return base + suffix; -} void IntegrationSensor::process_sensor_value_(float value) { const uint32_t now = millis(); const double old_value = this->last_value_; diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 437649c1dd..1d46973086 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -63,8 +63,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { this->last_save_ = now; this->rtc_.save(&result_f); } - std::string unit_of_measurement() override; - int8_t accuracy_decimals() override { return this->sensor_->get_accuracy_decimals() + 2; } sensor::Sensor *sensor_; IntegrationSensorTime time_; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 26c7c2871a..c35d42f385 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -2,7 +2,14 @@ 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_ICON, CONF_ID, CONF_SENSOR, CONF_RESTORE +from esphome.const import ( + CONF_ICON, + CONF_ID, + CONF_SENSOR, + CONF_RESTORE, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, +) from esphome.core.entity_helpers import inherit_property_from integration_ns = cg.esphome_ns.namespace("integration") @@ -30,6 +37,18 @@ CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" CONF_MIN_SAVE_INTERVAL = "min_save_interval" + +def inherit_unit_of_measurement(uom, config): + suffix = config[CONF_TIME_UNIT] + if uom.endswith("/" + suffix): + return uom[0 : -len("/" + suffix)] + return uom + suffix + + +def inherit_accuracy_decimals(decimals, config): + return decimals + 2 + + CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(IntegrationSensor), @@ -51,11 +70,19 @@ FINAL_VALIDATE_SCHEMA = cv.All( { cv.Required(CONF_ID): cv.use_id(IntegrationSensor), cv.Optional(CONF_ICON): cv.icon, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): sensor.validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): sensor.validate_accuracy_decimals, cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), }, extra=cv.ALLOW_EXTRA, ), inherit_property_from(CONF_ICON, CONF_SENSOR), + inherit_property_from( + CONF_UNIT_OF_MEASUREMENT, CONF_SENSOR, transform=inherit_unit_of_measurement + ), + inherit_property_from( + CONF_ACCURACY_DECIMALS, CONF_SENSOR, transform=inherit_accuracy_decimals + ), ) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index fda0a552f1..6a0e4c50d2 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -7,12 +7,11 @@ json_ns = cg.esphome_ns.namespace("json") CONFIG_SCHEMA = cv.All( cv.Schema({}), - cv.only_with_arduino, ) @coroutine_with_priority(1.0) async def to_code(config): - cg.add_library("ottowinter/ArduinoJson-esphomelib", "5.13.3") + cg.add_library("bblanchon/ArduinoJson", "6.18.5") 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 12c5beb73f..7e88fb6e59 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,8 +1,13 @@ -#ifdef USE_ARDUINO - #include "json_util.h" #include "esphome/core/log.h" +#ifdef USE_ESP8266 +#include +#endif +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace json { @@ -10,110 +15,49 @@ static const char *const TAG = "json"; static std::vector global_json_build_buffer; // NOLINT -const char *build_json(const json_build_t &f, size_t *length) { - global_json_buffer.clear(); - JsonObject &root = global_json_buffer.createObject(); +std::string build_json(const json_build_t &f) { + // Here we are allocating as much heap memory as available minus 2kb to be safe + // as we can not have a true dynamic sized document. + // The excess memory is freed below with `shrinkToFit()` +#ifdef USE_ESP8266 + const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP32) + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; +#endif + DynamicJsonDocument json_document(free_heap); + JsonObject root = json_document.to(); f(root); + json_document.shrinkToFit(); - // The Json buffer size gives us a good estimate for the required size. - // Usually, it's a bit larger than the actual required string size - // | JSON Buffer Size | String Size | - // Discovery | 388 | 351 | - // Discovery | 372 | 356 | - // Discovery | 336 | 311 | - // Discovery | 408 | 393 | - 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.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.data(); + std::string output; + serializeJson(json_document, output); + return output; } -void parse_json(const std::string &data, const json_parse_t &f) { - global_json_buffer.clear(); - JsonObject &root = global_json_buffer.parseObject(data); - if (!root.success()) { +void parse_json(const std::string &data, const json_parse_t &f) { + // Here we are allocating as much heap memory as available minus 2kb to be safe + // as we can not have a true dynamic sized document. + // The excess memory is freed below with `shrinkToFit()` +#ifdef USE_ESP8266 + const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP32) + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; +#endif + + DynamicJsonDocument json_document(free_heap); + DeserializationError err = deserializeJson(json_document, data); + json_document.shrinkToFit(); + + JsonObject root = json_document.as(); + + if (err) { ESP_LOGW(TAG, "Parsing JSON failed."); return; } f(root); } -std::string build_json(const json_build_t &f) { - size_t len; - const char *c_str = build_json(f, &len); - return std::string(c_str, len); -} - -VectorJsonBuffer::String::String(VectorJsonBuffer *parent) : parent_(parent), start_(parent->size_) {} -void VectorJsonBuffer::String::append(char c) const { - char *last = static_cast(this->parent_->do_alloc(1)); - *last = c; -} -const char *VectorJsonBuffer::String::c_str() const { - this->append('\0'); - return &this->parent_->buffer_[this->start_]; -} -void VectorJsonBuffer::clear() { - for (char *block : this->free_blocks_) - free(block); // NOLINT - - this->size_ = 0; - this->free_blocks_.clear(); -} -VectorJsonBuffer::String VectorJsonBuffer::startString() { return {this}; } // NOLINT -void *VectorJsonBuffer::alloc(size_t bytes) { - // Make sure memory addresses are aligned - uint32_t new_size = round_size_up(this->size_); - this->resize(new_size); - return this->do_alloc(bytes); -} -void *VectorJsonBuffer::do_alloc(size_t bytes) { // NOLINT - const uint32_t begin = this->size_; - this->resize(begin + bytes); - return &this->buffer_[begin]; -} -void VectorJsonBuffer::resize(size_t size) { // NOLINT - if (size <= this->size_) { - this->size_ = size; - return; - } - - this->reserve(size); - this->size_ = size; -} -void VectorJsonBuffer::reserve(size_t size) { // NOLINT - if (size <= this->capacity_) - return; - - uint32_t target_capacity = this->capacity_; - if (this->capacity_ == 0) { - // lazily initialize with a reasonable size - target_capacity = JSON_OBJECT_SIZE(16); - } - while (target_capacity < size) - target_capacity *= 2; - - char *old_buffer = this->buffer_; - 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_); - } - this->capacity_ = target_capacity; -} - -size_t VectorJsonBuffer::size() const { return this->size_; } - -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 577510e63a..57fe6107d8 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -1,68 +1,28 @@ #pragma once -#ifdef USE_ARDUINO - #include #include "esphome/core/helpers.h" + +#undef ARDUINOJSON_ENABLE_STD_STRING +#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT + #include namespace esphome { namespace json { /// Callback function typedef for parsing JsonObjects. -using json_parse_t = std::function; +using json_parse_t = std::function; /// Callback function typedef for building JsonObjects. -using json_build_t = std::function; +using json_build_t = std::function; /// Build a JSON string with the provided json build function. -const char *build_json(const json_build_t &f, size_t *length); - std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. void parse_json(const std::string &data, const json_parse_t &f); -class VectorJsonBuffer : public ArduinoJson::Internals::JsonBufferBase { - public: - class String { - public: - String(VectorJsonBuffer *parent); - - void append(char c) const; - - const char *c_str() const; - - protected: - VectorJsonBuffer *parent_; - uint32_t start_; - }; - - void *alloc(size_t bytes) override; - - size_t size() const; - - void clear(); - - String startString(); // NOLINT - - protected: - void *do_alloc(size_t bytes); // NOLINT - - void resize(size_t size); // NOLINT - - void reserve(size_t size); // NOLINT - - char *buffer_{nullptr}; - size_t size_{0}; - size_t capacity_{0}; - std::vector free_blocks_; -}; - -extern VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace json } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/kalman_combinator/__init__.py b/esphome/components/kalman_combinator/__init__.py new file mode 100644 index 0000000000..3356e61bb2 --- /dev/null +++ b/esphome/components/kalman_combinator/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/kalman_combinator/kalman_combinator.cpp b/esphome/components/kalman_combinator/kalman_combinator.cpp new file mode 100644 index 0000000000..d55f26126f --- /dev/null +++ b/esphome/components/kalman_combinator/kalman_combinator.cpp @@ -0,0 +1,82 @@ +#include "kalman_combinator.h" +#include "esphome/core/hal.h" +#include +#include + +namespace esphome { +namespace kalman_combinator { + +void KalmanCombinatorComponent::dump_config() { + ESP_LOGCONFIG("kalman_combinator", "Kalman Combinator:"); + ESP_LOGCONFIG("kalman_combinator", " Update variance: %f per ms", this->update_variance_value_); + ESP_LOGCONFIG("kalman_combinator", " Sensors:"); + for (const auto &sensor : this->sensors_) { + auto &entity = *sensor.first; + ESP_LOGCONFIG("kalman_combinator", " - %s", entity.get_name().c_str()); + } +} + +void KalmanCombinatorComponent::setup() { + for (const auto &sensor : this->sensors_) { + const auto stddev = sensor.second; + sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + } +} + +void KalmanCombinatorComponent::add_source(Sensor *sensor, std::function const &stddev) { + this->sensors_.emplace_back(sensor, stddev); +} + +void KalmanCombinatorComponent::add_source(Sensor *sensor, float stddev) { + this->add_source(sensor, std::function{[stddev](float) -> float { return stddev; }}); +} + +void KalmanCombinatorComponent::update_variance_() { + uint32_t now = millis(); + + // Variance increases by update_variance_ each millisecond + auto dt = now - this->last_update_; + auto dv = this->update_variance_value_ * dt; + this->variance_ += dv; + this->last_update_ = now; +} + +void KalmanCombinatorComponent::correct_(float value, float stddev) { + if (std::isnan(value) || std::isinf(stddev)) { + return; + } + + if (std::isnan(this->state_) || std::isinf(this->variance_)) { + this->state_ = value; + this->variance_ = stddev * stddev; + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(stddev); + } + return; + } + + this->update_variance_(); + + // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu + // Use the value with the smaller variance as mu1 to prevent precision errors + const bool this_first = this->variance_ < (stddev * stddev); + const float mu1 = this_first ? this->state_ : value; + const float mu2 = this_first ? value : this->state_; + + const float var1 = this_first ? this->variance_ : stddev * stddev; + const float var2 = this_first ? stddev * stddev : this->variance_; + + const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); + const float var = var1 - (var1 * var1) / (var1 + var2); + + // Update and publish state + this->state_ = mu; + this->variance_ = var; + + this->publish_state(mu); + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(std::sqrt(var)); + } +} +} // namespace kalman_combinator +} // namespace esphome diff --git a/esphome/components/kalman_combinator/kalman_combinator.h b/esphome/components/kalman_combinator/kalman_combinator.h new file mode 100644 index 0000000000..afbe3ece92 --- /dev/null +++ b/esphome/components/kalman_combinator/kalman_combinator.h @@ -0,0 +1,46 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include +#include + +namespace esphome { +namespace kalman_combinator { + +class KalmanCombinatorComponent : public Component, public sensor::Sensor { + public: + KalmanCombinatorComponent() = default; + + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + void dump_config() override; + void setup() override; + + void add_source(Sensor *sensor, std::function const &stddev); + void add_source(Sensor *sensor, float stddev); + void set_process_std_dev(float process_std_dev) { + this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; + } + void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } + + private: + void update_variance_(); + void correct_(float value, float stddev); + + // Source sensors and their error functions + std::vector>> sensors_; + + // Optional sensor for publishing the current error + sensor::Sensor *std_dev_sensor_{nullptr}; + + // Tick of the last update + uint32_t last_update_{0}; + // Change of the variance, per ms + float update_variance_value_{0.f}; + + // Best guess for the state and its variance + float state_{NAN}; + float variance_{INFINITY}; +}; +} // namespace kalman_combinator +} // namespace esphome diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py new file mode 100644 index 0000000000..9223f883b2 --- /dev/null +++ b/esphome/components/kalman_combinator/sensor.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_SOURCE, + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +kalman_combinator_ns = cg.esphome_ns.namespace("kalman_combinator") +KalmanCombinatorComponent = kalman_combinator_ns.class_( + "KalmanCombinatorComponent", cg.Component, sensor.Sensor +) + +CONF_ERROR = "error" +CONF_SOURCES = "sources" +CONF_PROCESS_STD_DEV = "process_std_dev" +CONF_STD_DEV = "std_dev" + + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(KalmanCombinatorComponent), + cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, + cv.Required(CONF_SOURCES): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), + } + ), + ), + cv.Optional(CONF_STD_DEV): sensor.SENSOR_SCHEMA, + } +) + +# Inherit some sensor values from the first source, for both the state and the error value +properties_to_inherit = [ + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, + # CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" +] +inherit_schema_for_state = [ + inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] +inherit_schema_for_std_dev = [ + inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] + +FINAL_VALIDATE_SCHEMA = cv.All( + CONFIG_SCHEMA.extend( + {cv.Required(CONF_ID): cv.use_id(KalmanCombinatorComponent)}, + extra=cv.ALLOW_EXTRA, + ), + *inherit_schema_for_state, + *inherit_schema_for_std_dev, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(var.set_process_std_dev(config[CONF_PROCESS_STD_DEV])) + for source_conf in config[CONF_SOURCES]: + source = await cg.get_variable(source_conf[CONF_SOURCE]) + error = await cg.templatable( + source_conf[CONF_ERROR], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, error)) + + if CONF_STD_DEV in config: + sens = await sensor.new_sensor(config[CONF_STD_DEV]) + cg.add(var.set_std_dev_sensor(sens)) diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.cpp b/esphome/components/lcd_gpio/gpio_lcd_display.cpp index b0344d313c..94ddc34051 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.cpp +++ b/esphome/components/lcd_gpio/gpio_lcd_display.cpp @@ -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); } diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 21a747e34d..a56dccfd72 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -15,17 +15,37 @@ namespace ledc { 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) { - const float max_div_num = ((1 << 20) - 1) / 256.0f; + +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) { - for (int i = 20; i >= 1; i--) { - const float min_frequency = ledc_min_frequency_for_bit_depth(i); + 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) + if (min_frequency <= frequency && frequency <= max_frequency) { + ESP_LOGD(TAG, "Resolution calculated as %d", i); return i; + } } return {}; } @@ -48,7 +68,7 @@ void LEDCOutput::write_state(float state) { ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF - auto speed_mode = channel_ < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; + 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); @@ -63,11 +83,15 @@ void LEDCOutput::setup() { ledcAttachPin(this->pin_->get_pin(), this->channel_); #endif #ifdef USE_ESP_IDF - auto speed_mode = channel_ < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; + auto 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; @@ -114,7 +138,7 @@ void LEDCOutput::update_frequency(float frequency) { ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); return; } - auto speed_mode = channel_ < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; + auto speed_mode = get_speed_mode(channel_); auto timer_num = static_cast((channel_ % 8) / 2); ledc_timer_config_t timer_conf{}; diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 03224d4c10..fe8a90b8db 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( 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, @@ -37,6 +38,7 @@ from .types import ( # noqa AddressableLight, LightTurnOnTrigger, LightTurnOffTrigger, + LightStateTrigger, ) CODEOWNERS = ["@esphome/core"] @@ -69,6 +71,11 @@ LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ex 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), + } + ), } ) @@ -151,6 +158,9 @@ async def setup_light_core_(light_var, output_var, config): for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) 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])) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index f3e6c0ef1d..a8e0c7b762 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -79,7 +79,7 @@ optional AddressableLightTransformer::apply() { // dynamically-calculated alpha values to match the look. float denom = (1.0f - smoothed_progress); - float alpha = denom == 0.0f ? 0.0f : (smoothed_progress - this->last_transition_progress_) / denom; + 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. diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index fea7508515..8302239d6a 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -22,6 +22,12 @@ using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphom /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); +/// 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 { public: virtual int32_t size() const = 0; @@ -81,7 +87,7 @@ class AddressableLight : public LightOutput, public Component { void mark_shown_() { #ifdef USE_POWER_SUPPLY for (const auto &c : *this) { - if (c.get().is_on()) { + 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; } diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 358fe69c23..d404898edf 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -167,7 +167,7 @@ class AddressableScanEffect : public AddressableLightEffect { this->last_move_ = now; it.all() = Color::BLACK; - for (auto i = 0; i < this->scan_width_; i++) { + for (uint32_t i = 0; i < this->scan_width_; i++) { it[this->at_led_ + i] = current_color; } @@ -178,7 +178,7 @@ class AddressableScanEffect : public AddressableLightEffect { 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}; }; @@ -331,9 +331,10 @@ class AddressableFlickerEffect : public AddressableLightEffect { return; this->last_update_ = now; - fast_random_set_seed(random_uint32()); + uint32_t rng_state = random_uint32(); for (auto var : it) { - const uint8_t flicker = fast_random_8() % intensity; + rng_state = (rng_state * 0x9E3779B9) + 0x9E37; + const uint8_t flicker = (rng_state & 0xFF) % intensity; // scale down by random factor var = var.get() * (255 - flicker); diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index cd5bcabd47..d358502430 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -16,24 +16,94 @@ class AddressableLightWrapper : public light::AddressableLight { void clear_effect_data() override { this->wrapper_state_[4] = 0; } - light::LightTraits get_traits() override { return this->light_state_->get_traits(); } + 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); - float brightness = fmaxf(r, fmaxf(g, b)); auto call = this->light_state_->make_call(); - call.set_state(true); - call.set_brightness_if_supported(1.0f); - call.set_color_brightness_if_supported(brightness); - call.set_red_if_supported(r); - call.set_green_if_supported(g); - call.set_blue_if_supported(b); - call.set_white_if_supported(w); + + 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); @@ -50,6 +120,7 @@ class AddressableLightWrapper : public light::AddressableLight { light::LightState *light_state_; uint8_t *wrapper_state_; + ColorMode color_mode_{ColorMode::UNKNOWN}; }; } // namespace light diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 5ec2cb626a..b63fc93dc5 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -141,6 +141,13 @@ class LightTurnOffTrigger : public Trigger<> { } }; +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); diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 9858590850..3f1b8aef30 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -98,7 +98,7 @@ void LightCall::perform() { // EFFECT auto effect = this->effect_; const char *effect_s; - if (effect == 0) + if (effect == 0u) effect_s = "None"; else effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 2e07d91046..c126859076 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -8,7 +8,7 @@ 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) { +void LightJSONSchema::dump_json(LightState &state, JsonObject root) { if (state.supports_effects()) root["effect"] = state.get_effect_name(); @@ -52,7 +52,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { if (values.get_color_mode() & ColorCapability::BRIGHTNESS) root["brightness"] = uint8_t(values.get_brightness() * 255); - JsonObject &color = root.createNestedObject("color"); + 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); @@ -72,7 +72,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { } } -void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject &root) { +void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { if (root.containsKey("state")) { auto val = parse_on_off(root["state"]); switch (val) { @@ -95,7 +95,7 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } if (root.containsKey("color")) { - JsonObject &color = root["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")) { @@ -140,7 +140,7 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } -void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject &root) { +void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { LightJSONSchema::parse_color_json(state, call, root); if (root.containsKey("flash")) { diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index 09a372f11c..c92dd7b655 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -14,12 +14,12 @@ namespace light { class LightJSONSchema { public: /// Dump the state of a light as JSON. - static void dump_json(LightState &state, JsonObject &root); + 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); + static void parse_json(LightState &state, LightCall &call, JsonObject root); protected: - static void parse_color_json(LightState &state, LightCall &call, JsonObject &root); + static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; } // namespace light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index dd904d0eed..35b045d5b4 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -39,7 +39,15 @@ class LightTransformer { protected: /// The progress of this transition, on a scale of 0 to 1. - float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } + 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; + + return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); + } uint32_t start_time_; uint32_t length_; diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 90646f4e61..a557bd39b1 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -18,10 +18,13 @@ class LightTransitionTransformer : public LightTransformer { this->start_values_.set_brightness(0.0f); } - // When turning light off from on state, use source state and only decrease brightness to zero. + // 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->target_values_ = LightColorValues(this->start_values_); - this->target_values_.set_brightness(0.0f); + 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. @@ -43,7 +46,7 @@ class LightTransitionTransformer : public LightTransformer { } LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; - LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_; + 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; @@ -57,6 +60,7 @@ class LightTransitionTransformer : public LightTransformer { 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_{}; }; @@ -69,9 +73,7 @@ class LightFlashTransformer : public LightTransformer { if (this->transition_length_ * 2 > this->length_) this->transition_length_ = this->length_ / 2; - // do not create transition if length is 0 - if (this->transition_length_ == 0) - return; + this->begun_lightstate_restore_ = false; // first transition to original target this->transformer_ = this->state_.get_output()->create_default_transition(); @@ -79,40 +81,45 @@ class LightFlashTransformer : public LightTransformer { } optional apply() override { - // transition transformer does not handle 0 length as progress returns nan - if (this->transition_length_ == 0) - return this->target_values_; + 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) { - if (!this->transformer_->is_finished()) { - return this->transformer_->apply(); - } else { + result = this->transformer_->apply(); + + if (this->transformer_->is_finished()) { this->transformer_->stop(); this->transformer_ = nullptr; } } - if (millis() > this->start_time_ + this->length_ - this->transition_length_) { - // second transition back to start value - this->transformer_ = this->state_.get_output()->create_default_transition(); - this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); - } - - // once transition is complete, don't change states until next transition - return optional(); + 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 diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index cf544e5435..a453debd94 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -4,10 +4,9 @@ from esphome import automation # Base light_ns = cg.esphome_ns.namespace("light") LightState = light_ns.class_("LightState", cg.EntityBase, cg.Component) -# Fake class for addressable lights -AddressableLightState = light_ns.class_("LightState", LightState) +AddressableLightState = light_ns.class_("AddressableLightState", LightState) LightOutput = light_ns.class_("LightOutput") -AddressableLight = light_ns.class_("AddressableLight", cg.Component) +AddressableLight = light_ns.class_("AddressableLight", LightOutput, cg.Component) AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") @@ -42,6 +41,7 @@ LightTurnOnTrigger = light_ns.class_( LightTurnOffTrigger = light_ns.class_( "LightTurnOffTrigger", automation.Trigger.template() ) +LightStateTrigger = light_ns.class_("LightStateTrigger", automation.Trigger.template()) # Effects LightEffect = light_ns.class_("LightEffect") diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index bc1bc6bb41..20a0b0f792 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -19,6 +19,8 @@ from esphome.const import ( 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 CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -52,6 +54,10 @@ LOG_LEVEL_SEVERITY = [ "VERY_VERBOSE", ] +ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2] + +UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"] + UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"] UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"] @@ -75,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) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2d85969bf3..11c0733701 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -153,13 +153,9 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; break; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) case UART_SELECTION_UART2: -#if !CONFIG_IDF_TARGET_ESP32S2 && !CONFIG_IDF_TARGET_ESP32C3 - // FIXME: Validate in config that UART2 can't be set for ESP32-S2 (only has - // UART0-UART1) this->hw_serial_ = &Serial2; -#endif break; #endif } @@ -173,9 +169,11 @@ void Logger::pre_setup() { 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_; @@ -223,7 +221,7 @@ 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; } +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"}; diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index e6fa6e2058..8756bc2387 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -24,7 +24,7 @@ namespace logger { enum UARTSelection { UART_SELECTION_UART0 = 0, UART_SELECTION_UART1, -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) UART_SELECTION_UART2, #endif #ifdef USE_ESP8266 diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index 36f3835724..959af68235 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -97,7 +97,7 @@ void LTR390Component::read_mode_(int mode_index) { // If there are more modes to read then begin the next // otherwise stop - if (mode_index + 1 < this->mode_funcs_.size()) { + if (mode_index + 1 < (int) this->mode_funcs_.size()) { this->read_mode_(mode_index + 1); } else { this->reading_ = false; diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 91946cde2c..126915dc15 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -203,16 +203,16 @@ float MAX31865Sensor::calc_temperature_(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/max7219digit/display.py b/esphome/components/max7219digit/display.py index e1ca128699..2753f70eef 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -13,10 +13,20 @@ 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": 0, - "STOP": 1, + "CONTINUOUS": ScrollMode.CONTINUOUS, + "STOP": ScrollMode.STOP, } CHIP_MODES = { @@ -37,6 +47,10 @@ CONFIG_SCHEMA = ( { 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( @@ -67,6 +81,8 @@ async def to_code(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])) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 4fedd3d312..2368c17448 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -26,9 +26,12 @@ void MAX7219Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); this->spi_setup(); this->stepsleft_ = 0; - this->max_displaybuffer_.reserve(500); // Create base space to write buffer - // Initialize buffer with 0 for display so all non written pixels are blank - this->max_displaybuffer_.resize(this->num_chips_ * 8, 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 @@ -46,6 +49,8 @@ void MAX7219Component::setup() { 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_); @@ -59,19 +64,19 @@ void MAX7219Component::loop() { uint32_t now = millis(); // check if the buffer has shrunk past the current position since last update - if ((this->max_displaybuffer_.size() >= this->old_buffer_size_ + 3) || - (this->max_displaybuffer_.size() <= this->old_buffer_size_ - 3)) { + 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_.size(); + 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_.size()) + 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_.size() <= this->num_chips_ * 8)) { + if (!this->scroll_ || (this->max_displaybuffer_[0].size() <= (size_t) get_width_internal())) { this->display(); return; } @@ -82,8 +87,8 @@ void MAX7219Component::loop() { } // Dwell time at end of string in case of stop at end - if (this->scroll_mode_ == 1) { - if (this->stepsleft_ >= this->max_displaybuffer_.size() - this->num_chips_ * 8 + 1) { + 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; @@ -107,30 +112,53 @@ void MAX7219Component::display() { // 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 i = 0; i < this->num_chips_; i++) { - for (uint8_t j = 0; j < 8; j++) { - if (this->reverse_) { - pixels[j] = this->max_displaybuffer_[(this->num_chips_ - i - 1) * 8 + j]; - } else { - pixels[j] = this->max_displaybuffer_[i * 8 + j]; + 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_(); } - this->send64pixels(i, pixels); + } +} + +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; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHER - // TO BE DONE -> CREATE Virtual size of screen and scroll + 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_ * 8; } - -size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } +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 > this->max_displaybuffer_.size()) { // Extend the display buffer in case required - this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); + 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 @@ -140,9 +168,9 @@ void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color colo uint8_t subpos = y; // Y is starting at 0 top left if (color.is_on()) { - this->max_displaybuffer_[pos] |= (1 << subpos); + this->max_displaybuffer_[subpos / 8][pos] |= (1 << subpos % 8); } else { - this->max_displaybuffer_[pos] &= ~(1 << subpos); + this->max_displaybuffer_[subpos / 8][pos] &= ~(1 << subpos % 8); } } @@ -158,8 +186,10 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { } void MAX7219Component::update() { this->update_ = true; - this->max_displaybuffer_.clear(); - this->max_displaybuffer_.resize(this->num_chips_ * 8, this->bckgrnd_); + 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); } @@ -175,7 +205,7 @@ void MAX7219Component::turn_on_off(bool on_off) { } } -void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell) { +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); @@ -183,7 +213,7 @@ void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_ this->set_scroll_delay(delay); } -void MAX7219Component::scroll(bool on_off, uint8_t mode) { +void MAX7219Component::scroll(bool on_off, ScrollMode mode) { this->set_scroll(on_off); this->set_scroll_mode(mode); } @@ -196,24 +226,26 @@ void MAX7219Component::intensity(uint8_t intensity) { void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } void MAX7219Component::scroll_left() { - if (this->update_) { - this->max_displaybuffer_.push_back(this->bckgrnd_); - for (uint16_t i = 0; i < this->stepsleft_; i++) { - this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); - this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); - this->update_ = false; + 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()); } - } else { - this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); - this->max_displaybuffer_.erase(this->max_displaybuffer_.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_[chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][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) diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 02fe8b6f42..3bf934632f 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -12,6 +12,16 @@ namespace esphome { namespace max7219digit { +enum ChipLinesStyle { + ZIGZAG = 0, + SNAKE, +}; + +enum ScrollMode { + CONTINUOUS = 0, + STOP, +}; + class MAX7219Component; using max7219_writer_t = std::function; @@ -46,20 +56,22 @@ class MAX7219Component : public PollingComponent, 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(uint8_t mode) { this->scroll_mode_ = mode; }; + 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, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell); - void scroll(bool on_off, uint8_t mode); + 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); @@ -84,9 +96,12 @@ class MAX7219Component : public PollingComponent, 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}; @@ -94,11 +109,11 @@ class MAX7219Component : public PollingComponent, uint16_t scroll_delay_; uint16_t scroll_dwell_; uint16_t old_buffer_size_ = 0; - uint8_t scroll_mode_; + ScrollMode scroll_mode_; bool invert_ = false; uint8_t orientation_; uint8_t bckgrnd_ = 0x0; - std::vector max_displaybuffer_; + std::vector> max_displaybuffer_; uint32_t last_scroll_ = 0; uint16_t stepsleft_; size_t get_buffer_length_(); diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index b7adeb94d2..af834b4c40 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -35,7 +35,6 @@ void MCP23S08::dump_config() { } bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) { - uint8_t data; this->enable(); this->transfer_byte(this->device_opcode_ | 1); this->transfer_byte(reg); diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index e975670faa..744f2fbe9c 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -24,6 +24,7 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { 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); diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index ce451cbb33..e845c79a64 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -127,9 +127,6 @@ canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { } canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { - canbus::Error res; - uint8_t cfg3; - if (divisor == CLKOUT_DISABLE) { /* Turn off CLKEN */ modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, 0x00); diff --git a/esphome/components/mcp3204/__init__.py b/esphome/components/mcp3204/__init__.py new file mode 100644 index 0000000000..0536166e56 --- /dev/null +++ b/esphome/components/mcp3204/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +DEPENDENCIES = ["spi"] +MULTI_CONF = True +CODEOWNERS = ["@rsumner"] + +mcp3204_ns = cg.esphome_ns.namespace("mcp3204") +MCP3204 = mcp3204_ns.class_("MCP3204", cg.Component, spi.SPIDevice) + +CONF_REFERENCE_VOLTAGE = "reference_voltage" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MCP3204), + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } +).extend(spi.spi_device_schema(cs_pin_required=True)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp new file mode 100644 index 0000000000..44044349a3 --- /dev/null +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -0,0 +1,35 @@ +#include "mcp3204.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3204 { + +static const char *const TAG = "mcp3204"; + +float MCP3204::get_setup_priority() const { return setup_priority::HARDWARE; } + +void MCP3204::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp3204"); + this->spi_setup(); +} + +void MCP3204::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3204:"); + LOG_PIN(" CS Pin:", this->cs_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); +} + +float MCP3204::read_data(uint8_t pin) { + uint8_t adc_primary_config = 0b00000110 & 0b00000111; + uint8_t adc_secondary_config = pin << 6; + this->enable(); + this->transfer_byte(adc_primary_config); + uint8_t adc_primary_byte = this->transfer_byte(adc_secondary_config); + uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->disable(); + uint16_t digital_value = (adc_primary_byte << 8 | adc_secondary_byte) & 0b111111111111; + return float(digital_value) / 4096.000 * this->reference_voltage_; +} + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h new file mode 100644 index 0000000000..27261aa373 --- /dev/null +++ b/esphome/components/mcp3204/mcp3204.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp3204 { + +class MCP3204 : public Component, + public spi::SPIDevice { + public: + MCP3204() = default; + + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + float read_data(uint8_t pin); + + protected: + float reference_voltage_; +}; + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py new file mode 100644 index 0000000000..1d8701a91e --- /dev/null +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -0,0 +1,32 @@ +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 mcp3204_ns, MCP3204 + +AUTO_LOAD = ["voltage_sampler"] + +DEPENDENCIES = ["mcp3204"] + +MCP3204Sensor = mcp3204_ns.class_( + "MCP3204Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) +CONF_MCP3204_ID = "mcp3204_id" + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MCP3204Sensor), + cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=3), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_NUMBER], + ) + await cg.register_parented(var, config[CONF_MCP3204_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp new file mode 100644 index 0000000000..ce0fd25462 --- /dev/null +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -0,0 +1,23 @@ +#include "mcp3204_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3204 { + +static const char *const TAG = "mcp3204.sensor"; + +MCP3204Sensor::MCP3204Sensor(uint8_t pin) : pin_(pin) {} + +float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MCP3204Sensor::dump_config() { + LOG_SENSOR("", "MCP3204 Sensor", this); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + LOG_UPDATE_INTERVAL(this); +} +float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_); } +void MCP3204Sensor::update() { this->publish_state(this->sample()); } + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h new file mode 100644 index 0000000000..21c45590ab --- /dev/null +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +#include "../mcp3204.h" + +namespace esphome { +namespace mcp3204 { + +class MCP3204Sensor : public PollingComponent, + public Parented, + public sensor::Sensor, + public voltage_sampler::VoltageSampler { + public: + MCP3204Sensor(uint8_t pin); + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + uint8_t pin_; +}; + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp47a1/__init__.py b/esphome/components/mcp47a1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp47a1/mcp47a1.cpp b/esphome/components/mcp47a1/mcp47a1.cpp new file mode 100644 index 0000000000..58f3b2ac72 --- /dev/null +++ b/esphome/components/mcp47a1/mcp47a1.cpp @@ -0,0 +1,21 @@ +#include "mcp47a1.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp47a1 { + +static const char *const TAG = "mcp47a1"; + +void MCP47A1::dump_config() { + ESP_LOGCONFIG(TAG, "MCP47A1 Output:"); + LOG_I2C_DEVICE(this); +} + +void MCP47A1::write_state(float state) { + const uint8_t value = remap(state, 0.0f, 1.0f, 63, 0); + this->write_byte(0, value); +} + +} // namespace mcp47a1 +} // namespace esphome diff --git a/esphome/components/mcp47a1/mcp47a1.h b/esphome/components/mcp47a1/mcp47a1.h new file mode 100644 index 0000000000..5c02e062ad --- /dev/null +++ b/esphome/components/mcp47a1/mcp47a1.h @@ -0,0 +1,17 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace mcp47a1 { + +class MCP47A1 : public Component, public output::FloatOutput, public i2c::I2CDevice { + public: + void dump_config() override; + void write_state(float state) override; +}; + +} // namespace mcp47a1 +} // namespace esphome diff --git a/esphome/components/mcp47a1/output.py b/esphome/components/mcp47a1/output.py new file mode 100644 index 0000000000..60235107e9 --- /dev/null +++ b/esphome/components/mcp47a1/output.py @@ -0,0 +1,27 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import output, i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +mcp47a1_ns = cg.esphome_ns.namespace("mcp47a1") +MCP47A1 = mcp47a1_ns.class_("MCP47A1", output.FloatOutput, cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MCP47A1), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x2E)) +) + + +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/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/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 372d980eb0..915c640b06 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -2,6 +2,7 @@ #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" @@ -13,17 +14,20 @@ namespace esphome { namespace mdns { +static const char *const TAG = "mdns"; + #ifndef WEBSERVER_PORT #define WEBSERVER_PORT 80 // NOLINT #endif -std::vector MDNSComponent::compile_services_() { - std::vector res; +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.service_type = "_esphomelib"; service.proto = "_tcp"; service.port = api::global_api_server->get_port(); service.txt_records.push_back({"version", ESPHOME_VERSION}); @@ -50,33 +54,43 @@ std::vector MDNSComponent::compile_services_() { service.txt_records.push_back({"package_import_url", dashboard_import::get_package_import_url()}); #endif - res.push_back(service); + this->services_.push_back(service); } #endif // USE_API #ifdef USE_PROMETHEUS { MDNSService service{}; - service.service_type = "prometheus-http"; + service.service_type = "_prometheus-http"; service.proto = "_tcp"; service.port = WEBSERVER_PORT; - res.push_back(service); + this->services_.push_back(service); } #endif - if (res.empty()) { + 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.service_type = "_http"; service.proto = "_tcp"; service.port = WEBSERVER_PORT; service.txt_records.push_back({"version", ESPHOME_VERSION}); - res.push_back(service); + 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()); + } } - return res; } -std::string MDNSComponent::compile_hostname_() { return App.get_name(); } } // namespace mdns } // namespace esphome diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 985947d99c..45614d509a 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -13,7 +13,11 @@ struct MDNSTXTRecord { }; 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; @@ -22,6 +26,7 @@ struct MDNSService { class MDNSComponent : public Component { public: void setup() override; + void dump_config() override; #if defined(USE_ESP8266) && defined(USE_ARDUINO) void loop() override; @@ -29,8 +34,9 @@ class MDNSComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } protected: - std::vector compile_services_(); - std::string compile_hostname_(); + std::vector services_{}; + std::string hostname_; + void compile_records_(); }; } // namespace mdns diff --git a/esphome/components/mdns/mdns_esp32_arduino.cpp b/esphome/components/mdns/mdns_esp32_arduino.cpp index 4d13b7321a..6a66beef92 100644 --- a/esphome/components/mdns/mdns_esp32_arduino.cpp +++ b/esphome/components/mdns/mdns_esp32_arduino.cpp @@ -7,13 +7,12 @@ namespace esphome { namespace mdns { -static const char *const TAG = "mdns"; - void MDNSComponent::setup() { - MDNS.begin(compile_hostname_().c_str()); + this->compile_records_(); - auto services = compile_services_(); - for (const auto &service : services) { + 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()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 48f31f1bbf..ff305f907a 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -9,17 +9,28 @@ namespace esphome { namespace mdns { -static const char *const TAG = "mdns"; - void MDNSComponent::setup() { - network::IPAddress addr = network::get_ip_address(); - MDNS.begin(compile_hostname_().c_str(), (uint32_t) addr); + this->compile_records_(); - auto services = compile_services_(); - for (const auto &service : services) { - MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port); + 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.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str()); + MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str()); } } } diff --git a/esphome/components/mdns/mdns_esp_idf.cpp b/esphome/components/mdns/mdns_esp_idf.cpp index 17874f1ffe..40d9f1d5f3 100644 --- a/esphome/components/mdns/mdns_esp_idf.cpp +++ b/esphome/components/mdns/mdns_esp_idf.cpp @@ -11,18 +11,19 @@ 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)); + ESP_LOGW(TAG, "mDNS init failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - mdns_hostname_set(compile_hostname_().c_str()); - mdns_instance_name_set(compile_hostname_().c_str()); + mdns_hostname_set(this->hostname_.c_str()); + mdns_instance_name_set(this->hostname_.c_str()); - auto services = compile_services_(); - for (const auto &service : services) { + for (const auto &service : this->services_) { std::vector txt_records; for (const auto &record : service.txt_records) { mdns_txt_item_t it{}; diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/ac_adapter.cpp similarity index 98% rename from esphome/components/midea/adapter.cpp rename to esphome/components/midea/ac_adapter.cpp index a3f19dbda8..2837713c35 100644 --- a/esphome/components/midea/adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -1,10 +1,11 @@ #ifdef USE_ARDUINO #include "esphome/core/log.h" -#include "adapter.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"; @@ -171,6 +172,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); } +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/ac_adapter.h similarity index 95% rename from esphome/components/midea/adapter.h rename to esphome/components/midea/ac_adapter.h index 2497cbbe5b..c17894ae31 100644 --- a/esphome/components/midea/adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -2,12 +2,15 @@ #ifdef USE_ARDUINO +// MideaUART #include + #include "esphome/components/climate/climate_traits.h" -#include "appliance_base.h" +#include "air_conditioner.h" namespace esphome { namespace midea { +namespace ac { using MideaMode = dudanov::midea::ac::Mode; using MideaSwingMode = dudanov::midea::ac::SwingMode; @@ -41,6 +44,7 @@ class Converters { static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/ac_automations.h similarity index 97% rename from esphome/components/midea/automations.h rename to esphome/components/midea/ac_automations.h index 5b638286ac..d4ed2e7168 100644 --- a/esphome/components/midea/automations.h +++ b/esphome/components/midea/ac_automations.h @@ -7,6 +7,7 @@ namespace esphome { namespace midea { +namespace ac { template class MideaActionBase : public Action { public: @@ -55,6 +56,7 @@ template class PowerOffAction : public MideaActionBase { void play(Ts... x) override { this->parent_->do_power_off(); } }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 103b852936..dd48f640a2 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -2,13 +2,11 @@ #include "esphome/core/log.h" #include "air_conditioner.h" -#include "adapter.h" -#ifdef USE_REMOTE_TRANSMITTER -#include "midea_ir.h" -#endif +#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)) @@ -122,7 +120,7 @@ void AirConditioner::dump_config() { void AirConditioner::do_follow_me(float temperature, bool beeper) { #ifdef USE_REMOTE_TRANSMITTER IrFollowMeData data(static_cast(lroundf(temperature)), beeper); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif @@ -131,7 +129,7 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) { void AirConditioner::do_swing_step() { #ifdef USE_REMOTE_TRANSMITTER IrSpecialData data(0x01); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif @@ -143,13 +141,14 @@ void AirConditioner::do_display_toggle() { } else { #ifdef USE_REMOTE_TRANSMITTER IrSpecialData data(0x08); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif } } +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 8dfb9dcb3d..a6023b78bb 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -2,17 +2,25 @@ #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 { +class AirConditioner : public ApplianceBase, public climate::Climate { public: void dump_config() override; void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } @@ -31,15 +39,26 @@ class AirConditioner : public ApplianceBase 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 diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 88a722e389..060cbd996b 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -2,84 +2,97 @@ #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" -#ifdef USE_REMOTE_TRANSMITTER -#include "esphome/components/remote_base/midea_protocol.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#endif -#include -#include +#include "ir_transmitter.h" namespace esphome { namespace midea { -using climate::ClimatePreset; -using climate::ClimateTraits; -using climate::ClimateMode; -using climate::ClimateSwingMode; -using climate::ClimateFanMode; +/* Stream from UART component */ +class UARTStream : public Stream { + public: + void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; } -template -class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate, public Stream { + /* 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); + 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); }); } - bool can_proceed() override { - return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; - } - float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } - void setup() override { this->base_.setup(); } - void loop() override { this->base_.loop(); } + +#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); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } virtual void on_status_change() = 0; -#ifdef USE_REMOTE_TRANSMITTER - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void transmit_ir(remote_base::MideaData &data) { - data.finalize(); - auto transmit = this->transmitter_->transmit(); - remote_base::MideaProtocol().encode(transmit.get_data(), data); - transmit.perform(); - } -#endif - - int available() override { return uart::UARTDevice::available(); } - int read() override { return uart::UARTDevice::read(); } - int peek() override { return uart::UARTDevice::peek(); } - void flush() override { uart::UARTDevice::flush(); } - size_t write(uint8_t data) override { return uart::UARTDevice::write(data); } protected: T base_; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + UARTStream stream_; #ifdef USE_REMOTE_TRANSMITTER - remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; + IrTransmitter transmitter_; #endif }; diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 08e82025b6..46c0019efa 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -40,9 +40,9 @@ AUTO_LOAD = ["sensor"] CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_POWER_USAGE = "power_usage" CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ns = cg.esphome_ns.namespace("midea") -AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) -Capabilities = midea_ns.namespace("Constants") +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): @@ -156,13 +156,13 @@ CONFIG_SCHEMA = cv.All( ) # Actions -FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) -DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) -SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) -BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) -BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) -PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) -PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) +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( { diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/ir_transmitter.h similarity index 57% rename from esphome/components/midea/midea_ir.h rename to esphome/components/midea/ir_transmitter.h index abd4324bcc..a8b89f9b7b 100644 --- a/esphome/components/midea/midea_ir.h +++ b/esphome/components/midea/ir_transmitter.h @@ -7,6 +7,7 @@ namespace esphome { namespace midea { +using remote_base::RemoteTransmitterBase; using IrData = remote_base::MideaData; class IrFollowMeData : public IrData { @@ -22,12 +23,12 @@ class IrFollowMeData : public IrData { } /* 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; } + uint8_t temp() const { return this->get_value_(4) - 1; } + void set_temp(uint8_t val) { this->set_value_(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); } + bool beeper() const { return this->get_value_(3, 128); } + void set_beeper(bool val) { this->set_mask_(3, val, 128); } protected: static const uint8_t MAX_TEMP = 37; @@ -38,6 +39,20 @@ class IrSpecialData : public IrData { 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 diff --git a/esphome/components/midea_ir/__init__.py b/esphome/components/midea_ir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py new file mode 100644 index 0000000000..140e4ee4e0 --- /dev/null +++ b/esphome/components/midea_ir/climate.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir", "coolix"] +CODEOWNERS = ["@dudanov"] + +midea_ir_ns = cg.esphome_ns.namespace("midea_ir") +MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MideaIR), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/midea_ir/midea_data.h b/esphome/components/midea_ir/midea_data.h new file mode 100644 index 0000000000..0f7e24907d --- /dev/null +++ b/esphome/components/midea_ir/midea_data.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/components/remote_base/midea_protocol.h" +#include "esphome/components/climate/climate_mode.h" + +namespace esphome { +namespace midea_ir { + +using climate::ClimateMode; +using climate::ClimateFanMode; +using remote_base::MideaData; + +class ControlData : public MideaData { + public: + // Default constructor (power: ON, mode: AUTO, fan: AUTO, temp: 25C) + ControlData() : MideaData({MIDEA_TYPE_CONTROL, 0x82, 0x48, 0xFF, 0xFF}) {} + // Copy from Base + ControlData(const MideaData &data) : MideaData(data) {} + + void set_temp(float temp); + float get_temp() const; + + void set_mode(ClimateMode mode); + ClimateMode get_mode() const; + + void set_fan_mode(ClimateFanMode mode); + ClimateFanMode get_fan_mode() const; + + void set_sleep_preset(bool value) { this->set_mask_(1, value, 64); } + bool get_sleep_preset() const { return this->get_value_(1, 64); } + + void set_fahrenheit(bool value) { this->set_mask_(2, value, 32); } + bool get_fahrenheit() const { return this->get_value_(2, 32); } + + void fix(); + + protected: + enum Mode : uint8_t { + MODE_COOL, + MODE_DRY, + MODE_AUTO, + MODE_HEAT, + MODE_FAN_ONLY, + }; + enum FanMode : uint8_t { + FAN_AUTO, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + }; + void set_fan_mode_(FanMode mode) { this->set_value_(1, mode, 3, 3); } + FanMode get_fan_mode_() const { return static_cast(this->get_value_(1, 3, 3)); } + void set_mode_(Mode mode) { this->set_value_(1, mode, 7); } + Mode get_mode_() const { return static_cast(this->get_value_(1, 7)); } + void set_power_(bool value) { this->set_mask_(1, value, 128); } + bool get_power_() const { return this->get_value_(1, 128); } +}; + +class FollowMeData : public MideaData { + public: + // Default constructor (temp: 30C, beeper: off) + FollowMeData() : MideaData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} + // Copy from Base + FollowMeData(const MideaData &data) : MideaData(data) {} + // Direct from temperature and beeper values + FollowMeData(uint8_t temp, bool beeper = false) : FollowMeData() { + this->set_temp(temp); + this->set_beeper(beeper); + } + + /* TEMPERATURE */ + uint8_t temp() const { return this->get_value_(4) - 1; } + void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } + + /* BEEPER */ + bool beeper() const { return this->get_value_(3, 128); } + void set_beeper(bool value) { this->set_mask_(3, value, 128); } + + protected: + static const uint8_t MAX_TEMP = 37; +}; + +class SpecialData : public MideaData { + public: + SpecialData(uint8_t code) : MideaData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} + static const uint8_t VSWING_STEP = 1; + static const uint8_t VSWING_TOGGLE = 2; + static const uint8_t TURBO_TOGGLE = 9; +}; + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp new file mode 100644 index 0000000000..5e507cbbb0 --- /dev/null +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -0,0 +1,201 @@ +#include "midea_ir.h" +#include "midea_data.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/components/coolix/coolix.h" + +namespace esphome { +namespace midea_ir { + +static const char *const TAG = "midea_ir.climate"; + +void ControlData::set_temp(float temp) { + uint8_t min; + if (this->get_fahrenheit()) { + min = MIDEA_TEMPF_MIN; + temp = esphome::clamp(celsius_to_fahrenheit(temp), MIDEA_TEMPF_MIN, MIDEA_TEMPF_MAX); + } else { + min = MIDEA_TEMPC_MIN; + temp = esphome::clamp(temp, MIDEA_TEMPC_MIN, MIDEA_TEMPC_MAX); + } + this->set_value_(2, lroundf(temp) - min, 31); +} + +float ControlData::get_temp() const { + const uint8_t temp = this->get_value_(2, 31); + if (this->get_fahrenheit()) + return fahrenheit_to_celsius(static_cast(temp + MIDEA_TEMPF_MIN)); + return static_cast(temp + MIDEA_TEMPC_MIN); +} + +void ControlData::fix() { + // In FAN_AUTO, modes COOL, HEAT and FAN_ONLY bit #5 in byte #1 must be set + const uint8_t value = this->get_value_(1, 31); + if (value == 0 || value == 3 || value == 4) + this->set_mask_(1, true, 32); + // In FAN_ONLY mode we need to set all temperature bits + if (this->get_mode_() == MODE_FAN_ONLY) + this->set_mask_(2, true, 31); +} + +void ControlData::set_mode(ClimateMode mode) { + switch (mode) { + case ClimateMode::CLIMATE_MODE_OFF: + this->set_power_(false); + return; + case ClimateMode::CLIMATE_MODE_COOL: + this->set_mode_(MODE_COOL); + break; + case ClimateMode::CLIMATE_MODE_DRY: + this->set_mode_(MODE_DRY); + break; + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + this->set_mode_(MODE_FAN_ONLY); + break; + case ClimateMode::CLIMATE_MODE_HEAT: + this->set_mode_(MODE_HEAT); + break; + default: + this->set_mode_(MODE_AUTO); + break; + } + this->set_power_(true); +} + +ClimateMode ControlData::get_mode() const { + if (!this->get_power_()) + return ClimateMode::CLIMATE_MODE_OFF; + switch (this->get_mode_()) { + case MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + case MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + case MODE_FAN_ONLY: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + case MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + default: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + } +} + +void ControlData::set_fan_mode(ClimateFanMode mode) { + switch (mode) { + case ClimateFanMode::CLIMATE_FAN_LOW: + this->set_fan_mode_(FAN_LOW); + break; + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + this->set_fan_mode_(FAN_MEDIUM); + break; + case ClimateFanMode::CLIMATE_FAN_HIGH: + this->set_fan_mode_(FAN_HIGH); + break; + default: + this->set_fan_mode_(FAN_AUTO); + break; + } +} + +ClimateFanMode ControlData::get_fan_mode() const { + switch (this->get_fan_mode_()) { + case FAN_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + case FAN_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + case FAN_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + default: + return ClimateFanMode::CLIMATE_FAN_AUTO; + } +} + +void MideaIR::control(const climate::ClimateCall &call) { + // swing and preset resets after unit powered off + if (call.get_mode() == climate::CLIMATE_MODE_OFF) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + this->preset = climate::CLIMATE_PRESET_NONE; + } else if (call.get_swing_mode().has_value() && ((*call.get_swing_mode() == climate::CLIMATE_SWING_OFF && + this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || + (*call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL && + this->swing_mode == climate::CLIMATE_SWING_OFF))) { + this->swing_ = true; + } else if (call.get_preset().has_value() && + ((*call.get_preset() == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || + (*call.get_preset() == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { + this->boost_ = true; + } + climate_ir::ClimateIR::control(call); +} + +void MideaIR::transmit_(MideaData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); +} + +void MideaIR::transmit_state() { + if (this->swing_) { + SpecialData data(SpecialData::VSWING_TOGGLE); + this->transmit_(data); + this->swing_ = false; + return; + } + if (this->boost_) { + SpecialData data(SpecialData::TURBO_TOGGLE); + this->transmit_(data); + this->boost_ = false; + return; + } + ControlData data; + data.set_fahrenheit(this->fahrenheit_); + data.set_temp(this->target_temperature); + data.set_mode(this->mode); + data.set_fan_mode(this->fan_mode.value_or(ClimateFanMode::CLIMATE_FAN_AUTO)); + data.set_sleep_preset(this->preset == climate::CLIMATE_PRESET_SLEEP); + data.fix(); + this->transmit_(data); +} + +bool MideaIR::on_receive(remote_base::RemoteReceiveData data) { + auto midea = remote_base::MideaProtocol().decode(data); + if (midea.has_value()) + return this->on_midea_(*midea); + return coolix::CoolixClimate::on_coolix(this, data); +} + +bool MideaIR::on_midea_(const MideaData &data) { + ESP_LOGV(TAG, "Decoded Midea IR data: %s", data.to_string().c_str()); + if (data.type() == MideaData::MIDEA_TYPE_CONTROL) { + const ControlData status = data; + if (status.get_mode() != climate::CLIMATE_MODE_FAN_ONLY) + this->target_temperature = status.get_temp(); + this->mode = status.get_mode(); + this->fan_mode = status.get_fan_mode(); + if (status.get_sleep_preset()) + this->preset = climate::CLIMATE_PRESET_SLEEP; + else if (this->preset == climate::CLIMATE_PRESET_SLEEP) + this->preset = climate::CLIMATE_PRESET_NONE; + this->publish_state(); + return true; + } + if (data.type() == MideaData::MIDEA_TYPE_SPECIAL) { + switch (data[1]) { + case SpecialData::VSWING_TOGGLE: + this->swing_mode = this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? climate::CLIMATE_SWING_OFF + : climate::CLIMATE_SWING_VERTICAL; + break; + case SpecialData::TURBO_TOGGLE: + this->preset = this->preset == climate::CLIMATE_PRESET_BOOST ? climate::CLIMATE_PRESET_NONE + : climate::CLIMATE_PRESET_BOOST; + break; + } + this->publish_state(); + return true; + } + + return false; +} + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/midea_ir/midea_ir.h b/esphome/components/midea_ir/midea_ir.h new file mode 100644 index 0000000000..b89b2a7efc --- /dev/null +++ b/esphome/components/midea_ir/midea_ir.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" +#include "midea_data.h" + +namespace esphome { +namespace midea_ir { + +// Temperature +const uint8_t MIDEA_TEMPC_MIN = 17; // Celsius +const uint8_t MIDEA_TEMPC_MAX = 30; // Celsius +const uint8_t MIDEA_TEMPF_MIN = 62; // Fahrenheit +const uint8_t MIDEA_TEMPF_MAX = 86; // Fahrenheit + +class MideaIR : public climate_ir::ClimateIR { + public: + MideaIR() + : climate_ir::ClimateIR( + MIDEA_TEMPC_MIN, MIDEA_TEMPC_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_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + + /// Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = value ? 0.5f : 1.0f; + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + void transmit_(MideaData &data); + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool on_midea_(const MideaData &data); + bool fahrenheit_{false}; + bool swing_{false}; + bool boost_{false}; +}; + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 1f6d868baf..60ce50097c 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -69,7 +69,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint8_t data_len = raw[2]; 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) { + if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { data_offset = 2; data_len = 4; } @@ -96,23 +96,27 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); return false; } - - waiting_for_response = 0; std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); - bool found = false; for (auto *device : this->devices_) { if (device->address_ == address) { // Is it an error response? if ((function_code & 0x80) == 0x80) { - ESP_LOGW(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); - device->on_modbus_error(function_code & 0x7F, raw[2]); + 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); } @@ -135,7 +139,9 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address uint8_t payload_len, const uint8_t *payload) { static const size_t MAX_VALUES = 128; - if (number_of_entities > MAX_VALUES) { + // 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; } @@ -175,7 +181,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address this->flow_control_pin_->digital_write(false); waiting_for_response = address; last_send_ = millis(); - ESP_LOGV(TAG, "Modbus write: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty(data).c_str()); } // Helper function for lambdas @@ -196,6 +202,7 @@ void Modbus::send_raw(const std::vector &payload) { if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); waiting_for_response = payload[0]; + ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty(payload).c_str()); last_send_ = millis(); } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 7a69029dab..f919cb0678 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -1,10 +1,21 @@ +import binascii import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import modbus -from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.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"] @@ -36,10 +47,16 @@ MODBUS_FUNCTION_CODE = { ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") -MODBUS_REGISTER_TYPE = { + +MODBUS_WRITE_REGISTER_TYPE = { + "custom": ModbusRegisterType.CUSTOM, "coil": ModbusRegisterType.COIL, - "discrete_input": ModbusRegisterType.DISCRETE, "holding": ModbusRegisterType.HOLDING, +} + +MODBUS_REGISTER_TYPE = { + **MODBUS_WRITE_REGISTER_TYPE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "read": ModbusRegisterType.READ, } @@ -61,6 +78,21 @@ SENSOR_VALUE_TYPE = { "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 @@ -80,6 +112,100 @@ CONFIG_SCHEMA = cv.All( ) +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])) @@ -104,11 +230,3 @@ def function_code_to_register(function_code): "write_multiple_registers": ModbusRegisterType.HOLDING, } return FUNCTION_CODE_TYPE_MAP[function_code] - - -def find_by_value(dict, find_value): - for (key, value) in MODBUS_REGISTER_TYPE.items(): - print(find_value, value) - if find_value == value: - return key - return "not found" diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index d46ff71f2d..557d76479d 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -2,16 +2,18 @@ from esphome.components import binary_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, MODBUS_REGISTER_TYPE, ) from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, @@ -27,30 +29,20 @@ ModbusBinarySensor = modbus_controller_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.BINARY_SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusBinarySensor), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, _ = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], @@ -65,17 +57,4 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusBinarySensor.operator("ptr"), "item"), - (cg.float_, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(bool), - ) - cg.add(var.set_template(template_)) + await add_modbus_base_properties(var, config, ModbusBinarySensor, bool, bool) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp index 81066b3f5c..c3eb3d4411 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -13,8 +13,6 @@ void ModbusBinarySensor::parse_and_publish(const std::vector &data) { switch (this->register_type) { case ModbusRegisterType::DISCRETE_INPUT: - value = coil_from_vector(this->offset, data); - break; case ModbusRegisterType::COIL: // offset for coil is the actual number of the coil not the byte offset value = coil_from_vector(this->offset, data); diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index c516d6b916..21afbc7053 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -31,12 +31,11 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, void dump_config() override; - using transform_func_t = - optional(ModbusBinarySensor *, bool, const std::vector &)>>; + using transform_func_t = std::function(ModbusBinarySensor *, bool, const std::vector &)>; void set_template(transform_func_t &&f) { this->transform_func_ = f; } protected: - transform_func_t transform_func_{nullopt}; + optional transform_func_{nullopt}; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 3cd114e673..baf72efb94 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -1,6 +1,7 @@ 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" @@ -9,5 +10,6 @@ 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 index 70b5bf8eae..d07a6d5335 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -24,12 +24,22 @@ bool ModbusController::send_next_command_() { if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) { auto &command = command_queue_.front(); - ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, - command->register_address, command->register_count); - command->send(); - this->last_command_timestamp_ = millis(); - if (!command->on_data_func) { // No handler remove from queue directly after sending + // remove from queue if command was sent too often + if (command->send_countdown < 1) { + ESP_LOGD( + TAG, + "Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue", + this->address_, command->register_address, command->send_countdown); command_queue_.pop_front(); + } else { + ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, + command->register_address, command->register_count); + command->send(); + this->last_command_timestamp_ = millis(); + // remove from queue if no handler is defined + if (!command->on_data_func) { + command_queue_.pop_front(); + } } } return (!command_queue_.empty()); @@ -69,30 +79,28 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ } } +SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { + auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { + return (r.start_address == start_address && r.register_type == register_type); + }); + + if (reg_it == register_ranges_.end()) { + ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address); + } else { + return reg_it->sensors; + } + + // not found + return {}; +} void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); - auto vec_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { - return (r.start_address == start_address && r.register_type == register_type); - }); - - if (vec_it == register_ranges_.end()) { - ESP_LOGE(TAG, "Handle incoming data : No matching range for sensor found - start_address : 0x%X", start_address); - return; - } - auto map_it = sensormap_.find(vec_it->first_sensorkey); - if (map_it == sensormap_.end()) { - ESP_LOGE(TAG, "Handle incoming data : No sensor found in at start_address : 0x%X (0x%llX)", start_address, - vec_it->first_sensorkey); - return; - } // loop through all sensors with the same start address - while (map_it != sensormap_.end() && map_it->second->start_address == start_address) { - if (map_it->second->register_type == register_type) { - map_it->second->parse_and_publish(data); - } - map_it++; + auto sensors = find_sensors_(register_type, start_address); + for (auto sensor : sensors) { + sensor->parse_and_publish(data); } } @@ -101,7 +109,7 @@ void ModbusController::queue_command(const ModbusCommandItem &command) { // 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) { + item->register_type == command.register_type && item->function_code == command.function_code) { ESP_LOGW(TAG, "Duplicate modbus command found"); // update the payload of the queued command // replaces a previous command @@ -116,9 +124,24 @@ void ModbusController::update_range_(RegisterRange &r) { ESP_LOGV(TAG, "Range : %X Size: %x (%d) skip: %d", r.start_address, r.register_count, (int) r.register_type, r.skip_updates_counter); if (r.skip_updates_counter == 0) { - ModbusCommandItem command_item = - ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count); - queue_command(command_item); + // if a custom command is used the user supplied custom_data is only available in the SensorItem. + if (r.register_type == ModbusRegisterType::CUSTOM) { + auto sensors = this->find_sensors_(r.register_type, r.start_address); + if (!sensors.empty()) { + auto sensor = sensors.cbegin(); + auto command_item = ModbusCommandItem::create_custom_command( + this, (*sensor)->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 = (*sensor)->start_address; + command_item.register_count = (*sensor)->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--; @@ -141,102 +164,110 @@ void ModbusController::update() { } } -// walk through the sensors and determine the registerranges to read +// walk through the sensors and determine the register ranges to read size_t ModbusController::create_register_ranges_() { register_ranges_.clear(); - uint8_t n = 0; - if (sensormap_.empty()) { + if (sensorset_.empty()) { + ESP_LOGW(TAG, "No sensors registered"); 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(); + // iterator is sorted see SensorItemsComparator for details + auto ix = sensorset_.begin(); + RegisterRange r = {}; + uint8_t buffer_offset = 0; + SensorItem *prev = nullptr; + while (ix != sensorset_.end()) { + SensorItem *curr = *ix; - // 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; + ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, + curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr); + + if (r.register_count == 0) { + // this is the first register in range + r.start_address = curr->start_address; + r.register_count = curr->register_count; + r.register_type = curr->register_type; + r.sensors.insert(curr); + r.skip_updates = curr->skip_updates; + r.skip_updates_counter = 0; + buffer_offset = curr->get_register_size(); + + ESP_LOGV(TAG, "Started new range"); } 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(); + // this is not the first register in range so it might be possible + // to reuse the last register or extend the current range + if (!curr->force_new_range && r.register_type == curr->register_type && + curr->register_type != ModbusRegisterType::CUSTOM) { + if (curr->start_address == (r.start_address + r.register_count - prev->register_count) && + curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) { + // this register can re-use the data from the previous register + + // remove this sensore because start_address is changed (sort-order) + ix = sensorset_.erase(ix); + + curr->start_address = r.start_address; + curr->offset += prev->offset; + + sensorset_.insert(curr); + // move iterator backwards because it will be incremented later + ix--; + + ESP_LOGV(TAG, "Re-use previous register - change to register: 0x%X %d offset=%u", curr->start_address, + curr->register_count, curr->offset); + } else if (curr->start_address == (r.start_address + r.register_count)) { + // this register can extend the current range + + // remove this sensore because start_address is changed (sort-order) + ix = sensorset_.erase(ix); + + curr->start_address = r.start_address; + curr->offset += buffer_offset; + buffer_offset += curr->get_register_size(); + r.register_count += curr->register_count; + + sensorset_.insert(curr); + // move iterator backwards because it will be incremented later + ix--; + + ESP_LOGV(TAG, "Extend range - change to register: 0x%X %d offset=%u", curr->start_address, + curr->register_count, curr->offset); + } } + } + + if (curr->start_address == r.start_address) { // 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); + if (curr->skip_updates != 0) { + if (r.skip_updates != 0) { + r.skip_updates = std::min(r.skip_updates, curr->skip_updates); } else { - skip_updates = ix->second->skip_updates; + r.skip_updates = curr->skip_updates; } } + + // add sensor to this range + r.sensors.insert(curr); + + ix++; + } else { + ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + register_ranges_.push_back(r); + r = {}; + buffer_offset = 0; + // do not increment the iterator here because the current sensor has to be re-evaluated } - prev = ix++; + + prev = curr; } - // 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; + + if (r.register_count > 0) { + // Add the last range 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(); } @@ -245,9 +276,15 @@ void ModbusController::dump_config() { 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()); + for (auto &it : sensorset_) { + ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d", + static_cast(it->register_type), it->start_address, it->offset, it->register_count, + it->get_register_size()); + } + ESP_LOGCONFIG(TAG, "ranges"); + for (auto &it : register_ranges_) { + ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), + it.start_address, it.register_count, it.skip_updates); } #endif } @@ -271,11 +308,11 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty 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()); +void ModbusController::dump_sensors_() { + ESP_LOGV(TAG, "sensors"); + for (auto &it : sensorset_) { + ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count, + it->get_register_size(), it->offset); } } @@ -422,6 +459,7 @@ bool ModbusCommandItem::send() { 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; } @@ -549,6 +587,9 @@ float payload_to_float(const std::vector &data, SensorValueType sensor_ 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; } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 4b5f4337db..6dbabac71e 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -6,7 +6,7 @@ #include "esphome/components/modbus/modbus.h" #include -#include +#include #include #include @@ -37,7 +37,7 @@ enum class ModbusFunctionCode { READ_FIFO_QUEUE = 0x18, // not implemented }; -enum class ModbusRegisterType : int { +enum class ModbusRegisterType : uint8_t { CUSTOM = 0x0, COIL = 0x01, DISCRETE_INPUT = 0x02, @@ -62,15 +62,6 @@ enum class SensorValueType : uint8_t { 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: @@ -102,26 +93,12 @@ inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_ return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; break; case ModbusRegisterType::READ: - return ModbusFunctionCode::CUSTOM; - break; default: return ModbusFunctionCode::CUSTOM; break; } } -/** All sensors are stored in a map - * to enable binary sensors for values encoded as bits in the same register the key of each sensor - * the key is a 64 bit integer that combines the register properties - * sensormap_ is sorted by this key. The key ensures the correct order when creating consequtive ranges - * Format: function_code (8 bit) | start address (16 bit)| offset (8bit)| bitmask (32 bit) - */ -inline uint64_t calc_key(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset = 0, - uint32_t bitmask = 0) { - return uint64_t((uint16_t(register_type) << 24) + (uint32_t(start_address) << 8) + (offset & 0xFF)) << 32 | bitmask; -} -inline uint16_t register_from_key(uint64_t key) { return (key >> 40) & 0xFFFF; } - inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } /** Get a byte from a hex string @@ -221,7 +198,7 @@ template N mask_and_shift_by_rightbit(N data, uint32_t mask) { if (result == 0) { return result; } - for (int pos = 0; pos < sizeof(N) << 3; pos++) { + for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { if ((mask & (1 << pos)) != 0) return result >> pos; } @@ -247,53 +224,77 @@ float payload_to_float(const std::vector &data, SensorValueType sensor_ class ModbusController; -struct SensorItem { +class SensorItem { + public: + virtual void parse_and_publish(const std::vector &data) = 0; + + void set_custom_data(const std::vector &data) { custom_data = data; } + 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}; +}; - virtual void parse_and_publish(const std::vector &data) = 0; - - uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } - - size_t virtual get_register_size() const { - size_t size = 0; - switch (sensor_value_type) { - case SensorValueType::BIT: - size = 1; - break; - case SensorValueType::U_WORD: - case SensorValueType::S_WORD: - size = 2; - break; - case SensorValueType::U_DWORD: - case SensorValueType::S_DWORD: - case SensorValueType::U_DWORD_R: - case SensorValueType::S_DWORD_R: - case SensorValueType::FP32: - case SensorValueType::FP32_R: - size = 4; - break; - case SensorValueType::U_QWORD: - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD: - case SensorValueType::S_QWORD_R: - size = 8; - break; - case SensorValueType::RAW: - size = this->register_count * 2; +// ModbusController::create_register_ranges_ tries to optimize register range +// for this the sensors must be ordered by register_type, start_address and bitmask +class SensorItemsComparator { + public: + bool operator()(const SensorItem *lhs, const SensorItem *rhs) const { + // first sort according to register type + if (lhs->register_type != rhs->register_type) { + return lhs->register_type < rhs->register_type; } - return size; + + // ensure that sensor with force_new_range set are before the others + if (lhs->force_new_range != rhs->force_new_range) { + return lhs->force_new_range > rhs->force_new_range; + } + + // sort by start address + if (lhs->start_address != rhs->start_address) { + return lhs->start_address < rhs->start_address; + } + + // sort by offset (ensures update of sensors in ascending order) + if (lhs->offset != rhs->offset) { + return lhs->offset < rhs->offset; + } + + // The pointer to the sensor is used last to ensure that + // multiple sensors with the same values can be added with a stable sort order. + return lhs < rhs; } }; -struct ModbusCommandItem { +using SensorSet = std::set; + +struct RegisterRange { + uint16_t start_address; + ModbusRegisterType register_type; + uint8_t register_count; + uint8_t skip_updates; // the config value + SensorSet sensors; // all sensors of this range + uint8_t skip_updates_counter; // the running value +}; + +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; @@ -303,7 +304,9 @@ struct ModbusCommandItem { on_data_func; std::vector payload = {}; bool send(); - + // wrong commands (esp. custom commands) can block the send queue + // limit the number of repeats + uint8_t send_countdown{MAX_SEND_REPEATS}; /// factory methods /** Create modbus read command * Function code 02-04 @@ -399,8 +402,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// 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 add_sensor_item(SensorItem *item) { sensorset_.insert(item); } + /// called when a modbus response was parsed without errors void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; @@ -416,6 +419,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { 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 + SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const; /// submit the read command for the address range to the send queue void update_range_(RegisterRange &r); /// parse incoming modbus data @@ -425,10 +430,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// 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_(); + void dump_sensors_(); /// Collection of all sensors for this component - /// see calc_key how the key is contructed - std::map sensormap_; + SensorSet sensorset_; /// Continous range of modbus registers std::vector register_ranges_; /// Hold the pending requests to be sent diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index c7919bb972..4ad6601fee 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -4,29 +4,28 @@ from esphome.components import number from esphome.const import ( CONF_ADDRESS, CONF_ID, - CONF_LAMBDA, CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MULTIPLY, - CONF_OFFSET, CONF_STEP, ) from .. import ( + add_modbus_base_properties, modbus_controller_ns, - ModbusController, - SENSOR_VALUE_TYPE, + modbus_calc_properties, + ModbusItemBaseSchema, SensorItem, + SENSOR_VALUE_TYPE, ) - from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, + CONF_CUSTOM_COMMAND, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, - CONF_REGISTER_COUNT, CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) @@ -39,22 +38,6 @@ ModbusNumber = modbus_controller_ns.class_( "ModbusNumber", cg.Component, number.Number, SensorItem ) -TYPE_REGISTER_MAP = { - "RAW": 1, - "U_WORD": 1, - "S_WORD": 1, - "U_DWORD": 2, - "U_DWORD_R": 2, - "S_DWORD": 2, - "S_DWORD_R": 2, - "U_QWORD": 4, - "U_QWORDU_R": 4, - "S_QWORD": 4, - "U_QWORD_R": 4, - "FP32": 2, - "FP32_R": 2, -} - def validate_min_max(config): if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: @@ -66,45 +49,38 @@ def validate_min_max(config): 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( + number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusNumber), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), - cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.GenerateID(): cv.declare_id(ModbusNumber), # 24 bits are the maximum value for fp32 before precison is lost # 0x00FFFFFF = 16777215 cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_, cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_, cv.Optional(CONF_STEP, default=1): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate_min_max, + validate_modbus_number, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] - value_type = config[CONF_VALUE_TYPE] - reg_count = config[CONF_REGISTER_COUNT] - if reg_count == 0: - reg_count = TYPE_REGISTER_MAP[value_type] + byte_offset, reg_count = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_ADDRESS], @@ -130,28 +106,16 @@ async def to_code(config): cg.add(var.set_parent(parent)) cg.add(parent.add_sensor_item(var)) - if CONF_LAMBDA in config: + 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_LAMBDA], + config[CONF_WRITE_LAMBDA], [ (ModbusNumber.operator("ptr"), "item"), (cg.float_, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), ], return_type=cg.optional.template(float), ) - cg.add(var.set_template(template_)) - if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_WRITE_LAMBDA], - [ - (ModbusNumber.operator("ptr"), "item"), - (cg.float_, "x"), - (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), - ], - return_type=cg.optional.template(float), - ) - cg.add(var.set_write_template(template_)) + 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 index 95c6ac6f6a..5e977f5df4 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -8,12 +8,7 @@ namespace modbus_controller { static const char *const TAG = "modbus.number"; void ModbusNumber::parse_and_publish(const std::vector &data) { - union { - float float_value; - uint32_t raw; - } raw_to_float; - - float result = payload_to_float(data, *this); + 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 @@ -31,13 +26,8 @@ void ModbusNumber::parse_and_publish(const std::vector &data) { } void ModbusNumber::control(float value) { - union { - float float_value; - uint32_t raw; - } raw_to_float; - std::vector data; - auto original_value = value; + float write_value = value; // Is there are lambda configured? if (this->write_transform_func_.has_value()) { // data is passed by reference @@ -46,28 +36,32 @@ void ModbusNumber::control(float value) { auto val = (*this->write_transform_func_)(this, value, data); if (val.has_value()) { ESP_LOGV(TAG, "Value overwritten by lambda"); - value = val.value(); + write_value = val.value(); } else { ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); return; } } else { - value = multiply_by_ * value; + write_value = multiply_by_ * write_value; } // lambda didn't set payload if (data.empty()) { - data = float_to_payload(value, this->sensor_value_type); + 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, value); + this->get_name().c_str(), this->start_address, this->register_count, value, write_value); // Create and send the write command - auto write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, - this->register_count, data); - + 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) { diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 271bbfac50..c678cd00cc 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -35,6 +35,7 @@ class ModbusNumber : public number::Number, public Component, public SensorItem 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; @@ -42,6 +43,7 @@ class ModbusNumber : public number::Number, public Component, public SensorItem optional write_transform_func_; ModbusController *parent_; float multiply_by_{1.0}; + bool use_write_multiple_{false}; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 9c41fc011c..1bf989ce8b 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,74 +1,109 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output - from esphome.const import ( CONF_ADDRESS, CONF_ID, CONF_MULTIPLY, - CONF_OFFSET, ) from .. import ( - SensorItem, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + ModbusItemBaseSchema, + SensorItem, + SENSOR_VALUE_TYPE, ) from ..const import ( - CONF_BYTE_OFFSET, CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) -from ..sensor import SENSOR_VALUE_TYPE DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] -ModbusOutput = modbus_controller_ns.class_( - "ModbusOutput", cg.Component, output.FloatOutput, SensorItem +ModbusFloatOutput = modbus_controller_ns.class_( + "ModbusFloatOutput", cg.Component, output.FloatOutput, SensorItem +) +ModbusBinaryOutput = modbus_controller_ns.class_( + "ModbusBinaryOutput", cg.Component, output.BinaryOutput, SensorItem ) -CONFIG_SCHEMA = cv.All( - output.FLOAT_OUTPUT_SCHEMA.extend( - { - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.GenerateID(): cv.declare_id(ModbusOutput), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), - cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, - } - ), + +CONFIG_SCHEMA = cv.typed_schema( + { + "coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusBinaryOutput), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + "holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusFloatOutput), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( + SENSOR_VALUE_TYPE + ), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + }, + lower=True, + key=CONF_REGISTER_TYPE, + default_type="holding", ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] - var = cg.new_Pvariable( - config[CONF_ID], config[CONF_ADDRESS], byte_offset, config[CONF_VALUE_TYPE] - ) + byte_offset, reg_count = modbus_calc_properties(config) + # Binary Output + if config[CONF_REGISTER_TYPE] == "coil": + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + ) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusBinaryOutput.operator("ptr"), "item"), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), + ], + return_type=cg.optional.template(bool), + ) + # Float Output + else: + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + config[CONF_VALUE_TYPE], + reg_count, + ) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusFloatOutput.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) 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 index f7d7c42342..b647312f52 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -1,23 +1,17 @@ #include #include "modbus_output.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace modbus_controller { static const char *const TAG = "modbus_controller.output"; -void ModbusOutput::setup() {} - /** Write a value to the device * */ -void ModbusOutput::write_state(float value) { - union { - float float_value; - uint32_t raw; - } raw_to_float; - +void ModbusFloatOutput::write_state(float value) { std::vector data; auto original_value = value; // Is there are lambda configured? @@ -45,16 +39,72 @@ void ModbusOutput::write_state(float value) { this->start_address, this->register_count, value, original_value); // Create and send the write command - auto write_cmd = - ModbusCommandItem::create_write_multiple_command(parent_, this->start_address, this->register_count, data); + 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() { +void ModbusFloatOutput::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); + ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address); + ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count); + ESP_LOGCONFIG(TAG, " Value type: %d", static_cast(this->sensor_value_type)); +} + +// ModbusBinaryOutput +void ModbusBinaryOutput::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 binary output 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 new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state), + (int) this->register_type, this->start_address, this->offset); + + // 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); + } + } + this->parent_->queue_command(cmd); +} + +void ModbusBinaryOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus Binary Output:"); + LOG_BINARY_OUTPUT(this); + ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address); + ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count); + ESP_LOGCONFIG(TAG, " Value type: %d", static_cast(this->sensor_value_type)); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 053186a321..6237805d24 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -7,22 +7,20 @@ namespace esphome { namespace modbus_controller { -using value_to_data_t = std::function(float); - -class ModbusOutput : public output::FloatOutput, public Component, public SensorItem { +class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem { public: - ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type) + ModbusFloatOutput(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; } @@ -30,8 +28,9 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusOutput *, float, std::vector &)>; + using write_transform_func_t = std::function(ModbusFloatOutput *, 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; @@ -39,6 +38,37 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor ModbusController *parent_; float multiply_by_{1.0}; + bool use_write_multiple_; +}; + +class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem { + public: + ModbusBinaryOutput(uint16_t start_address, uint8_t offset) : output::BinaryOutput(), Component() { + this->register_type = ModbusRegisterType::COIL; + this->start_address = start_address; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = 0; + this->register_count = 1; + this->start_address += offset; + this->offset = 0; + } + void dump_config() override; + + void set_parent(ModbusController *parent) { this->parent_ = parent; } + // Do nothing + void parse_and_publish(const std::vector &data) override{}; + + using write_transform_func_t = std::function(ModbusBinaryOutput *, bool, 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(bool state) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_; + bool use_write_multiple_; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index 687f3d82fb..da7b8928b4 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -2,17 +2,19 @@ from esphome.components import sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE, ) from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_COUNT, @@ -29,61 +31,31 @@ ModbusSensor = modbus_controller_ns.class_( "ModbusSensor", cg.Component, sensor.Sensor, SensorItem ) -TYPE_REGISTER_MAP = { - "RAW": 1, - "U_WORD": 1, - "S_WORD": 1, - "U_DWORD": 2, - "U_DWORD_R": 2, - "S_DWORD": 2, - "S_DWORD_R": 2, - "U_QWORD": 4, - "U_QWORDU_R": 4, - "S_QWORD": 4, - "U_QWORD_R": 4, - "FP32": 2, - "FP32_R": 2, -} - - CONFIG_SCHEMA = cv.All( - sensor.SENSOR_SCHEMA.extend( + sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusSensor), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_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, - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, reg_count = modbus_calc_properties(config) value_type = config[CONF_VALUE_TYPE] - reg_count = config[CONF_REGISTER_COUNT] - if reg_count == 0: - reg_count = TYPE_REGISTER_MAP[value_type] var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], - config[CONF_VALUE_TYPE], + value_type, reg_count, config[CONF_SKIP_UPDATES], config[CONF_FORCE_NEW_RANGE], @@ -93,17 +65,4 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusSensor.operator("ptr"), "item"), - (cg.float_, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(float), - ) - cg.add(var.set_template(template_)) + 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 index dbd0525347..a21fd91032 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -10,11 +10,6 @@ static const char *const TAG = "modbus_controller.sensor"; void ModbusSensor::dump_config() { LOG_SENSOR(TAG, "Modbus Controller Sensor", this); } void ModbusSensor::parse_and_publish(const std::vector &data) { - union { - float float_value; - uint32_t raw; - } raw_to_float; - float result = payload_to_float(data, *this); // Is there a lambda registered diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 4f48c2a4dd..37ea9d0dd0 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -25,6 +25,7 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem 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: diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index e03b0d37be..0dfbd83cb8 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -3,19 +3,24 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - MODBUS_REGISTER_TYPE, - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, + MODBUS_REGISTER_TYPE, ) from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, + CONF_WRITE_LAMBDA, ) DEPENDENCIES = ["modbus_controller"] @@ -26,56 +31,48 @@ ModbusSwitch = modbus_controller_ns.class_( "ModbusSwitch", cg.Component, switch.Switch, SensorItem ) - CONFIG_SCHEMA = cv.All( - switch.SWITCH_SCHEMA.extend( + switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusSwitch), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, + 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, } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + 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 switch.register_switch(var, config) paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) - cg.add(paren.add_sensor_item(var)) cg.add(var.set_parent(paren)) - if CONF_LAMBDA in config: - publish_template_ = await cg.process_lambda( - config[CONF_LAMBDA], + 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"), - (bool, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), ], return_type=cg.optional.template(bool), ) - cg.add(var.set_template(publish_template_)) + 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 index ce9557e6c4..ca8d0be720 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -45,22 +45,50 @@ void ModbusSwitch::parse_and_publish(const std::vector &data) { void ModbusSwitch::write_state(bool state) { // This will be called every time the user requests a state change. ModbusCommandItem cmd; - ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), - ONOFF(state), (int) this->register_type, this->start_address, this->offset); - switch (this->register_type) { - case ModbusRegisterType::COIL: + 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 - cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); - break; - case ModbusRegisterType::DISCRETE_INPUT: - cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, state); - break; - - default: + 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 - cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, - state ? 0xFFFF & this->bitmask : 0); - break; + 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); diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index a38668fabb..6732c01eef 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -10,14 +10,14 @@ 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) + uint8_t skip_updates, 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->skip_updates = skip_updates; this->register_count = 1; if (register_type == ModbusRegisterType::HOLDING || register_type == ModbusRegisterType::COIL) { this->start_address += offset; @@ -33,11 +33,16 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem 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 diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 2c02c86795..5cc85af5bc 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -3,15 +3,17 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, MODBUS_REGISTER_TYPE, ) from ..const import ( - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_COUNT, @@ -38,32 +40,23 @@ RAW_ENCODING = { } CONFIG_SCHEMA = cv.All( - text_sensor.TEXT_SENSOR_SCHEMA.extend( + text_sensor.TEXT_SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusTextSensor), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int, cv.Optional(CONF_RAW_ENCODE, default="NONE"): cv.enum(RAW_ENCODING), - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, reg_count = modbus_calc_properties(config) response_size = config[CONF_RESPONSE_SIZE] reg_count = config[CONF_REGISTER_COUNT] if reg_count == 0: @@ -85,17 +78,6 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusTextSensor.operator("ptr"), "item"), - (cg.std_string.operator("const").operator("ref"), "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(cg.std_string), - ) - cg.add(var.set_template(template_)) + 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 index a06d44e90b..c90890c88f 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -13,19 +13,19 @@ void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Te void ModbusTextSensor::parse_and_publish(const std::vector &data) { std::ostringstream output; - uint8_t max_items = this->response_bytes_; + uint8_t max_items = this->response_bytes; + uint8_t index = this->offset; char buffer[4]; - bool add_comma = false; - for (auto b : data) { + while ((max_items != 0) && index < data.size()) { + uint8_t b = data[index]; switch (this->encode_) { case RawEncoding::HEXBYTES: sprintf(buffer, "%02x", b); output << buffer; break; case RawEncoding::COMMA: - sprintf(buffer, add_comma ? ",%d" : "%d", b); + sprintf(buffer, index != this->offset ? ",%d" : "%d", b); output << buffer; - add_comma = true; break; // Anything else no encoding case RawEncoding::NONE: @@ -33,9 +33,8 @@ void ModbusTextSensor::parse_and_publish(const std::vector &data) { output << (char) b; break; } - if (--max_items == 0) { - break; - } + + index++; } auto result = output.str(); diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index 28d0f0b241..3db4d94a45 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -17,7 +17,7 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi this->register_type = register_type; this->start_address = start_address; this->offset = offset; - this->response_bytes_ = response_bytes; + this->response_bytes = response_bytes; this->register_count = register_count; this->encode_ = encode; this->skip_updates = skip_updates; @@ -25,13 +25,6 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi this->sensor_value_type = SensorValueType::RAW; this->force_new_range = force_new_range; } - size_t get_register_size() const override { - if (sensor_value_type == SensorValueType::RAW) { - return this->response_bytes_; - } else { - return SensorItem::get_register_size(); - } - } void dump_config() override; @@ -45,7 +38,6 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi protected: RawEncoding encode_; - uint16_t response_bytes_; }; } // namespace modbus_controller diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 8f02f8d437..755b0c685c 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_DISCOVERY, CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, + CONF_DISCOVERY_UNIQUE_ID_GENERATOR, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, @@ -34,6 +35,7 @@ from esphome.const import ( CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_TRIGGER_ID, + CONF_USE_ABBREVIATIONS, CONF_USERNAME, CONF_WILL_MESSAGE, ) @@ -78,7 +80,7 @@ MQTTMessageTrigger = mqtt_ns.class_( "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component ) MQTTJsonMessageTrigger = mqtt_ns.class_( - "MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConstRef) + "MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst) ) MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) @@ -93,6 +95,13 @@ 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): @@ -152,6 +161,10 @@ CONFIG_SCHEMA = cv.All( 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, @@ -215,7 +228,7 @@ async def to_code(config): await cg.register_component(var, config) # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json - cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.4") + cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) @@ -229,16 +242,28 @@ async 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()) @@ -286,7 +311,7 @@ async def to_code(config): for conf in config.get(CONF_ON_JSON_MESSAGE, []): trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS]) - await automation.build_automation(trig, [(cg.JsonObjectConstRef, "x")], conf) + await automation.build_automation(trig, [(cg.JsonObjectConst, "x")], conf) MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( @@ -338,7 +363,7 @@ async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string) cg.add(var.set_topic(template_)) - args_ = args + [(cg.JsonObjectRef, "root")] + args_ = args + [(cg.JsonObject, "root")] lambda_ = await cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void) cg.add(var.set_payload(lambda_)) template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 9795d69304..0852a17cf1 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -74,9 +74,9 @@ class CustomMQTTDevice { * } * * // topic parameter can be remove if not needed: - * // e.g.: void on_json_message(JsonObject &payload) { + * // e.g.: void on_json_message(JsonObject payload) { * - * void on_json_message(const std::string &topic, JsonObject &payload) { + * void on_json_message(const std::string &topic, JsonObject payload) { * // do something with topic and payload * if (payload["number"] == 1) { * digitalWrite(5, HIGH); @@ -93,11 +93,9 @@ class CustomMQTTDevice { * @param qos The Quality of Service to subscribe with. Defaults to 0. */ template - void subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject &), - uint8_t qos = 0); + void subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos = 0); - template - void subscribe_json(const std::string &topic, void (T::*callback)(JsonObject &), uint8_t qos = 0); + template void subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos = 0); /** Publish an MQTT message with the given payload and QoS and retain settings. * @@ -155,7 +153,7 @@ class CustomMQTTDevice { * * ```cpp * void in_some_method() { - * publish("the/topic", [=](JsonObject &root) { + * publish("the/topic", [=](JsonObject root) { * root["the_key"] = "Hello World!"; * }, 0, false); * } @@ -174,7 +172,7 @@ class CustomMQTTDevice { * * ```cpp * void in_some_method() { - * publish("the/topic", [=](JsonObject &root) { + * publish("the/topic", [=](JsonObject root) { * root["the_key"] = "Hello World!"; * }); * } @@ -205,13 +203,13 @@ template void CustomMQTTDevice::subscribe(const std::string &topic, global_mqtt_client->subscribe(topic, f, qos); } template -void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject &), +void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos) { auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); global_mqtt_client->subscribe_json(topic, f, qos); } template -void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject &), uint8_t qos) { +void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos) { auto f = std::bind(callback, (T *) this, std::placeholders::_2); global_mqtt_client->subscribe_json(topic, f, qos); } diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 188df0f7b9..0bf3b751fd 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -1,6 +1,8 @@ #include "mqtt_binary_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_BINARY_SENSOR @@ -27,13 +29,13 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } } -void MQTTBinarySensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +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() { diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 0efb490367..f6579fcd19 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -22,7 +22,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; void set_is_status(bool status); diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp new file mode 100644 index 0000000000..52df63093a --- /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..42389caecc --- /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 040b0001fe..de25c5b2e3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -1,4 +1,5 @@ #include "mqtt_client.h" +#define USE_MQTT #ifdef USE_MQTT @@ -346,7 +347,7 @@ void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t ca void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) { auto f = [callback](const std::string &topic, const std::string &payload) { - json::parse_json(payload, [topic, callback](JsonObject &root) { callback(topic, root); }); + json::parse_json(payload, [topic, callback](JsonObject root) { callback(topic, root); }); }; MQTTSubscription subscription{ .topic = topic, @@ -416,9 +417,8 @@ bool MQTTClientComponent::publish(const MQTTMessage &message) { } bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) { - size_t len; - const char *message = json::build_json(f, &len); - return this->publish(topic, message, len, qos, retain); + std::string message = json::build_json(f); + return this->publish(topic, message, qos, retain); } /** Check if the message topic matches the given subscription topic @@ -535,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; } diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index fa689eaa04..a6a7025c6f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -20,7 +20,7 @@ namespace mqtt { * First parameter is the topic, the second one is the payload. */ using mqtt_callback_t = std::function; -using mqtt_json_callback_t = std::function; +using mqtt_json_callback_t = std::function; /// internal struct for MQTT messages. struct MQTTMessage { @@ -55,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. @@ -63,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 { @@ -98,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. @@ -297,11 +306,11 @@ class MQTTMessageTrigger : public Trigger, public Component { optional payload_; }; -class MQTTJsonMessageTrigger : public Trigger { +class MQTTJsonMessageTrigger : public Trigger { public: explicit MQTTJsonMessageTrigger(const std::string &topic, uint8_t qos) { global_mqtt_client->subscribe_json( - topic, [this](const std::string &topic, JsonObject &root) { this->trigger(root); }, qos); + topic, [this](const std::string &topic, JsonObject root) { this->trigger(root); }, qos); } }; @@ -329,7 +338,7 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - void set_payload(std::function payload) { this->payload_ = payload; } + 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); @@ -340,8 +349,8 @@ template class MQTTPublishJsonAction : public Action { } protected: - void encode_(Ts... x, JsonObject &root) { this->payload_(x..., root); } - std::function payload_; + void encode_(Ts... x, JsonObject root) { this->payload_(x..., root); } + std::function payload_; MQTTClientComponent *parent_; }; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 47b6684dec..f6ef3a5e8f 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,6 +1,8 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_CLIMATE @@ -11,19 +13,19 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; -void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { auto traits = this->device_->get_traits(); // 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"); @@ -41,47 +43,47 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC 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["temp_unit"] = "C"; + root[MQTT_TEMPERATURE_UNIT] = "C"; 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() || !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"); + JsonArray fan_modes = root.createNestedArray("fan_modes"); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) fan_modes.add("on"); if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) @@ -106,11 +108,11 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC 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"); + JsonArray swing_modes = root.createNestedArray("swing_modes"); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) swing_modes.add("off"); if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) @@ -135,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; @@ -146,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; @@ -158,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; diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index 40ac4c18c1..ea3e2ab3fa 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -14,7 +14,7 @@ namespace mqtt { class MQTTClimateComponent : public mqtt::MQTTComponent { public: MQTTClimateComponent(climate::Climate *device); - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; std::string component_type() const override; void setup() override; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 0ece4b3501..62dbae3bcc 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -7,6 +7,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/version.h" +#include "mqtt_const.h" + namespace esphome { namespace mqtt { @@ -15,7 +17,7 @@ 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_allowlist(App.get_name(), HOSTNAME_CHARACTER_ALLOWLIST); + std::string sanitized_name = str_sanitize(App.get_name()); return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + this->get_default_object_id_() + "/config"; } @@ -61,7 +63,7 @@ bool MQTTComponent::send_discovery_() { return global_mqtt_client->publish_json( this->get_discovery_topic_(discovery_info), - [this](JsonObject &root) { + [this](JsonObject root) { SendDiscoveryConfig config; config.state_topic = true; config.command_topic = true; @@ -69,49 +71,68 @@ bool MQTTComponent::send_discovery_() { this->send_discovery(root, config); // Fields from EntityBase - root["name"] = this->friendly_name(); + root[MQTT_NAME] = this->friendly_name(); if (this->is_disabled_by_default()) - root["enabled_by_default"] = false; + root[MQTT_ENABLED_BY_DEFAULT] = false; if (!this->get_icon().empty()) - root["icon"] = this->get_icon(); + 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(); - device_info["model"] = ESPHOME_BOARD; - 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); } @@ -123,7 +144,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return sanitize_string_allowlist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_ALLOWLIST); + return str_sanitize(str_snake_case(this->friendly_name())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 657ab7b608..e83523a712 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -70,7 +70,7 @@ class MQTTComponent : public Component { void call_dump_config() override; /// Send discovery info the Home Assistant, override this. - virtual void send_discovery(JsonObject &root, SendDiscoveryConfig &config) = 0; + virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config) = 0; virtual bool send_initial_state() = 0; 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 e8bc7f0e30..e5525bc0f7 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,6 +1,8 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_COVER @@ -22,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; @@ -34,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,22 +63,22 @@ void MQTTCoverComponent::dump_config() { ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic().c_str()); } } -void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (!this->cover_->get_device_class().empty()) - root["device_class"] = this->cover_->get_device_class(); + 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()) { config.state_topic = false; - root["position_topic"] = this->get_position_state_topic(); - root["set_position_topic"] = this->get_position_command_topic(); + 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; diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 149d46ac85..f3e6053d0b 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -16,7 +16,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { explicit MQTTCoverComponent(cover::Cover *cover); void setup() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; MQTT_COMPONENT_CUSTOM_TOPIC(position, command) MQTT_COMPONENT_CUSTOM_TOPIC(position, state) diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 898183cc58..0f2eb6535f 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,6 +1,8 @@ #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" @@ -69,7 +71,7 @@ void MQTTFanComponent::setup() { if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_level_command_topic(), [this](const std::string &topic, const std::string &payload) { - optional speed_level_opt = parse_int(payload); + 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()) { @@ -88,9 +90,12 @@ void MQTTFanComponent::setup() { if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { +#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 }); } @@ -115,16 +120,16 @@ void MQTTFanComponent::dump_config() { bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } -void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->state_->get_traits().supports_oscillation()) { - root["oscillation_command_topic"] = this->get_oscillation_command_topic(); - root["oscillation_state_topic"] = this->get_oscillation_state_topic(); + 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["speed_command_topic"] = this->get_speed_command_topic(); - root["speed_state_topic"] = this->get_speed_state_topic(); + root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic(); + root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic(); } } bool MQTTFanComponent::publish_state() { @@ -145,6 +150,8 @@ bool MQTTFanComponent::publish_state() { } if (traits.supports_speed()) { const char *payload; +#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) @@ -161,6 +168,7 @@ bool MQTTFanComponent::publish_state() { break; } } +#pragma GCC diagnostic pop bool success = this->publish(this->get_speed_state_topic(), payload); failed = failed || !success; } diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index a160d5366b..9d15a6cd0e 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -22,7 +22,7 @@ class MQTTFanComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(speed, command) MQTT_COMPONENT_CUSTOM_TOPIC(speed, state) - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index a88358a6b2..ee1cc36af7 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,6 +1,8 @@ #include "mqtt_light.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_LIGHT @@ -16,7 +18,7 @@ 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->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { LightCall call = this->state_->make_call(); LightJSONSchema::parse_json(*this->state_, call, root); call.perform(); @@ -30,16 +32,16 @@ MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponen bool MQTTJSONLightComponent::publish_state_() { return this->publish_json(this->get_state_topic_(), - [this](JsonObject &root) { LightJSONSchema::dump_json(*this->state_, root); }); + [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } -void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { root["schema"] = "json"; auto traits = this->state_->get_traits(); - root["color_mode"] = true; - JsonArray &color_modes = root.createNestedArray("supported_color_modes"); + 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)) @@ -64,7 +66,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover 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"); diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 192cba39b6..3d1e770d4d 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -21,7 +21,7 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 674fd77bdf..73d37f7cd3 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -1,6 +1,8 @@ #include "mqtt_number.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_NUMBER @@ -15,7 +17,7 @@ MQTTNumberComponent::MQTTNumberComponent(Number *number) : MQTTComponent(), numb void MQTTNumberComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { - auto val = parse_float(state); + auto val = parse_number(state); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); return; @@ -35,12 +37,24 @@ void MQTTNumberComponent::dump_config() { 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) { +void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = number_->traits; // https://www.home-assistant.io/integrations/number.mqtt/ - root["min"] = traits.get_min_value(); - root["max"] = traits.get_max_value(); - root["step"] = traits.get_step(); + 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; } diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h index 66622d7c29..10500c8333 100644 --- a/esphome/components/mqtt/mqtt_number.h +++ b/esphome/components/mqtt/mqtt_number.h @@ -25,7 +25,7 @@ class MQTTNumberComponent : public mqtt::MQTTComponent { void setup() override; void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b499636006..cb4c9c9052 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -1,6 +1,8 @@ #include "mqtt_select.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_SELECT @@ -30,10 +32,10 @@ void MQTTSelectComponent::dump_config() { 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) { +void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = select_->traits; // https://www.home-assistant.io/integrations/select.mqtt/ - JsonArray &options = root.createNestedArray("options"); + JsonArray options = root.createNestedArray(MQTT_OPTIONS); for (const auto &option : traits.get_options()) options.add(option); diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h index d77d0cf513..e0d8ac2417 100644 --- a/esphome/components/mqtt/mqtt_select.h +++ b/esphome/components/mqtt/mqtt_select.h @@ -25,7 +25,7 @@ class MQTTSelectComponent : public mqtt::MQTTComponent { void setup() override; void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 78710ff403..303aa0e753 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -1,6 +1,8 @@ #include "mqtt_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_SENSOR @@ -40,21 +42,21 @@ uint32_t MQTTSensorComponent::get_expire_after() const { void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; } void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } -void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (!this->sensor_->get_device_class().empty()) - root["device_class"] = this->sensor_->get_device_class(); + 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; + 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["state_class"] = state_class_to_string(this->sensor_->get_state_class()); + root[MQTT_STATE_CLASS] = state_class_to_string(this->sensor_->get_state_class()); config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 22609fdfef..adc201736a 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -27,7 +27,7 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { /// Disable Home Assistant value expiry. void disable_expire_after(); - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 16cf102f7e..2e91f8e502 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -1,6 +1,8 @@ #include "mqtt_switch.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_SWITCH @@ -42,9 +44,9 @@ 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) { +void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->switch_->assumed_state()) - root["optimistic"] = true; + root[MQTT_OPTIMISTIC] = true; } bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index a0a7a23220..c4d3f7164c 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -20,7 +20,7 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { void setup() override; void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 7b89915649..010364e221 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -12,7 +12,7 @@ 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) { +void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index 83743245cc..fe53a6fefd 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -15,7 +15,7 @@ class MQTTTextSensor : public mqtt::MQTTComponent { public: explicit MQTTTextSensor(text_sensor::TextSensor *sensor); - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; void setup() override; diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp index e1accf3c70..273de10376 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp @@ -13,7 +13,7 @@ void MQTTSubscribeSensor::setup() { mqtt::global_mqtt_client->subscribe( this->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()); this->publish_state(NAN); diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 1d7516dbe8..4b34e1d71a 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -75,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/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 0117f1b063..6bb1bc8f99 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import light from esphome.const import ( + CONF_CHANNEL, CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, @@ -13,7 +14,26 @@ from esphome.const import ( 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_( @@ -46,127 +66,115 @@ def validate_type(value): 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 - - if CORE.is_esp32: - return cv.one_of(*ESP32_METHODS, upper=True, space="_")(value) + pin = config[CONF_PIN] if CORE.is_esp8266: - return cv.one_of(*ESP8266_METHODS, upper=True, space="_")(value) - raise NotImplementedError - - -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 - - for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): - if opt in value and value[opt] not in pins_: - raise cv.Invalid( - f"Method {method} only supports pin(s) {', '.join(f'GPIO{x}' for x in pins_)}", - path=[CONF_METHOD], + 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, + } ) - return value - - -VARIANTS = { - "WS2812X": "Ws2812x", - "SK6812": "Sk6812", - "800KBPS": "800Kbps", - "400KBPS": "400Kbps", -} - -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", -} - - -def format_method(config): - variant = VARIANTS[config[CONF_VARIANT]] - method = config[CONF_METHOD] - - if config[CONF_INVERT]: - if method == "ESP8266_DMA": - variant = f"Inverted{variant}" else: - variant += "Inverted" + config[CONF_METHOD] = _validate_method(METHOD_BIT_BANG) - if CORE.is_esp8266: - return ESP8266_METHODS[method].format(variant) if CORE.is_esp32: - return ESP32_METHODS[method].format(variant) - 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(config): - if CONF_PIN in 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("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'") + 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]." + ) + + 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 + + +def _validate_method(value): + if value is None: + # default method is determined afterwards because it depends on the chip type chosen + return None + + 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) 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.Optional(CONF_VARIANT, default="800KBPS"): validate_variant, - cv.Optional(CONF_METHOD, default=None): validate_method, + 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, @@ -174,19 +182,23 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, } ).extend(cv.COMPONENT_SCHEMA), + _choose_default_method, _validate, - validate_method_pin, - cv.only_with_arduino, ) async def to_code(config): has_white = "W" in config[CONF_TYPE] - template = cg.TemplateArguments(getattr(cg.global_ns, format_method(config))) + method = config[CONF_METHOD] + + method_template = METHODS[method[CONF_TYPE]].to_code( + method, config[CONF_VARIANT], config[CONF_INVERT] + ) + 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) await light.register_light(var, config) @@ -204,4 +216,4 @@ async def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library("makuna/NeoPixelBus", "2.6.7") + cg.add_library("makuna/NeoPixelBus", "2.6.9") diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index c5bfa78efe..bf6e74cb38 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -33,7 +33,7 @@ void NextionBinarySensor::update() { if (this->variable_name_.empty()) // This is a touch component return; - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nextion) { @@ -48,7 +48,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); } } diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h index b86ee74013..b6b23ada85 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -10,8 +10,7 @@ class NextionBinarySensor; class NextionBinarySensor : public NextionComponent, public binary_sensor::BinarySensorInitiallyOff, - public PollingComponent, - public std::enable_shared_from_this { + public PollingComponent { public: NextionBinarySensor(NextionBase *nextion) { this->nextion_ = nextion; } diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index f4b35fd56f..d95810bfbe 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -8,7 +8,7 @@ from esphome.const import ( CONF_BRIGHTNESS, CONF_TRIGGER_ID, ) - +from esphome.core import CORE from . import Nextion, nextion_ns, nextion_ref from .base_component import ( CONF_ON_SLEEP, @@ -76,6 +76,9 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) + if CORE.is_esp32: + 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])) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index d56c370412..fcb3885db9 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -64,7 +64,7 @@ bool Nextion::check_connect_() { if (response.empty() || response.find("comok") == std::string::npos) { #ifdef NEXTION_PROTOCOL_LOG ESP_LOGN(TAG, "Bad connect request %s", response.c_str()); - for (int i = 0; i < response.length(); i++) { + 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 @@ -196,7 +196,7 @@ 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_) { + for (auto *i : this->nextion_queue_) { if (count++ == 10) break; @@ -257,9 +257,8 @@ bool Nextion::remove_from_q_(bool report_empty) { return false; } - auto nb = std::move(this->nextion_queue_.front()); - this->nextion_queue_.pop_front(); - auto &component = nb->component; + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; ESP_LOGN(TAG, "Removing %s from the queue", component->get_variable_name().c_str()); @@ -267,8 +266,10 @@ bool Nextion::remove_from_q_(bool report_empty) { 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; } @@ -328,6 +329,7 @@ void Nextion::process_nextion_commands_() { break; case 0x02: // invalid Component ID or name was used + ESP_LOGW(TAG, "Nextion reported component ID or name invalid!"); this->remove_from_q_(); break; case 0x03: // invalid Page ID or name was used @@ -357,7 +359,7 @@ void Nextion::process_nextion_commands_() { int index = 0; int found = -1; for (auto &nb : this->nextion_queue_) { - auto &component = nb->component; + 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!", @@ -368,6 +370,9 @@ void Nextion::process_nextion_commands_() { found = index; + delete component; // NOLINT(cppcoreguidelines-owning-memory) + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + break; } ++index; @@ -383,6 +388,7 @@ void Nextion::process_nextion_commands_() { } break; case 0x1A: // variable name invalid + ESP_LOGW(TAG, "Nextion reported variable name invalid!"); this->remove_from_q_(); break; @@ -464,9 +470,8 @@ void Nextion::process_nextion_commands_() { break; } - auto nb = std::move(this->nextion_queue_.front()); - this->nextion_queue_.pop_front(); - auto &component = nb->component; + 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", @@ -477,6 +482,9 @@ void Nextion::process_nextion_commands_() { 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 @@ -505,9 +513,8 @@ void Nextion::process_nextion_commands_() { ++dataindex; } - auto nb = std::move(this->nextion_queue_.front()); - this->nextion_queue_.pop_front(); - auto &component = nb->component; + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; if (component->get_queue_type() != NextionQueueType::SENSOR && component->get_queue_type() != NextionQueueType::BINARY_SENSOR && @@ -521,6 +528,9 @@ void Nextion::process_nextion_commands_() { component->set_state_from_int(value, true, false); } + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); + break; } @@ -555,11 +565,10 @@ void Nextion::process_nextion_commands_() { // FF FF FF - End case 0x90: { // Switched component std::string variable_name; - uint8_t index = 0; // Get variable name - index = to_process.find('\0'); - if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + 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; @@ -583,10 +592,9 @@ void Nextion::process_nextion_commands_() { // FF FF FF - End case 0x91: { // Sensor component std::string variable_name; - uint8_t index = 0; - index = to_process.find('\0'); - if (static_cast(index) == std::string::npos || (to_process_length - index - 1) != 4) { + 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; @@ -618,11 +626,10 @@ void Nextion::process_nextion_commands_() { case 0x92: { // Text Sensor Component std::string variable_name; std::string text_value; - uint8_t index = 0; // Get variable name - index = to_process.find('\0'); - if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + 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; @@ -652,11 +659,10 @@ void Nextion::process_nextion_commands_() { // FF FF FF - End case 0x93: { // Binary Sensor component std::string variable_name; - uint8_t index = 0; // Get variable name - index = to_process.find('\0'); - if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + 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; @@ -682,7 +688,7 @@ void Nextion::process_nextion_commands_() { int index = 0; int found = -1; for (auto &nb : this->nextion_queue_) { - auto &component = nb->component; + 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 @@ -699,6 +705,8 @@ void Nextion::process_nextion_commands_() { component->get_wave_buffer().begin() + buffer_to_send); } found = index; + delete component; // NOLINT(cppcoreguidelines-owning-memory) + delete nb; // NOLINT(cppcoreguidelines-owning-memory) break; } ++index; @@ -726,8 +734,8 @@ void Nextion::process_nextion_commands_() { uint32_t ms = millis(); if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { - for (int i = 0; i < this->nextion_queue_.size(); i++) { - auto &component = this->nextion_queue_[i]->component; + 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", @@ -744,8 +752,11 @@ void Nextion::process_nextion_commands_() { 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--; @@ -899,16 +910,18 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool * @param variable_name Name for the queue */ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { - auto nextion_queue = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; - nextion_queue->component = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); nextion_queue->queue_time = millis(); - ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); + this->nextion_queue_.push_back(nextion_queue); - this->nextion_queue_.push_back(std::move(nextion_queue)); + ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); } /** @@ -979,7 +992,7 @@ bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_na * @param is_sleep_safe The command is safe to send when the Nextion is sleeping */ -void Nextion::add_no_result_to_queue_with_set(std::shared_ptr component, int state_value) { +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); } @@ -1007,8 +1020,7 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia * @param state_value String value to set * @param is_sleep_safe The command is safe to send when the Nextion is sleeping */ -void Nextion::add_no_result_to_queue_with_set(std::shared_ptr component, - const std::string &state_value) { +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); } @@ -1028,11 +1040,12 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia state_value.c_str()); } -void Nextion::add_to_get_queue(std::shared_ptr component) { +void Nextion::add_to_get_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->ignore_is_setup_)) return; - auto nextion_queue = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; nextion_queue->component = component; nextion_queue->queue_time = millis(); @@ -1043,7 +1056,7 @@ void Nextion::add_to_get_queue(std::shared_ptr component) std::string command = "get " + component->get_variable_name_to_send(); if (this->send_command_(command)) { - this->nextion_queue_.push_back(std::move(nextion_queue)); + this->nextion_queue_.push_back(nextion_queue); } } @@ -1055,13 +1068,15 @@ void Nextion::add_to_get_queue(std::shared_ptr component) * @param buffer_to_send The buffer size * @param buffer_size The buffer data */ -void Nextion::add_addt_command_to_queue(std::shared_ptr component) { +void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; - auto nextion_queue = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; - nextion_queue->component = std::make_shared(); + // 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() @@ -1070,7 +1085,7 @@ void Nextion::add_addt_command_to_queue(std::shared_ptr co std::string command = "addt " + to_string(component->get_component_id()) + "," + to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); if (this->send_command_(command)) { - this->nextion_queue_.push_back(std::move(nextion_queue)); + this->nextion_queue_.push_back(nextion_queue); } } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 1bee41f6cf..285b3ac9a3 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -707,18 +707,17 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state); void set_nextion_text_state(const std::string &name, const std::string &state); - void add_no_result_to_queue_with_set(std::shared_ptr component, int state_value) override; + 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(std::shared_ptr component, - const std::string &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(std::shared_ptr component) override; + void add_to_get_queue(NextionComponentBase *component) override; - void add_addt_command_to_queue(std::shared_ptr component) override; + void add_addt_command_to_queue(NextionComponentBase *component) override; void update_components_by_prefix(const std::string &prefix); @@ -729,7 +728,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } protected: - std::deque> nextion_queue_; + 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; diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index d91c70c960..a24fd74060 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -24,19 +24,18 @@ class NextionBase; class NextionBase { public: - virtual void add_no_result_to_queue_with_set(std::shared_ptr component, int state_value) = 0; + virtual void add_no_result_to_queue_with_set(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(std::shared_ptr component, - const std::string &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(std::shared_ptr component) = 0; + virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0; - virtual void add_to_get_queue(std::shared_ptr 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; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 931b934ba2..f83aafc595 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -171,7 +171,7 @@ void Nextion::set_component_coordinates(const char *component, int x, int 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); + 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) { diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 2725d5a30c..71ad803bc4 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "esphome/core/defines.h" namespace esphome { @@ -23,7 +22,7 @@ class NextionComponentBase; class NextionQueue { public: virtual ~NextionQueue() = default; - std::shared_ptr component; + NextionComponentBase *component; uint32_t queue_time = 0; }; diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index cebdbec31a..1b60034bd1 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -8,6 +8,10 @@ #include "esphome/core/log.h" #include "esphome/components/network/util.h" +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace nextion { static const char *const TAG = "nextion_upload"; @@ -95,7 +99,7 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { } http->end(); ESP_LOGN(TAG, "this->content_length_ %d sent %d", this->content_length_, sent); - for (uint32_t i = 0; i < range; i += 4096) { + 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, @@ -158,12 +162,8 @@ void Nextion::upload_tft() { 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_; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); return; } else { ESP_LOGD(TAG, "Connected"); @@ -238,7 +238,7 @@ void Nextion::upload_tft() { // The Nextion display will, if it's ready to accept data, send a 0x05 byte. ESP_LOGD(TAG, "Upgrade response is %s %zu", response.c_str(), response.length()); - for (int i = 0; i < response.length(); i++) { + for (size_t i = 0; i < response.length(); i++) { ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); } @@ -252,7 +252,7 @@ void Nextion::upload_tft() { // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 #ifdef USE_ESP32 uint32_t chunk_size = 8192; - if (psramFound()) { + if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) { chunk_size = this->content_length_; } else { if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand @@ -269,30 +269,18 @@ void Nextion::upload_tft() { #endif if (this->transfer_buffer_ == nullptr) { -#ifdef USE_ESP32 - if (psramFound()) { - ESP_LOGD(TAG, "Allocating PSRAM buffer size %d, Free PSRAM size is %u", chunk_size, ESP.getFreePsram()); - this->transfer_buffer_ = (uint8_t *) ps_malloc(chunk_size); - if (this->transfer_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate buffer size %d!", chunk_size); - this->upload_end_(); - } - } else { -#endif - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); - this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; // NOLINT(cppcoreguidelines-owning-memory) - if (this->transfer_buffer_ == nullptr) { // Try a smaller size - ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); - chunk_size = 4096; - ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); - this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; // NOLINT(cppcoreguidelines-owning-memory) + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + this->transfer_buffer_ = allocator.allocate(chunk_size); + if (this->transfer_buffer_ == nullptr) { // Try a smaller size + ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); + this->transfer_buffer_ = allocator.allocate(chunk_size); - if (!this->transfer_buffer_) - this->upload_end_(); -#ifdef USE_ESP32 - } -#endif + if (!this->transfer_buffer_) + this->upload_end_(); } this->transfer_buffer_size_ = chunk_size; @@ -330,7 +318,8 @@ void Nextion::upload_end_() { WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.compare(0, 6, "https:") == 0) { if (this->wifi_client_secure_ == nullptr) { - this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); // NOLINT(cppcoreguidelines-owning-memory) + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); this->wifi_client_secure_->setInsecure(); this->wifi_client_secure_->setBufferSizes(512, 512); } @@ -338,7 +327,8 @@ WiFiClient *Nextion::get_wifi_client_() { } if (this->wifi_client_ == nullptr) { - this->wifi_client_ = new WiFiClient(); // NOLINT(cppcoreguidelines-owning-memory) + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_ = new WiFiClient(); } return this->wifi_client_; } diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index e983ebcc6f..32bfccf9f8 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -24,7 +24,7 @@ void NextionSensor::add_to_wave_buffer(float state) { wave_buffer_.push_back(wave_state); - if (this->wave_buffer_.size() > this->wave_max_length_) { + if (this->wave_buffer_.size() > (size_t) this->wave_max_length_) { this->wave_buffer_.erase(this->wave_buffer_.begin()); } } @@ -34,7 +34,7 @@ void NextionSensor::update() { return; if (this->wave_chan_id_ == UINT8_MAX) { - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } else { if (this->send_last_value_) { this->add_to_wave_buffer(this->last_value_); @@ -62,9 +62,9 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { double to_multiply = pow(10, this->precision_); int state_value = (int) (state * to_multiply); - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state_value); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); } else { - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); } } } @@ -103,7 +103,7 @@ void NextionSensor::wave_update_() { buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); #endif - this->nextion_->add_addt_command_to_queue(shared_from_this()); + this->nextion_->add_addt_command_to_queue(this); } } // namespace nextion diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h index 068ff0451b..e4dde9a513 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.h +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -8,10 +8,7 @@ namespace esphome { namespace nextion { class NextionSensor; -class NextionSensor : public NextionComponent, - public sensor::Sensor, - public PollingComponent, - public std::enable_shared_from_this { +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); }; diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index 0bd958e0d8..1f32ad3425 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -20,7 +20,7 @@ void NextionSwitch::process_bool(const std::string &variable_name, bool on) { void NextionSwitch::update() { if (!this->nextion_->is_setup()) return; - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { @@ -32,7 +32,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); } } if (publish) { diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h index d7783e5c51..1548287473 100644 --- a/esphome/components/nextion/switch/nextion_switch.h +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -8,10 +8,7 @@ namespace esphome { namespace nextion { class NextionSwitch; -class NextionSwitch : public NextionComponent, - public switch_::Switch, - public PollingComponent, - public std::enable_shared_from_this { +class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent { public: NextionSwitch(NextionBase *nextion) { this->nextion_ = nextion; } diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index fa7cb35025..08f032df74 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -18,7 +18,7 @@ void NextionTextSensor::process_text(const std::string &variable_name, const std void NextionTextSensor::update() { if (!this->nextion_->is_setup()) return; - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } void NextionTextSensor::set_state(const std::string &state, bool publish, bool send_to_nextion) { @@ -29,7 +29,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s if (this->nextion_->is_sleeping() || !this->visible_) { this->needs_to_send_update_ = true; } else { - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), state); + this->nextion_->add_no_result_to_queue_with_set(this, state); } } diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h index 762797727d..5716d0a008 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.h +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -8,10 +8,7 @@ namespace esphome { namespace nextion { class NextionTextSensor; -class NextionTextSensor : public NextionComponent, - public text_sensor::TextSensor, - public PollingComponent, - public std::enable_shared_from_this { +class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent { public: NextionTextSensor(NextionBase *nextion) { this->nextion_ = nextion; } void update() override; diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index d8c940254e..d7d134aedb 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -93,7 +93,7 @@ bool NdefMessage::add_uri_record(const std::string &uri) { return this->add_reco std::vector NdefMessage::encode() { std::vector data; - for (uint8_t i = 0; i < this->records_.size(); i++) { + 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()); } diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 706c09a5aa..09dbdcfe94 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -10,7 +10,7 @@ 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 (uint8_t i = 0; i < uid.size(); i++) { + for (size_t i = 0; i < uid.size(); i++) { const char *format = "%02X"; if (i + 1 < uid.size()) format = "%02X-"; @@ -22,7 +22,7 @@ std::string format_uid(std::vector &uid) { std::string format_bytes(std::vector &bytes) { char buf[(bytes.size() * 2) + bytes.size() - 1]; int offset = 0; - for (uint8_t i = 0; i < bytes.size(); i++) { + for (size_t i = 0; i < bytes.size(); i++) { const char *format = "%02X"; if (i + 1 < bytes.size()) format = "%02X "; diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2856a25ee7..71e288a4cc 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -7,9 +7,11 @@ 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, ) @@ -39,10 +41,17 @@ 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_COMPONENT_SCHEMA).extend( +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), @@ -59,6 +68,8 @@ NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( }, 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), } ) @@ -73,6 +84,8 @@ async def setup_number_core_( 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) @@ -87,6 +100,8 @@ async def setup_number_core_( cg.add(trigger.set_max(template_)) await automation.build_automation(trigger, [(float, "x")], conf) + if CONF_UNIT_OF_MEASUREMENT in config: + cg.add(var.traits.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 57a5c7c4bd..99a2c04a22 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -41,6 +41,15 @@ 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 diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index ed104fb477..40fdfceec1 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -13,6 +13,9 @@ namespace number { 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; @@ -33,6 +36,12 @@ class NumberCall { 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; } @@ -42,10 +51,21 @@ class NumberTraits { 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. diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index bcfb28979d..b3d3b7ad23 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -15,7 +15,7 @@ from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket"] +AUTO_LOAD = ["socket", "md5"] CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_BEGIN = "on_begin" @@ -35,20 +35,12 @@ OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template()) OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template()) -def validate_password_support(value): - if CORE.using_arduino: - return value - if CORE.using_esp_idf: - raise cv.Invalid("Password support is not implemented yet for ESP-IDF") - raise NotImplementedError - - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(OTAComponent), cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port, - cv.Optional(CONF_PASSWORD): cv.All(cv.string, validate_password_support), + cv.Optional(CONF_PASSWORD): cv.string, cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, @@ -98,9 +90,7 @@ async def to_code(config): ) cg.add(RawExpression(f"if ({condition}) return")) - if CORE.is_esp8266: - cg.add_library("Update", None) - elif CORE.is_esp32 and CORE.using_arduino: + if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) use_state_callback = False diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index c253e009c6..5c5b61a278 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -12,6 +12,7 @@ class OTABackend { 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 diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h index 8343bdf94f..f86a70d678 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -9,11 +9,13 @@ 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 diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index d1195af911..329f2cf0f2 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -5,6 +5,7 @@ #include "ota_component.h" #include "ota_backend.h" +#include "esphome/core/macros.h" namespace esphome { namespace ota { @@ -16,6 +17,11 @@ class ArduinoESP8266OTABackend : public OTABackend { 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 diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 4eb17d82f1..336b3798d9 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -4,6 +4,7 @@ #include "ota_backend_esp_idf.h" #include "ota_component.h" #include +#include "esphome/components/md5/md5.h" namespace esphome { namespace ota { @@ -24,15 +25,15 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { } return OTA_RESPONSE_ERROR_UNKNOWN; } + this->md5_.init(); return OTA_RESPONSE_OK; } -void IDFOTABackend::set_update_md5(const char *md5) { - // pass -} +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; @@ -45,6 +46,11 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { } 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) { diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index d6e2e2742a..af09d0d693 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -5,6 +5,7 @@ #include "ota_component.h" #include "ota_backend.h" #include +#include "esphome/components/md5/md5.h" namespace esphome { namespace ota { @@ -16,10 +17,13 @@ class IDFOTABackend : public OTABackend { 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 diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 9ad3814f5c..92256eb1b6 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -8,15 +8,12 @@ #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 -#ifdef USE_OTA_PASSWORD -#include -#endif - namespace esphome { namespace ota { @@ -107,6 +104,8 @@ 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; @@ -142,14 +141,14 @@ void OTAComponent::handle_() { 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 @@ -157,61 +156,67 @@ void OTAComponent::handle_() { buf[1] = OTA_VERSION_1_0; this->writeall_(buf, 2); + backend = make_ota_backend(); + // Read features - 1 byte if (!this->readall_(buf, 1)) { ESP_LOGW(TAG, "Reading features failed!"); - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT ESP_LOGV(TAG, "OTA features is 0x%02X", ota_features); // Acknowledge header - 1 byte buf[0] = OTA_RESPONSE_HEADER_OK; + if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { + buf[0] = OTA_RESPONSE_SUPPORTS_COMPRESSION; + } + this->writeall_(buf, 1); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { buf[0] = OTA_RESPONSE_REQUEST_AUTH; this->writeall_(buf, 1); - MD5Builder md5_builder{}; - md5_builder.begin(); + 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->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->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->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); @@ -223,7 +228,7 @@ 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 @@ -235,7 +240,7 @@ void OTAComponent::handle_() { // Read size, 4 bytes MSB first 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++) { @@ -244,10 +249,9 @@ void OTAComponent::handle_() { } ESP_LOGV(TAG, "OTA size is %u bytes", ota_size); - backend = make_ota_backend(); error_code = backend->begin(ota_size); if (error_code != OTA_RESPONSE_OK) - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) update_started = true; // Acknowledge prepare OK - 1 byte @@ -257,7 +261,7 @@ void OTAComponent::handle_() { // Read binary MD5, 32 bytes 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); @@ -273,17 +277,24 @@ void OTAComponent::handle_() { 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; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } else if (read == 0) { + // $ man recv + // "When a stream socket peer has performed an orderly shutdown, the return value will + // be 0 (the traditional "end-of-file" return)." + ESP_LOGW(TAG, "Remote end closed connection"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } error_code = backend->write(buf, read); if (error_code != OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Error writing binary data to flash!"); - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } total += read; @@ -295,8 +306,9 @@ void OTAComponent::handle_() { #ifdef USE_OTA_STATE_CALLBACK this->state_callback_.call(OTA_IN_PROGRESS, percentage, 0); #endif - // slow down OTA update to avoid getting killed by task watchdog (task_wdt) - delay(10); + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); } } @@ -307,7 +319,7 @@ void OTAComponent::handle_() { error_code = backend->end(); if (error_code != OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Error ending OTA!"); - goto error; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // Acknowledge Update end OK - 1 byte @@ -360,14 +372,19 @@ bool OTAComponent::readall_(uint8_t *buf, size_t len) { ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); delay(1); continue; } ESP_LOGW(TAG, "Failed to read %d bytes of data, errno: %d", len, errno); return false; + } else if (read == 0) { + ESP_LOGW(TAG, "Remote closed connection"); + return false; } else { at += read; } + App.feed_wdt(); delay(1); } @@ -386,6 +403,7 @@ bool OTAComponent::writeall_(const uint8_t *buf, size_t len) { ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); delay(1); continue; } @@ -394,6 +412,7 @@ bool OTAComponent::writeall_(const uint8_t *buf, size_t len) { } else { at += written; } + App.feed_wdt(); delay(1); } return true; diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index e08e187df6..5647d52eeb 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -19,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, diff --git a/esphome/components/output/switch/__init__.py b/esphome/components/output/switch/__init__.py index 11d073d28c..46135d117e 100644 --- a/esphome/components/output/switch/__init__.py +++ b/esphome/components/output/switch/__init__.py @@ -1,15 +1,28 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output, switch -from esphome.const import CONF_ID, CONF_OUTPUT +from esphome.const import CONF_ID, CONF_OUTPUT, CONF_RESTORE_MODE from .. import output_ns OutputSwitch = output_ns.class_("OutputSwitch", switch.Switch, cg.Component) +OutputSwitchRestoreMode = output_ns.enum("OutputSwitchRestoreMode") +RESTORE_MODES = { + "RESTORE_DEFAULT_OFF": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_DEFAULT_OFF, + "RESTORE_DEFAULT_ON": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_DEFAULT_ON, + "ALWAYS_OFF": OutputSwitchRestoreMode.OUTPUT_SWITCH_ALWAYS_OFF, + "ALWAYS_ON": OutputSwitchRestoreMode.OUTPUT_SWITCH_ALWAYS_ON, + "RESTORE_INVERTED_DEFAULT_OFF": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + "RESTORE_INVERTED_DEFAULT_ON": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_ON, +} + CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(OutputSwitch), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -21,3 +34,5 @@ async def to_code(config): output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/output/switch/output_switch.cpp b/esphome/components/output/switch/output_switch.cpp index 8db45f3a2b..3691896cbe 100644 --- a/esphome/components/output/switch/output_switch.cpp +++ b/esphome/components/output/switch/output_switch.cpp @@ -8,15 +8,32 @@ static const char *const TAG = "output.switch"; void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); } void OutputSwitch::setup() { - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; - - if (*restored) { - this->turn_on(); - } else { - this->turn_off(); + bool initial_state = false; + switch (this->restore_mode_) { + case OUTPUT_SWITCH_RESTORE_DEFAULT_OFF: + initial_state = this->get_initial_state().value_or(false); + break; + case OUTPUT_SWITCH_RESTORE_DEFAULT_ON: + initial_state = this->get_initial_state().value_or(true); + break; + case OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_OFF: + initial_state = !this->get_initial_state().value_or(true); + break; + case OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_ON: + initial_state = !this->get_initial_state().value_or(false); + break; + case OUTPUT_SWITCH_ALWAYS_OFF: + initial_state = false; + break; + case OUTPUT_SWITCH_ALWAYS_ON: + initial_state = true; + break; } + + if (initial_state) + this->turn_on(); + else + this->turn_off(); } void OutputSwitch::write_state(bool state) { if (state) { diff --git a/esphome/components/output/switch/output_switch.h b/esphome/components/output/switch/output_switch.h index fc9540fede..fc2c110662 100644 --- a/esphome/components/output/switch/output_switch.h +++ b/esphome/components/output/switch/output_switch.h @@ -7,18 +7,30 @@ namespace esphome { namespace output { +enum OutputSwitchRestoreMode { + OUTPUT_SWITCH_RESTORE_DEFAULT_OFF, + OUTPUT_SWITCH_RESTORE_DEFAULT_ON, + OUTPUT_SWITCH_ALWAYS_OFF, + OUTPUT_SWITCH_ALWAYS_ON, + OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_ON, +}; + class OutputSwitch : public switch_::Switch, public Component { public: void set_output(BinaryOutput *output) { output_ = output; } + void set_restore_mode(OutputSwitchRestoreMode restore_mode) { restore_mode_ = restore_mode; } + 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: void write_state(bool state) override; output::BinaryOutput *output_; + OutputSwitchRestoreMode restore_mode_; }; } // namespace output diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index df0f0de13d..7483d65b9d 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_REF, CONF_REFRESH, CONF_URL, + CONF_USERNAME, + CONF_PASSWORD, ) import esphome.config_validation as cv @@ -93,6 +95,8 @@ 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), @@ -124,6 +128,8 @@ def _process_base_package(config: dict) -> dict: 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] diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 1ad6f4a665..957f4062fc 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -1,6 +1,7 @@ #include "pca9685_output.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace pca9685 { diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index 15c1c5f076..fc012aaa39 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -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/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 7dbbd798ad..13a08bbd16 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -413,8 +413,6 @@ void Pipsolar::loop() { this->state_ = STATE_IDLE; break; case POLLING_QT: - this->state_ = STATE_IDLE; - break; case POLLING_QMN: this->state_ = STATE_IDLE; break; @@ -481,7 +479,7 @@ void Pipsolar::loop() { ESP_LOGD(TAG, "Decode QFLAG"); // result like:"(EbkuvxzDajy" // get through all char: ignore first "(" Enable flag on 'E', Disable on 'D') else set the corresponding value - for (int i = 1; i < strlen(tmp); i++) { + for (size_t i = 1; i < strlen(tmp); i++) { switch (tmp[i]) { case 'E': enabled = true; @@ -530,7 +528,7 @@ void Pipsolar::loop() { this->value_warnings_present_ = false; this->value_faults_present_ = true; - for (int i = 1; i < strlen(tmp); i++) { + for (size_t i = 1; i < strlen(tmp); i++) { enabled = tmp[i] == '1'; switch (i) { case 1: @@ -656,7 +654,7 @@ void Pipsolar::loop() { case 32: fc = tmp[i]; fc += tmp[i + 1]; - this->value_fault_code_ = strtol(fc.c_str(), nullptr, 10); + this->value_fault_code_ = parse_number(fc).value_or(0); break; case 34: this->value_warnung_low_pv_energy_ = enabled; diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 5e4dd6c40c..a206e41988 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -89,7 +89,7 @@ TYPES = { UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + 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 @@ -159,7 +159,7 @@ TYPES = { UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY ), CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + 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 diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 0474d6ffd0..5de94699f0 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -96,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: @@ -133,20 +134,25 @@ optional PMSX003Component::check_byte_() { void PMSX003Component::parse_data_() { switch (this->type_) { case PMSX003_TYPE_5003ST: { - uint16_t formaldehyde = this->get_16_bit_uint_(28); float temperature = this->get_16_bit_uint_(30) / 10.0f; float humidity = this->get_16_bit_uint_(32) / 10.0f; - ESP_LOGD(TAG, "Got Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3", temperature, humidity, - formaldehyde); + 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 PMS5003ST matches the PMS5003, continue on + // 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); diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index a5adecb534..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 { diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 350117a235..56a91d22fc 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -42,21 +42,23 @@ PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) TYPE_PMSX003 = "PMSX003" TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003ST = "PMS5003ST" +TYPE_PMS5003S = "PMS5003S" 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], } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index ed2a2c1e35..0c46ff8a57 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -145,7 +145,7 @@ void PN532::loop() { if (nfcid.size() == this->current_uid_.size()) { bool same_uid = false; - for (uint8_t i = 0; i < nfcid.size(); i++) + for (size_t i = 0; i < nfcid.size(); i++) same_uid |= nfcid[i] == this->current_uid_[i]; if (same_uid) return; @@ -367,7 +367,7 @@ bool PN532BinarySensor::process(std::vector &data) { if (data.size() != this->uid_.size()) return false; - for (uint8_t i = 0; i < data.size(); i++) { + for (size_t i = 0; i < data.size(); i++) { if (data[i] != this->uid_[i]) return false; } diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index ec32e45b3d..be58f265b9 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -26,7 +26,7 @@ bool PN532Spi::write_data(const std::vector &data) { delay(2); // First byte, communication mode: Write data this->write_byte(0x01); - ESP_LOGV(TAG, "Writing data: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty(data).c_str()); this->write_array(data.data(), data.size()); this->disable(); @@ -65,7 +65,7 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { this->read_array(data.data(), len); this->disable(); data.insert(data.begin(), 0x01); - ESP_LOGV(TAG, "Read data: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Read data: %s", format_hex_pretty(data).c_str()); return true; } @@ -97,7 +97,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { std::vector header(7); this->read_array(header.data(), 7); - ESP_LOGV(TAG, "Header data: %s", hexencode(header).c_str()); + ESP_LOGV(TAG, "Header data: %s", format_hex_pretty(header).c_str()); if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { // invalid packet @@ -127,7 +127,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { this->read_array(data.data(), len + 1); this->disable(); - ESP_LOGV(TAG, "Response data: %s", hexencode(data).c_str()); + 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++) { diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index f5f166d085..45345f06e8 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -15,7 +15,8 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), - } + }, + cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index fa7b4fe132..618c866d5b 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace prometheus { void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { - AsyncResponseStream *stream = req->beginResponseStream("text/plain"); + AsyncResponseStream *stream = req->beginResponseStream("text/plain; version=0.0.4; charset=utf-8"); #ifdef USE_SENSOR this->sensor_type_(stream); diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py new file mode 100644 index 0000000000..ac6d034514 --- /dev/null +++ b/esphome/components/psram/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.core import CORE +from esphome.const import ( + CONF_ID, +) + +CODEOWNERS = ["@esphome/core"] + +psram_ns = cg.esphome_ns.namespace("psram") +PsramComponent = psram_ns.class_("PsramComponent", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(PsramComponent)}), cv.only_on_esp32 +) + + +async def to_code(config): + if CORE.using_arduino: + cg.add_build_flag("-DBOARD_HAS_PSRAM") + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp new file mode 100644 index 0000000000..8325709632 --- /dev/null +++ b/esphome/components/psram/psram.cpp @@ -0,0 +1,32 @@ +#include "psram.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +#include +#include + +namespace esphome { +namespace psram { + +static const char *const TAG = "psram"; + +void PsramComponent::dump_config() { + // Technically this can be false if the PSRAM is full, but heap_caps_get_total_size() isn't always available, and it's + // very unlikely for the PSRAM to be full. + bool available = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0; + + ESP_LOGCONFIG(TAG, "PSRAM:"); + ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) + if (available) { + ESP_LOGCONFIG(TAG, " Size: %d MB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024 / 1024); + } +#endif +} + +} // namespace psram +} // namespace esphome + +#endif diff --git a/esphome/components/psram/psram.h b/esphome/components/psram/psram.h new file mode 100644 index 0000000000..8c891feee9 --- /dev/null +++ b/esphome/components/psram/psram.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" + +namespace esphome { +namespace psram { + +class PsramComponent : public Component { + void dump_config() override; +}; + +} // namespace psram +} // namespace esphome + +#endif diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index f538a4c905..5232ebc427 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "pulse_counter"; const char *const EDGE_MODE_TO_STRING[] = {"DISABLE", "INCREMENT", "DECREMENT"}; -#ifdef USE_ESP8266 +#ifndef HAS_PCNT void IRAM_ATTR PulseCounterStorage::gpio_intr(PulseCounterStorage *arg) { const uint32_t now = micros(); const bool discard = now - arg->last_pulse < arg->filter_us; @@ -43,7 +43,7 @@ pulse_counter_t PulseCounterStorage::read_raw_value() { } #endif -#ifdef USE_ESP32 +#ifdef HAS_PCNT bool PulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; this->pin = pin; @@ -96,7 +96,7 @@ bool PulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint16_t filter_val = std::min(this->filter_us * 80u, 1023u); + uint16_t filter_val = std::min(static_cast(this->filter_us * 80u), 1023u); ESP_LOGCONFIG(TAG, " Filter Value: %uus (val=%u)", this->filter_us, filter_val); error = pcnt_set_filter_value(this->pcnt_unit, filter_val); if (error != ESP_OK) { @@ -155,16 +155,20 @@ 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 - - ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value); - this->publish_state(value); + 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); + } 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; } } // namespace pulse_counter diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 94e37bc232..86c387d52a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -4,8 +4,9 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include +#define HAS_PCNT #endif namespace esphome { @@ -17,10 +18,9 @@ enum PulseCounterCountMode { PULSE_COUNTER_DECREMENT, }; -#ifdef USE_ESP32 +#ifdef HAS_PCNT using pulse_counter_t = int16_t; -#endif -#ifdef USE_ESP8266 +#else using pulse_counter_t = int32_t; #endif @@ -30,16 +30,15 @@ struct PulseCounterStorage { static void gpio_intr(PulseCounterStorage *arg); -#ifdef USE_ESP8266 +#ifndef HAS_PCNT volatile pulse_counter_t counter{0}; volatile uint32_t last_pulse{0}; #endif InternalGPIOPin *pin; -#ifdef USE_ESP32 +#ifdef HAS_PCNT pcnt_unit_t pcnt_unit; -#endif -#ifdef USE_ESP8266 +#else ISRInternalGPIOPin isr_pin; #endif PulseCounterCountMode rising_edge_mode{PULSE_COUNTER_INCREMENT}; @@ -65,7 +64,8 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { protected: InternalGPIOPin *pin_; PulseCounterStorage storage_; - uint32_t current_total_ = 0; + uint32_t last_time_{0}; + uint32_t current_total_{0}; sensor::Sensor *total_sensor_; }; diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index fd1403b4fd..7d526b241b 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -35,7 +35,7 @@ void PulseMeterSensor::loop() { this->publish_state(0); } else { // Calculate pulses/min from the pulse width in ms - this->publish_state((60.0 * 1000.0) / pulse_width_ms); + this->publish_state((60.0f * 1000.0f) / pulse_width_ms); } } diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py index b17878f01b..12090bddba 100644 --- a/esphome/components/pvvx_mithermometer/sensor.py +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -49,12 +50,14 @@ CONFIG_SCHEMA = ( 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, ), } ) diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index b1a9607304..c3738d1852 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -7,6 +7,7 @@ namespace pzemac { static const char *const TAG = "pzemac"; static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers void PZEMAC::on_modbus_data(const std::vector &data) { @@ -73,5 +74,12 @@ void PZEMAC::dump_config() { LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); } +void PZEMAC::reset_energy_() { + std::vector cmd; + cmd.push_back(this->address_); + cmd.push_back(PZEM_CMD_RESET_ENERGY); + this->send_raw(cmd); +} + } // namespace pzemac } // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 07f661535f..e9f76972a3 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" @@ -7,6 +8,8 @@ namespace esphome { namespace pzemac { +template class ResetEnergyAction; + class PZEMAC : public PollingComponent, public modbus::ModbusDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } @@ -23,12 +26,25 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { void dump_config() override; protected: + template friend class ResetEnergyAction; sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; sensor::Sensor *energy_sensor_; sensor::Sensor *frequency_sensor_; sensor::Sensor *power_factor_sensor_; + + void reset_energy_(); +}; + +template class ResetEnergyAction : public Action { + public: + ResetEnergyAction(PZEMAC *pzemac) : pzemac_(pzemac) {} + + void play(Ts... x) override { this->pzemac_->reset_energy_(); } + + protected: + PZEMAC *pzemac_; }; } // namespace pzemac diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index b6697e3d19..ab7dd3e202 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -1,5 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id from esphome.components import sensor, modbus from esphome.const import ( CONF_CURRENT, @@ -29,6 +31,9 @@ AUTO_LOAD = ["modbus"] pzemac_ns = cg.esphome_ns.namespace("pzemac") PZEMAC = pzemac_ns.class_("PZEMAC", cg.PollingComponent, modbus.ModbusDevice) +# Actions +ResetEnergyAction = pzemac_ns.class_("ResetEnergyAction", automation.Action) + CONFIG_SCHEMA = ( cv.Schema( { @@ -75,6 +80,20 @@ CONFIG_SCHEMA = ( ) +@automation.register_action( + "pzemac.reset_energy", + ResetEnergyAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PZEMAC), + } + ), +) +async def reset_energy_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]) await cg.register_component(var, config) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 385641fea0..d203b3ce8f 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -28,7 +28,7 @@ std::string format_buffer(uint8_t *b, uint8_t len) { std::string format_uid(std::vector &uid) { char buf[32]; int offset = 0; - for (uint8_t i = 0; i < uid.size(); i++) { + for (size_t i = 0; i < uid.size(); i++) { const char *format = "%02X"; if (i + 1 < uid.size()) format = "%02X-"; @@ -479,7 +479,7 @@ bool RC522BinarySensor::process(std::vector &data) { if (data.size() != this->uid_.size()) result = false; else { - for (uint8_t i = 0; i < data.size(); i++) { + for (size_t i = 0; i < data.size(); i++) { if (data[i] != this->uid_[i]) { result = false; break; diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index d2b848600d..72a91a99dd 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -27,6 +27,7 @@ from esphome.const import ( CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2, + CONF_LEVEL, ) from esphome.core import coroutine from esphome.jsonschema import jschema_extractor @@ -234,6 +235,45 @@ async def build_dumpers(config): return dumpers +# Coolix +( + CoolixData, + CoolixBinarySensor, + CoolixTrigger, + CoolixAction, + CoolixDumper, +) = declare_protocol("Coolix") +COOLIX_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) + + +@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SCHEMA) +def coolix_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CoolixData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("coolix", CoolixTrigger, CoolixData) +def coolix_trigger(var, config): + pass + + +@register_dumper("coolix", CoolixDumper) +def coolix_dumper(var, config): + pass + + +@register_action("coolix", CoolixAction, COOLIX_SCHEMA) +async def coolix_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + cg.add(var.set_data(template_)) + + # Dish DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( "Dish" @@ -439,6 +479,49 @@ async def pioneer_action(var, config, args): 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" @@ -1081,6 +1164,58 @@ async def panasonic_action(var, config, args): cg.add(var.set_command(template_)) +# Nexa +NexaData, NexaBinarySensor, NexaTrigger, NexaAction, NexaDumper = declare_protocol( + "Nexa" +) +NEXA_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint32_t, + cv.Required(CONF_GROUP): cv.hex_uint8_t, + cv.Required(CONF_STATE): cv.hex_uint8_t, + cv.Required(CONF_CHANNEL): cv.hex_uint8_t, + cv.Required(CONF_LEVEL): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("nexa", NexaBinarySensor, NEXA_SCHEMA) +def nexa_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + NexaData, + ("device", config[CONF_DEVICE]), + ("group", config[CONF_GROUP]), + ("state", config[CONF_STATE]), + ("channel", config[CONF_CHANNEL]), + ("level", config[CONF_LEVEL]), + ) + ) + ) + + +@register_trigger("nexa", NexaTrigger, NexaData) +def nexa_trigger(var, config): + pass + + +@register_dumper("nexa", NexaDumper) +def nexa_dumper(var, config): + pass + + +@register_action("nexa", NexaAction, NEXA_SCHEMA) +def nexa_action(var, config, args): + cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint32)))) + cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8)))) + cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, cg.uint8)))) + cg.add( + var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + ) + cg.add(var.set_level((yield cg.templatable(config[CONF_LEVEL], args, cg.uint8)))) + + # Midea MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( "Midea" diff --git a/esphome/components/remote_base/coolix_protocol.cpp b/esphome/components/remote_base/coolix_protocol.cpp new file mode 100644 index 0000000000..3e6e7e185a --- /dev/null +++ b/esphome/components/remote_base/coolix_protocol.cpp @@ -0,0 +1,84 @@ +#include "coolix_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.coolix"; + +static const int32_t TICK_US = 560; +static const int32_t HEADER_MARK_US = 8 * TICK_US; +static const int32_t HEADER_SPACE_US = 8 * TICK_US; +static const int32_t BIT_MARK_US = 1 * TICK_US; +static const int32_t BIT_ONE_SPACE_US = 3 * TICK_US; +static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; +static const int32_t FOOTER_MARK_US = 1 * TICK_US; +static const int32_t FOOTER_SPACE_US = 10 * TICK_US; + +static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { + // Break data into bytes, starting at the Most Significant + // Byte. Each byte then being sent normal, then followed inverted. + for (unsigned shift = 16;; shift -= 8) { + // Grab a bytes worth of data. + const uint8_t byte = src >> shift; + // Normal + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); + // Inverted + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (byte & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); + // Data end + if (shift == 0) + break; + } +} + +void CoolixProtocol::encode(RemoteTransmitData *dst, const CoolixData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 2 * 48 + 2 + 2 + 2 * 48 + 1); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + encode_data(dst, data); + dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + encode_data(dst, data); + dst->mark(FOOTER_MARK_US); +} + +static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { + uint32_t data = 0; + for (unsigned n = 3;; data <<= 8) { + // Read byte + for (uint32_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_mark(BIT_MARK_US)) + return false; + if (src.expect_space(BIT_ONE_SPACE_US)) + data |= mask; + else if (!src.expect_space(BIT_ZERO_SPACE_US)) + return false; + } + // Check for inverse byte + for (uint32_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_item(BIT_MARK_US, (data & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) + return false; + } + // Checking the end of reading + if (--n == 0) { + dst = data; + return true; + } + } +} + +optional CoolixProtocol::decode(RemoteReceiveData data) { + CoolixData first, second; + if (data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(data, first) && + data.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && + decode_data(data, second) && data.expect_mark(FOOTER_MARK_US) && first == second) + return first; + return {}; +} + +void CoolixProtocol::dump(const CoolixData &data) { ESP_LOGD(TAG, "Received Coolix: 0x%06X", data); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h new file mode 100644 index 0000000000..9ce3eabb0e --- /dev/null +++ b/esphome/components/remote_base/coolix_protocol.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +using CoolixData = uint32_t; + +class CoolixProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const CoolixData &data) override; + optional decode(RemoteReceiveData data) override; + void dump(const CoolixData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Coolix) + +template class CoolixAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(CoolixData, data) + void encode(RemoteTransmitData *dst, Ts... x) override { + CoolixData data = this->data_.value(x...); + CoolixProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp index baf64f246f..bf67429001 100644 --- a/esphome/components/remote_base/midea_protocol.cpp +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -6,89 +6,63 @@ namespace remote_base { static const char *const TAG = "remote.midea"; +static const int32_t TICK_US = 560; +static const int32_t HEADER_MARK_US = 8 * TICK_US; +static const int32_t HEADER_SPACE_US = 8 * TICK_US; +static const int32_t BIT_MARK_US = 1 * TICK_US; +static const int32_t BIT_ONE_SPACE_US = 3 * TICK_US; +static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; +static const int32_t FOOTER_MARK_US = 1 * TICK_US; +static const int32_t FOOTER_SPACE_US = 10 * TICK_US; + 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); + for (uint8_t idx = 0; idx < OFFSET_CS; idx++) + cs -= reverse_bits(this->data_[idx]); + return reverse_bits(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; +bool MideaData::is_compliment(const MideaData &rhs) const { + return std::equal(this->data_.begin(), this->data_.end(), rhs.data_.begin(), + [](const uint8_t &a, const uint8_t &b) { return a + b == 255; }); } -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) { +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &src) { 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); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 1); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + for (unsigned idx = 0; idx < 6; idx++) + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (src[idx] & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); + dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + for (unsigned idx = 0; idx < 6; idx++) + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (src[idx] & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); + dst->mark(FOOTER_MARK_US); } -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)) +static bool decode_data(RemoteReceiveData &src, MideaData &dst) { + for (unsigned idx = 0; idx < 6; idx++) { + uint8_t data = 0; + for (uint8_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_mark(BIT_MARK_US)) + return false; + if (src.expect_space(BIT_ONE_SPACE_US)) + data |= mask; + else if (!src.expect_space(BIT_ZERO_SPACE_US)) return false; } + dst[idx] = data; } 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)) + if (src.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(src, out) && out.is_valid() && + src.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && src.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && + decode_data(src, inv) && src.expect_mark(FOOTER_MARK_US) && out.is_compliment(inv)) return out; return {}; } diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 35ea23acfb..135a93b36d 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "remote_base.h" @@ -9,70 +10,61 @@ namespace remote_base { class MideaData { public: - // Make zero-filled - MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make default + MideaData() {} // Make from initializer_list - MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + MideaData(std::initializer_list data) { + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); + } // Make from vector MideaData(const std::vector &data) { - memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Default copy constructor MideaData(const MideaData &) = default; - uint8_t *data() { return this->data_; } - const uint8_t *data() const { return this->data_; } - uint8_t size() const { return sizeof(this->data_); } + uint8_t *data() { return this->data_.data(); } + const uint8_t *data() const { return this->data_.data(); } + uint8_t size() const { return this->data_.size(); } bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } - bool check_compliment(const MideaData &rhs) const; - std::string to_string() const { return hexencode(*this); } + bool is_compliment(const MideaData &rhs) const; + std::string to_string() const { return format_hex_pretty(this->data_.data(), this->data_.size()); } // compare only 40-bits - bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + bool operator==(const MideaData &rhs) const { + return std::equal(this->data_.begin(), this->data_.begin() + OFFSET_CS, rhs.data_.begin()); + } enum MideaDataType : uint8_t { - MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_CONTROL = 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); } + uint8_t &operator[](size_t idx) { return this->data_[idx]; } + const uint8_t &operator[](size_t idx) const { return this->data_[idx]; } 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); + uint8_t get_value_(uint8_t idx, uint8_t mask = 255, uint8_t shift = 0) const { + return (this->data_[idx] >> shift) & mask; } + void set_value_(uint8_t idx, uint8_t value, uint8_t mask = 255, uint8_t shift = 0) { + this->data_[idx] &= ~(mask << shift); + this->data_[idx] |= (value << shift); + } + void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } static const uint8_t OFFSET_CS = 5; // 48-bits data - uint8_t data_[6]; + std::array data_; // Calculate checksum uint8_t calc_cs_() const; }; class MideaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MideaData &data) override; + void encode(RemoteTransmitData *dst, const MideaData &src) 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 { diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 79a30903a4..47b4d676dd 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -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/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp new file mode 100644 index 0000000000..814b46135a --- /dev/null +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -0,0 +1,235 @@ +#include "nexa_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.nexa"; + +static const uint8_t NBITS = 32; +static const uint32_t HEADER_HIGH_US = 319; +static const uint32_t HEADER_LOW_US = 2610; +static const uint32_t BIT_HIGH_US = 319; +static const uint32_t BIT_ONE_LOW_US = 1000; +static const uint32_t BIT_ZERO_LOW_US = 140; + +static const uint32_t TX_HEADER_HIGH_US = 250; +static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static const uint32_t TX_BIT_HIGH_US = 250; +static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; + +void NexaProtocol::one(RemoteTransmitData *dst) const { + // '1' => '10' + dst->item(TX_BIT_HIGH_US, TX_BIT_ONE_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); +} + +void NexaProtocol::zero(RemoteTransmitData *dst) const { + // '0' => '01' + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ONE_LOW_US); +} + +void NexaProtocol::sync(RemoteTransmitData *dst) const { dst->item(TX_HEADER_HIGH_US, TX_HEADER_LOW_US); } + +void NexaProtocol::encode(RemoteTransmitData *dst, const NexaData &data) { + dst->set_carrier_frequency(0); + + // Send SYNC + this->sync(dst); + + // Device (26 bits) + for (int16_t i = 26 - 1; i >= 0; i--) { + if (data.device & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + + // Group (1 bit) + if (data.group != 0) + this->one(dst); + else + this->zero(dst); + + // State (1 bit) + if (data.state == 2) { + // Special case for dimmers...send 00 as state + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + } else if (data.state == 1) + this->one(dst); + else + this->zero(dst); + + // Channel (4 bits) + for (int16_t i = 4 - 1; i >= 0; i--) { + if (data.channel & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + + // Level (4 bits) + if (data.state == 2) { + for (int16_t i = 4 - 1; i >= 0; i--) { + if (data.level & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + } + + // Send finishing Zero + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); +} + +optional NexaProtocol::decode(RemoteReceiveData src) { + NexaData out{ + .device = 0, + .group = 0, + .state = 0, + .channel = 0, + .level = 0, + }; + + // From: http://tech.jolowe.se/home-automation-rf-protocols/ + // New data: http://tech.jolowe.se/old-home-automation-rf-protocols/ + /* + + SHHHH HHHH HHHH HHHH HHHH HHHH HHGO EE BB DDDD 0 P + + S = Sync bit. + H = The first 26 bits are transmitter unique codes, and it is this code that the reciever "learns" to recognize. + G = Group code, set to one for the whole group. + O = On/Off bit. Set to 1 for on, 0 for off. + E = Unit to be turned on or off. The code is inverted, i.e. '11' equals 1, '00' equals 4. + B = Button code. The code is inverted, i.e. '11' equals 1, '00' equals 4. + D = Dim level bits. + 0 = packet always ends with a zero. + P = Pause, a 10 ms pause in between re-send. + + Update: First of all the '1' and '0' bit seems to be reversed (and be the same as Jula I protocol below), i.e. + + */ + + // Require a SYNC pulse + long gap + if (!src.expect_pulse_with_gap(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + // Device + for (uint8_t i = 0; i < 26; i++) { + out.device <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.device |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.device |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // GROUP + for (uint8_t i = 0; i < 1; i++) { + out.group <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.group |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.group |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // STATE + for (uint8_t i = 0; i < 1; i++) { + out.state <<= 1UL; + + // Special treatment as we should handle 01, 10 and 00 + // We need to care for the advance made in the expect functions + // hence take them one at a time so that we do not get out of sync + // in decoding + + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // Starts with '1' + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // '10' => 1 + out.state |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // '11' => NOT OK + // This case is here to make sure we advance through the correct index + // This should not happen...failed command + return {}; + } + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // Starts with '0' + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // '01' => 0 + out.state |= 0x00; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // '00' => Special case for dimmer! => 2 + out.state |= 0x02; + } + } + } + + // CHANNEL (EE and BB bits) + for (uint8_t i = 0; i < 4; i++) { + out.channel <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.channel |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.channel |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // Optional to transmit LEVEL data (8 bits more) + if (int32_t(src.get_index() + 8) >= src.size()) { + return out; + } + + // LEVEL + for (uint8_t i = 0; i < 4; i++) { + out.level <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.level |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.level |= 0x00; + } else { + // This should not happen...failed command + break; + } + } + + return out; +} + +void NexaProtocol::dump(const NexaData &data) { + ESP_LOGD(TAG, "Received NEXA: device=0x%04X group=%d state=%d channel=%d level=%d", data.device, data.group, + data.state, data.channel, data.level); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h new file mode 100644 index 0000000000..f1ce380780 --- /dev/null +++ b/esphome/components/remote_base/nexa_protocol.h @@ -0,0 +1,52 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct NexaData { + uint32_t device; + uint8_t group; + uint8_t state; + uint8_t channel; + uint8_t level; + bool operator==(const NexaData &rhs) const { + return device == rhs.device && group == rhs.group && state == rhs.state && channel == rhs.channel && + level == rhs.level; + } +}; + +class NexaProtocol : public RemoteProtocol { + public: + void one(RemoteTransmitData *dst) const; + void zero(RemoteTransmitData *dst) const; + void sync(RemoteTransmitData *dst) const; + + void encode(RemoteTransmitData *dst, const NexaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const NexaData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Nexa) + +template class NexaAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, device) + TEMPLATABLE_VALUE(uint8_t, group) + TEMPLATABLE_VALUE(uint8_t, state) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, level) + void encode(RemoteTransmitData *dst, Ts... x) override { + NexaData data{}; + data.device = this->device_.value(x...); + data.group = this->group_.value(x...); + data.state = this->state_.value(x...); + data.channel = this->channel_.value(x...); + data.level = this->level_.value(x...); + NexaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome 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/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 6b7d1b725a..1dc094d552 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -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; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index a853c9849e..97ee027b84 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -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 dd6f7c3482..e1af41274e 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -116,6 +116,16 @@ class RemoteReceiveData { return false; } + bool expect_pulse_with_gap(uint32_t mark, uint32_t space) { + if (this->peek_mark(mark, 0) && this->peek_space_at_least(space, 1)) { + this->advance(2); + return true; + } + return false; + } + + uint32_t get_index() { return index_; } + void reset() { this->index_ = 0; } int32_t pos(uint32_t index) const { return (*this->data_)[index]; } diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index dde9b843c9..5a7fb3c985 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -78,6 +78,7 @@ void RemoteReceiverComponent::loop() { if (this->temp_.empty()) return; + this->temp_.push_back(-this->idle_us_); this->call_listeners_dumpers_(); } } @@ -86,9 +87,10 @@ 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); } else { @@ -102,8 +104,8 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { } 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) { @@ -120,10 +122,6 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { 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) { @@ -139,10 +137,6 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { 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) { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 733ac5e50d..a4235e875f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -32,6 +32,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, 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 USE_ESP32 diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 500d7193f3..368b21f892 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -113,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)); @@ -121,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 33c01985d7..39752cac5b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -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/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/__init__.py similarity index 73% rename from esphome/components/restart/switch.py rename to esphome/components/restart/switch/__init__.py index 4f1904e273..de30392b45 100644 --- a/esphome/components/restart/switch.py +++ b/esphome/components/restart/switch/__init__.py @@ -1,7 +1,14 @@ 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 +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) @@ -13,6 +20,9 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( "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) diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/switch/restart_switch.cpp similarity index 100% rename from esphome/components/restart/restart_switch.cpp rename to esphome/components/restart/switch/restart_switch.cpp 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/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index a4259e5aa2..d8c8047496 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -52,7 +52,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { if (action == RF_CODE_LEARN_OK) ESP_LOGD(TAG, "Learning success"); - ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, + 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->data_callback_.call(data); break; @@ -73,7 +73,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { data.code += next_byte; } - ESP_LOGD(TAG, "Received RFBridge Advanced Code: length=0x%02X protocol=0x%02X code=0x%s", data.length, + 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; @@ -97,7 +97,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { str += " "; } } - ESP_LOGD(TAG, "Received RFBridge Bucket: %s", str.c_str()); + ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str()); break; } default: @@ -186,7 +186,7 @@ void RFBridgeComponent::dump_config() { } void RFBridgeComponent::start_advanced_sniffing() { - ESP_LOGD(TAG, "Advanced Sniffing on"); + ESP_LOGI(TAG, "Advanced Sniffing on"); this->write(RF_CODE_START); this->write(RF_CODE_SNIFFING_ON); this->write(RF_CODE_STOP); @@ -194,7 +194,7 @@ void RFBridgeComponent::start_advanced_sniffing() { } void RFBridgeComponent::stop_advanced_sniffing() { - ESP_LOGD(TAG, "Advanced Sniffing off"); + ESP_LOGI(TAG, "Advanced Sniffing off"); this->write(RF_CODE_START); this->write(RF_CODE_SNIFFING_OFF); this->write(RF_CODE_STOP); @@ -202,7 +202,7 @@ void RFBridgeComponent::stop_advanced_sniffing() { } void RFBridgeComponent::start_bucket_sniffing() { - ESP_LOGD(TAG, "Raw Bucket Sniffing on"); + ESP_LOGI(TAG, "Raw Bucket Sniffing on"); this->write(RF_CODE_START); this->write(RF_CODE_RFIN_BUCKET); this->write(RF_CODE_STOP); diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 7c95fac98e..aff8fc381c 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -125,6 +125,22 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore 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(); @@ -142,6 +158,18 @@ void RotaryEncoderSensor::dump_config() { 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"); @@ -189,13 +217,20 @@ void RotaryEncoderSensor::loop() { 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 4825e472a1..a69d738fa8 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -10,6 +10,12 @@ 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 = @@ -40,6 +46,15 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { 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. * * By default, this component will increment the counter by 1 with every A-B input cycle. @@ -58,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) @@ -79,6 +95,9 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { 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_{}; diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index ef1110c6d8..cd747264b3 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -14,9 +14,17 @@ from esphome.const import ( CONF_PIN_A, CONF_PIN_B, CONF_TRIGGER_ID, + CONF_RESTORE_MODE, ) 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, @@ -27,6 +35,7 @@ RESOLUTIONS = { 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 @@ -70,6 +79,10 @@ CONFIG_SCHEMA = cv.All( 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( @@ -99,6 +112,8 @@ async def to_code(config): cg.add(var.set_pin_a(pin_a)) 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 = await cg.gpio_pin_expression(config[CONF_PIN_RESET]) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index d571c2f287..c76d4a89b0 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -159,7 +159,7 @@ void Rtttl::loop() { // Now play the note if (note) { auto note_index = (scale - 4) * 12 + note; - if (note_index < 0 || note_index >= sizeof(NOTES)) { + if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { ESP_LOGE(TAG, "Note out of valid range"); return; } diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py index 342a5eff24..2bb9549195 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -19,6 +19,7 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_CELSIUS, @@ -95,22 +96,26 @@ CONFIG_SCHEMA = ( 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, ), } ) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index f150d6e086..ab884bfee4 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -CODEOWNERS = ["@paulmonigatti"] +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 index 0ad814ff4f..b6c3e852f6 100644 --- a/esphome/components/safe_mode/switch/__init__.py +++ b/esphome/components/safe_mode/switch/__init__.py @@ -3,10 +3,12 @@ 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 @@ -23,6 +25,9 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( "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) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index d1246d9766..272ee75e30 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -60,7 +60,7 @@ void SCD30Component::setup() { // According ESP32 clock stretching is typically 30ms and up to 150ms "due to // internal calibration processes". The I2C peripheral only supports 13ms (at // least when running at 80MHz). - // In practise it seems that clock stretching occurs during this calibration + // 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. // @@ -69,6 +69,16 @@ void SCD30Component::setup() { 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_)) { @@ -99,6 +109,12 @@ void SCD30Component::setup() { 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() { @@ -128,19 +144,13 @@ void SCD30Component::dump_config() { ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_compensation_); ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); - LOG_UPDATE_INTERVAL(this); + 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(); @@ -186,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); diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index f11b7cc1f4..64193d0cb6 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -8,7 +8,7 @@ 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; } @@ -19,9 +19,10 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { 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; } @@ -30,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, @@ -41,6 +43,7 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { 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 c0317c96e0..cd25649f2a 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,3 +1,4 @@ +from esphome import core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor @@ -6,6 +7,7 @@ from esphome.const import ( CONF_HUMIDITY, CONF_TEMPERATURE, CONF_CO2, + CONF_UPDATE_INTERVAL, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -18,7 +20,7 @@ from esphome.const import ( DEPENDENCIES = ["i2c"] scd30_ns = cg.esphome_ns.namespace("scd30") -SCD30Component = scd30_ns.class_("SCD30Component", cg.PollingComponent, i2c.I2CDevice) +SCD30Component = scd30_ns.class_("SCD30Component", cg.Component, i2c.I2CDevice) CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" @@ -55,9 +57,15 @@ CONFIG_SCHEMA = ( ), 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.polling_component_schema("60s")) + .extend(cv.COMPONENT_SCHEMA) .extend(i2c.i2c_device_schema(0x61)) ) @@ -81,6 +89,8 @@ async def to_code(config): 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 = await sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index c91fd5e882..eacb39edf1 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -1,4 +1,5 @@ #include "scd4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -38,6 +39,7 @@ void SCD4XComponent::setup() { 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"); @@ -46,68 +48,72 @@ void SCD4XComponent::setup() { 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; + } - 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)); - uint16_t raw_serial_number[3]; - if (!this->read_data_(raw_serial_number, 3)) { - ESP_LOGE(TAG, "Failed to read serial number"); - this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; - this->mark_failed(); - return; - } - ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8), - uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); - - if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, - (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { - ESP_LOGE(TAG, "Error setting temperature offset."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - - // If pressure compensation available use it - // else use altitude - if (ambient_pressure_compensation_) { - if (!this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, ambient_pressure_compensation_)) { - ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + 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; } - } else { - if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { - ESP_LOGE(TAG, "Error setting altitude compensation."); + + // 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; } - } - 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; + } - // 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"); + initialized_ = true; + ESP_LOGD(TAG, "Sensor initialized"); + }); }); } @@ -150,6 +156,13 @@ void SCD4XComponent::update() { 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(); @@ -191,6 +204,28 @@ void SCD4XComponent::update() { 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; diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 3c428b8623..4fe2bf14cc 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -18,10 +18,8 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } - void set_ambient_pressure_compensation(float pressure) { - ambient_pressure_compensation_ = true; - ambient_pressure_ = (uint16_t)(pressure * 1000); - } + void set_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; } @@ -33,6 +31,7 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { 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_; @@ -47,6 +46,8 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { 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 diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 0b1a960f6f..3e814ffe78 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -29,6 +29,7 @@ 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( @@ -62,6 +63,9 @@ CONFIG_SCHEMA = ( ), 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")) @@ -92,7 +96,10 @@ async def to_code(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/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp index 2348c88938..9c35d306ad 100644 --- a/esphome/components/sdm_meter/sdm_meter.cpp +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -57,15 +57,19 @@ void SDMMeter::on_modbus_data(const std::vector &data) { phase.phase_angle_sensor_->publish_state(phase_angle); } + float total_power = sdm_meter_get_float(SDM_TOTAL_SYSTEM_POWER * 2); 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); + 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, T.P=%.3f W", + frequency, import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy, + total_power); + if (this->total_power_sensor_ != nullptr) + this->total_power_sensor_->publish_state(total_power); if (this->frequency_sensor_ != nullptr) this->frequency_sensor_->publish_state(frequency); if (this->import_active_energy_sensor_ != nullptr) @@ -95,6 +99,7 @@ void SDMMeter::dump_config() { LOG_SENSOR(" ", "Power Factor", phase.power_factor_sensor_); LOG_SENSOR(" ", "Phase Angle", phase.phase_angle_sensor_); } + LOG_SENSOR(" ", "Total Power", this->total_power_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_); diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h index 07ebe65bb7..66f0fb8c5e 100644 --- a/esphome/components/sdm_meter/sdm_meter.h +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -37,6 +37,7 @@ class SDMMeter : public PollingComponent, public modbus::ModbusDevice { this->phases_[phase].setup = true; this->phases_[phase].phase_angle_sensor_ = phase_angle_sensor; } + void set_total_power_sensor(sensor::Sensor *total_power_sensor) { this->total_power_sensor_ = total_power_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; @@ -69,6 +70,7 @@ class SDMMeter : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *phase_angle_sensor_{nullptr}; } phases_[3]; sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *total_power_sensor_{nullptr}; sensor::Sensor *import_active_energy_sensor_{nullptr}; sensor::Sensor *export_active_energy_sensor_{nullptr}; sensor::Sensor *import_reactive_energy_sensor_{nullptr}; diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 8a0d9674a7..4f439ac506 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_CURRENT, CONF_EXPORT_ACTIVE_ENERGY, CONF_EXPORT_REACTIVE_ENERGY, + CONF_TOTAL_POWER, CONF_FREQUENCY, CONF_ID, CONF_IMPORT_ACTIVE_ENERGY, @@ -64,13 +65,11 @@ PHASE_SENSORS = { CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( @@ -100,6 +99,12 @@ CONFIG_SCHEMA = ( accuracy_decimals=3, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_TOTAL_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_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, @@ -115,13 +120,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), } @@ -136,6 +139,10 @@ async def to_code(config): await cg.register_component(var, config) await modbus.register_modbus_device(var, config) + if CONF_TOTAL_POWER in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_POWER]) + cg.add(var.set_total_power_sensor(sens)) + if CONF_FREQUENCY in config: sens = await sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_frequency_sensor(sens)) diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index ba7a028f8e..107ed2902f 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -1,5 +1,6 @@ #include "sdp3x.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome { @@ -10,6 +11,7 @@ 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_(); } @@ -25,46 +27,69 @@ void SDP3XComponent::setup() { ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason } - delay_microseconds_accurate(20000); + 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; + } - 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; + } - uint8_t data[18]; - if (this->read(data, 18) != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Read 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 (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) { - ESP_LOGE(TAG, "CRC ID SDP3X failed!"); - this->mark_failed(); - return; - } - - if (data[3] == 0x01) { - ESP_LOGCONFIG(TAG, "SDP3X is SDP31"); - pressure_scale_factor_ = 60.0f * 100.0f; // Scale factors converted to hPa per count - } else if (data[3] == 0x02) { - ESP_LOGCONFIG(TAG, "SDP3X is SDP32"); - pressure_scale_factor_ = 240.0f * 100.0f; - } - - if (this->write(SDP3X_START_DP_AVG, 2) != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Start Measurements SDP3X failed!"); - this->mark_failed(); - return; - } - ESP_LOGCONFIG(TAG, "SDP3X started!"); + 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); @@ -90,8 +115,12 @@ void SDP3XComponent::read_pressure_() { } int16_t pressure_raw = encode_uint16(data[0], data[1]); - float pressure = pressure_raw / pressure_scale_factor_; - ESP_LOGV(TAG, "Got raw pressure=%d, scale factor =%.3f ", pressure_raw, pressure_scale_factor_); + 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); diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index 51c9973c61..0e74d0883d 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -7,6 +7,8 @@ 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. @@ -16,14 +18,14 @@ class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public se 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); - - float pressure_scale_factor_ = 0.0f; // hPa per count + MeasurementMode measurement_mode_; }; } // namespace sdp3x diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 08d7250f6e..45f5cc4d9a 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -14,6 +14,14 @@ 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, @@ -24,6 +32,9 @@ CONFIG_SCHEMA = ( .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")) @@ -36,3 +47,4 @@ async def to_code(config): 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/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 168d3a3db2..e698255c25 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -71,25 +71,21 @@ SENSORS = { CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_APPARENT_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_ACTIVE_POWER: sensor.sensor_schema( @@ -101,13 +97,11 @@ SENSORS = { CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE: sensor.sensor_schema( @@ -142,13 +136,11 @@ SENSORS = { CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c156a63a86..c15036e9f9 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -30,8 +30,7 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) icon = cv.icon - -SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( +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), @@ -90,6 +89,6 @@ async def to_code(config): async def select_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_OPTION], args, str) + template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string) cg.add(var.set_option(template_)) return var diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 610892dd9e..50b9e01f17 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -141,12 +141,16 @@ void SenseAirComponent::abc_get_period() { } 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, response_length); this->flush(); return ret; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 4b2e9dc019..14a15da2f1 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, + CONF_ENTITY_CATEGORY, CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, @@ -18,6 +19,7 @@ from esphome.const import ( CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, + CONF_QUANTILE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_STATE_CLASS, @@ -133,7 +135,6 @@ def validate_datapoint(value): # Base -sensor_ns = cg.esphome_ns.namespace("sensor") Sensor = sensor_ns.class_("Sensor", cg.EntityBase) SensorPtr = Sensor.operator("ptr") @@ -151,6 +152,7 @@ SensorPublishAction = sensor_ns.class_("SensorPublishAction", automation.Action) # Filters 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) @@ -226,6 +228,7 @@ def sensor_schema( 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: @@ -258,6 +261,14 @@ def sensor_schema( 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 @@ -276,6 +287,30 @@ async def filter_out_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) +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("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], + ) + + MEDIAN_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 321e3a4a4f..7a8a557273 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,7 +1,8 @@ #include "filter.h" -#include "sensor.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "sensor.h" +#include namespace esphome { namespace sensor { @@ -66,6 +67,41 @@ optional MedianFilter::new_value(float value) { return {}; } +// 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) {} diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index d595e419a6..0ed7ce4801 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -42,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 . diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 793ae170c3..73730f6482 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -24,7 +24,10 @@ Sensor::Sensor() : Sensor("") {} std::string Sensor::get_unit_of_measurement() { if (this->unit_of_measurement_.has_value()) return *this->unit_of_measurement_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->unit_of_measurement(); +#pragma GCC diagnostic pop } void Sensor::set_unit_of_measurement(const std::string &unit_of_measurement) { this->unit_of_measurement_ = unit_of_measurement; @@ -34,7 +37,10 @@ std::string Sensor::unit_of_measurement() { return ""; } int8_t Sensor::get_accuracy_decimals() { if (this->accuracy_decimals_.has_value()) return *this->accuracy_decimals_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->accuracy_decimals(); +#pragma GCC diagnostic pop } void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } int8_t Sensor::accuracy_decimals() { return 0; } @@ -42,7 +48,10 @@ int8_t Sensor::accuracy_decimals() { return 0; } std::string Sensor::get_device_class() { if (this->device_class_.has_value()) return *this->device_class_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->device_class(); +#pragma GCC diagnostic pop } void Sensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } std::string Sensor::device_class() { return ""; } @@ -51,7 +60,10 @@ void Sensor::set_state_class(StateClass state_class) { this->state_class_ = stat StateClass Sensor::get_state_class() { if (this->state_class_.has_value()) return *this->state_class_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->state_class(); +#pragma GCC diagnostic pop } StateClass Sensor::state_class() { return StateClass::STATE_CLASS_NONE; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 6cab46f7f9..d31fe9d834 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -150,16 +150,28 @@ class Sensor : public EntityBase { void internal_send_state_to_frontend(float state); protected: - /// Override this to set the default unit of measurement. + /** Override this to set the default unit of measurement. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string unit_of_measurement(); // NOLINT - /// Override this to set the default accuracy in decimals. + /** Override this to set the default accuracy in decimals. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual int8_t accuracy_decimals(); // NOLINT - /// Override this to set the default device class. + /** Override this to set the default device class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string device_class(); // NOLINT - /// Override this to set the default state class. + /** Override this to set the default state class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual StateClass state_class(); // NOLINT uint32_t hash_base() override; diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 1a64a12907..4157fd55cf 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,4 +1,5 @@ #include "sgp30.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" #include @@ -146,8 +147,8 @@ void SGP30Component::read_iaq_baseline_() { // much if (this->store_baseline_ && (this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL || - abs(this->baselines_storage_.eco2 - this->eco2_baseline_) > MAXIMUM_STORAGE_DIFF || - abs(this->baselines_storage_.tvoc - this->tvoc_baseline_) > MAXIMUM_STORAGE_DIFF)) { + (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_; diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.cpp b/esphome/components/sgp40/sensirion_voc_algorithm.cpp index f3cdeee35b..d76b776641 100644 --- a/esphome/components/sgp40/sensirion_voc_algorithm.cpp +++ b/esphome/components/sgp40/sensirion_voc_algorithm.cpp @@ -149,7 +149,7 @@ static fix16_t fix16_div(fix16_t a, fix16_t b) { /* Figure out the sign of result */ if ((a ^ b) & 0x80000000) { #ifndef FIXMATH_NO_OVERFLOW - if (result == FIX16_MINIMUM) + if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare) return FIX16_OVERFLOW; #endif diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index a3d2c74eb7..da6659c90f 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -144,8 +144,8 @@ int32_t SGP40Component::measure_voc_index_() { // 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 (abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF || - abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) { + 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_; @@ -211,7 +211,7 @@ uint16_t SGP40Component::measure_raw_() { ESP_LOGD(TAG, "write error"); return UINT16_MAX; } - delay(250); // NOLINT + delay(30); uint16_t raw_data[1]; if (!this->read_data_(raw_data, 1)) { diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h index bb68a1ffcf..c854b21060 100644 --- a/esphome/components/sgp40/sgp40.h +++ b/esphome/components/sgp40/sgp40.h @@ -66,7 +66,7 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2 uint8_t generate_crc_(const uint8_t *data, uint8_t datalen); uint16_t measure_raw_(); ESPPreferenceObject pref_; - int32_t seconds_since_last_store_; + uint32_t seconds_since_last_store_; SGP40Baselines baselines_storage_; VocAlgorithmParams voc_algorithm_params_; bool self_test_complete_; diff --git a/esphome/components/shutdown/__init__.py b/esphome/components/shutdown/__init__.py index f70ffa9520..480a6f3e31 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@esphome/core"] +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/__init__.py similarity index 73% rename from esphome/components/shutdown/switch.py rename to esphome/components/shutdown/switch/__init__.py index 30c2bc2b74..49970b4c2f 100644 --- a/esphome/components/shutdown/switch.py +++ b/esphome/components/shutdown/switch/__init__.py @@ -1,7 +1,14 @@ 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 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) @@ -13,6 +20,9 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( "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) diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/switch/shutdown_switch.cpp similarity index 100% rename from esphome/components/shutdown/shutdown_switch.cpp rename to esphome/components/shutdown/switch/shutdown_switch.cpp 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 0887b8640f..4143627084 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All( .extend(uart.UART_DEVICE_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "sim800l", baud_rate=9600, require_tx=True, require_rx=True + "sim800l", require_tx=True, require_rx=True ) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index e48b1ac9bd..eb6d62ca33 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -128,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_); } } @@ -146,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 diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py index 4f44582eba..0ce1c9f9e2 100644 --- a/esphome/components/slow_pwm/output.py +++ b/esphome/components/slow_pwm/output.py @@ -2,15 +2,37 @@ from esphome import pins, core from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_PIN, CONF_PERIOD +from esphome import automation +from esphome.const import ( + CONF_ID, + CONF_PIN, + CONF_PERIOD, + CONF_TURN_ON_ACTION, + CONF_TURN_OFF_ACTION, +) slow_pwm_ns = cg.esphome_ns.namespace("slow_pwm") SlowPWMOutput = slow_pwm_ns.class_("SlowPWMOutput", output.FloatOutput, cg.Component) +CONF_STATE_CHANGE_ACTION = "state_change_action" + CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(SlowPWMOutput), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, + cv.Inclusive( + CONF_TURN_ON_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Inclusive( + CONF_TURN_OFF_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation( + single=True + ), cv.Required(CONF_PERIOD): cv.All( cv.positive_time_period_milliseconds, cv.Range(min=core.TimePeriod(milliseconds=100)), @@ -23,7 +45,21 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) + if CONF_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + if CONF_STATE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_state_change_trigger(), + [(bool, "state")], + config[CONF_STATE_CHANGE_ACTION], + ) + if CONF_TURN_ON_ACTION in config: + await automation.build_automation( + var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] + ) + await automation.build_automation( + var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] + ) - 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 9b2589e735..573adbe3dc 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -7,34 +7,59 @@ namespace slow_pwm { static const char *const TAG = "output.slow_pwm"; void SlowPWMOutput::setup() { - this->pin_->setup(); + if (this->pin_) + this->pin_->setup(); this->turn_off(); } +/// turn on/off the configured output +void SlowPWMOutput::set_output_state_(bool new_state) { + if (this->pin_) { + this->pin_->digital_write(new_state); + } + if (new_state != current_state_) { + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(new_state); + } + if (new_state) { + if (this->turn_on_trigger_) + this->turn_on_trigger_->trigger(); + } else { + if (this->turn_off_trigger_) + this->turn_off_trigger_->trigger(); + } + current_state_ = new_state; + } +} + void SlowPWMOutput::loop() { uint32_t now = millis(); float scaled_state = this->state_ * this->period_; - if (now - this->period_start_time_ > this->period_) { + if (now - this->period_start_time_ >= this->period_) { ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state); this->period_start_time_ += this->period_; } if (scaled_state > now - this->period_start_time_) { - this->pin_->digital_write(true); + this->set_output_state_(true); } else { - this->pin_->digital_write(false); + this->set_output_state_(false); } } void SlowPWMOutput::dump_config() { ESP_LOGCONFIG(TAG, "Slow PWM Output:"); LOG_PIN(" Pin: ", this->pin_); + if (this->state_change_trigger_) + ESP_LOGCONFIG(TAG, " State change automation configured"); + if (this->turn_on_trigger_) + ESP_LOGCONFIG(TAG, " Turn on automation configured"); + if (this->turn_off_trigger_) + ESP_LOGCONFIG(TAG, " Turn off automation configured"); ESP_LOGCONFIG(TAG, " Period: %d ms", this->period_); LOG_FLOAT_OUTPUT(this); } -void SlowPWMOutput::write_state(float state) { this->state_ = state; } - } // namespace slow_pwm } // namespace esphome diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index f0524f36d8..d5c5883f25 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -1,5 +1,5 @@ #pragma once - +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" @@ -11,19 +11,42 @@ class SlowPWMOutput : public output::FloatOutput, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; }; void set_period(unsigned int period) { period_ = period; }; - /// 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; - void loop() override; + Trigger<> *get_turn_on_trigger() { + // Lazy create + if (!this->turn_on_trigger_) + this->turn_on_trigger_ = make_unique>(); + return this->turn_on_trigger_.get(); + } + Trigger<> *get_turn_off_trigger() { + if (!this->turn_off_trigger_) + this->turn_off_trigger_ = make_unique>(); + return this->turn_off_trigger_.get(); + } - GPIOPin *pin_; + Trigger *get_state_change_trigger() { + if (!this->state_change_trigger_) + this->state_change_trigger_ = make_unique>(); + return this->state_change_trigger_.get(); + } + + protected: + void loop() override; + void write_state(float state) override { state_ = state; } + /// turn on/off the configured output + void set_output_state_(bool state); + + GPIOPin *pin_{nullptr}; + std::unique_ptr> turn_on_trigger_{nullptr}; + std::unique_ptr> turn_off_trigger_{nullptr}; + std::unique_ptr> state_change_trigger_{nullptr}; float state_{0}; + bool current_state_{false}; unsigned int period_start_time_{0}; unsigned int period_{5000}; }; diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp index e41a4855db..c726faec48 100644 --- a/esphome/components/sm300d2/sm300d2.cpp +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -50,10 +50,10 @@ void SM300D2Sensor::update() { 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.1)) > 128) - ? (((response[12] + (response[13] * 0.1)) - 128) * -1) - : response[12] + (response[13] * 0.1); - const float humidity = response[14] + (response[15] * 0.1); + 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) diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 0d1ff6ecba..630abc8bca 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -60,7 +60,7 @@ 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=7), + 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( diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 2b6cd10e80..21fcb96842 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -3,6 +3,9 @@ #ifdef USE_ESP32 #include "lwip/apps/sntp.h" +#ifdef USE_ESP_IDF +#include "esp_sntp.h" +#endif #endif #ifdef USE_ESP8266 #include "sntp.h" @@ -37,6 +40,9 @@ void SNTPComponent::setup() { if (!this->server_3_.empty()) { sntp_setservername(2, strdup(this->server_3_.c_str())); } +#ifdef USE_ESP_IDF + sntp_set_sync_interval(this->get_update_interval()); +#endif sntp_init(); } @@ -47,7 +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() {} +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; @@ -56,7 +71,7 @@ void SNTPComponent::loop() { if (!time.is_valid()) return; - ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + 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/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 54dfddac3f..d57413c739 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -320,8 +320,7 @@ class LWIPRawImpl : public Socket { return -1; } if (rx_closed_ && rx_buf_ == nullptr) { - errno = ECONNRESET; - return -1; + return 0; } if (len == 0) { return 0; @@ -366,6 +365,11 @@ class LWIPRawImpl : public Socket { read += copysize; } + if (read == 0) { + errno = EWOULDBLOCK; + return -1; + } + return read; } ssize_t readv(const struct iovec *iov, int iovcnt) override { @@ -379,7 +383,7 @@ class LWIPRawImpl : public Socket { return err; } ret += err; - if (err != iov[i].iov_len) + if ((size_t) err != iov[i].iov_len) break; } return ret; @@ -458,7 +462,7 @@ class LWIPRawImpl : public Socket { return err; } written += err; - if (err != iov[i].iov_len) + if ((size_t) err != iov[i].iov_len) break; } if (written == 0) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 3a96cce99b..c917fe1ad8 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -46,7 +46,9 @@ async def to_code(config): mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) - if CORE.is_esp32: + if CORE.is_esp32 and CORE.using_arduino: + cg.add_library("SPI", None) + if CORE.is_esp8266: cg.add_library("SPI", None) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index d883142c81..d427e2c91b 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -55,13 +55,9 @@ void SPIComponent::setup() { } } #ifdef USE_ESP8266 - if (clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) { - // pass - } else if (clk_pin == 14 && (!has_miso || miso_pin == 12) && (!has_mosi || mosi_pin == 13)) { - // pass - } else { + 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; diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 601a5c5a7e..6c3fd17e56 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -195,7 +195,14 @@ class SPIComponent : public Component { void enable(GPIOPin *cs) { #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 { diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 472b7606ed..6160120564 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -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 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 bc2e558f1b..f2e4ef5811 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -26,10 +26,12 @@ MODELS = { "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, + "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } @@ -60,8 +62,8 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, cv.Optional(CONF_FLIP_X, default=True): cv.boolean, cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, - cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=15), - cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=15), + cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) @@ -84,7 +86,7 @@ async def setup_ssd1306(var, config): if CONF_FLIP_X in config: cg.add(var.init_flip_x(config[CONF_FLIP_X])) if CONF_FLIP_Y in config: - cg.add(var.init_flip_y(config[CONF_FLIP_X])) + 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: diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index b1a2538ebd..5ff220fce9 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -94,7 +94,9 @@ 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 SH1107_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: this->command(0x12); @@ -110,7 +112,14 @@ void SSD1306::setup() { // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); - this->command(0x00); + switch (this->model_) { + case SH1107_MODEL_128_64: + this->command(0x35); + break; + default: + this->command(0x00); + break; + } // Display output follow RAM (0xA4) this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); @@ -141,6 +150,7 @@ void SSD1306::display() { this->command(SSD1306_COMMAND_COLUMN_ADDRESS); switch (this->model_) { case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: this->command(0x20 + this->offset_x_); this->command(0x20 + this->offset_x_ + this->get_width_internal() - 1); break; @@ -196,7 +206,10 @@ void SSD1306::turn_off() { } int SSD1306::get_height_internal() { switch (this->model_) { + case SH1107_MODEL_128_64: + return 128; case SSD1306_MODEL_128_32: + case SSD1306_MODEL_64_32: case SH1106_MODEL_128_32: case SSD1305_MODEL_128_32: return 32; @@ -227,7 +240,9 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_96_16: return 96; case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: + case SH1107_MODEL_128_64: return 64; default: return 0; @@ -271,6 +286,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: @@ -283,10 +300,12 @@ const char *SSD1306::model_str_() { return "SH1106 96x16"; case SH1106_MODEL_64_48: return "SH1106 64x48"; + case SH1107_MODEL_128_64: + return "SH1107 128x64"; case SSD1305_MODEL_128_32: return "SSD1305 128x32"; case SSD1305_MODEL_128_64: - return "SSD1305 128x32"; + 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 09417a2c10..5ab68143c7 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -12,10 +12,12 @@ 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, + SH1107_MODEL_128_64, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, }; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index fddea25fc8..64b09c0672 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -40,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/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 33d474a8ee..7f025d77cd 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -37,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/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 8490aa1fe4..a0c2d80d16 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -265,8 +265,8 @@ void ST7735::setup() { 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 + colstart_ == 0 ? colstart_ = 24 : colstart_; + rowstart_ == 0 ? rowstart_ = 0 : rowstart_; } else { // colstart, rowstart left at default '0' values display_init_(RCMD2RED); @@ -275,7 +275,7 @@ void ST7735::setup() { uint8_t data = 0; if (this->model_ != INITR_HALLOWING) { - uint8_t data = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY; + data = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY; } if (this->usebgr_) { data = data | ST7735_MADCTL_BGR; @@ -446,7 +446,7 @@ void HOT ST7735::write_display_data_() { this->dc_pin_->digital_write(true); if (this->eightbitcolor_) { - for (int line = 0; line < this->get_buffer_length(); line = line + this->get_width_internal()) { + 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); diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index d985b0a426..63fa0ba72f 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -74,7 +74,7 @@ void ST7920::goto_xy_(uint16_t x, uint16_t y) { void HOT ST7920::write_display_data() { uint8_t i, j, b; - for (j = 0; j < this->get_height_internal() / 2; j++) { + 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+ diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index d0258d922c..5f32e7ff23 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -14,7 +14,7 @@ using st7920_writer_t = std::function; class ST7920 : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevice { + spi::DATA_RATE_200KHZ> { public: void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; } void set_height(uint16_t height) { this->height_ = height; } diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py index e462bc5385..9367706388 100644 --- a/esphome/components/status/binary_sensor.py +++ b/esphome/components/status/binary_sensor.py @@ -1,7 +1,13 @@ 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_( @@ -14,6 +20,9 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( 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) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 88341e0add..08cbccbe35 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -36,6 +36,7 @@ SwitchTurnOffTrigger = switch_ns.class_( icon = cv.icon + SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index e4d20719e1..b9b99b4147 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -34,7 +34,7 @@ void Switch::publish_state(bool state) { this->state = state != this->inverted_; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(state)); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); } bool Switch::assumed_state() { return false; } diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index f1b7d5f424..879ced2fb3 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -80,8 +80,8 @@ def validate_mode(value): CONF_SX1509 = "sx1509" SX1509_PIN_SCHEMA = cv.All( { - cv.GenerateID(): cv.declare_id(SX1509Component), - cv.Required(CONF_SX1509): cv.use_id(SX1509GPIOPin), + 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( { diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index 5117ad8969..f3f8685287 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -22,7 +22,7 @@ i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffer void TCA9548AComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); uint8_t status = 0; - if (!this->read_register(0x00, &status, 1)) { + if (this->read(&status, 1) != i2c::ERROR_OK) { ESP_LOGI(TAG, "TCA9548A failed"); this->mark_failed(); return; diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 314346d317..39d07c2eb4 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -24,6 +24,7 @@ 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); diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index 6c74c86faf..fcc56e395f 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_GAIN, CONF_ID, CONF_ILLUMINANCE, + CONF_GLASS_ATTENUATION_FACTOR, CONF_INTEGRATION_TIME, DEVICE_CLASS_ILLUMINANCE, ICON_LIGHTBULB, @@ -34,8 +35,20 @@ TCS34725_INTEGRATION_TIMES = { "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, - "700ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_700MS, + "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") @@ -79,6 +92,9 @@ CONFIG_SCHEMA = ( 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")) @@ -93,6 +109,7 @@ async def to_code(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 = await sensor.new_sensor(config[CONF_RED_CHANNEL]) diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 564d3dcda7..f7ffe2a97d 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -26,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; } @@ -61,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; @@ -74,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; @@ -87,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/sensor/teleinfo_sensor.cpp b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp index 661c149c09..ad9c6dae00 100644 --- a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp @@ -6,9 +6,9 @@ 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_float(val); - publish_state(*newval); + auto newval = parse_number(val).value_or(0.0f); + publish_state(newval); } -void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", tag.c_str(), this); } +void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", "Teleinfo Sensor", this); } } // namespace teleinfo } // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index badd66ae83..d9f80134f4 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -118,7 +118,7 @@ void TeleInfo::loop() { * */ while ((buf_finger = static_cast(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) && - ((buf_finger - buf_) < buf_index_)) { + ((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 @@ -141,21 +141,22 @@ void TeleInfo::loop() { field_len = get_field(tag_, buf_finger, grp_end, separator_, MAX_TAG_SIZE); if (!field_len || field_len >= MAX_TAG_SIZE) { ESP_LOGE(TAG, "Invalid tag."); - break; + 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", - * it means there is a timestamp to read first. + * 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) { + 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"); - break; + ESP_LOGE(TAG, "Invalid timestamp for tag %s", timestamp_); + continue; } /* Advance buf_finger to after the first data and the separator. */ @@ -164,8 +165,8 @@ void TeleInfo::loop() { field_len = get_field(val_, buf_finger, grp_end, separator_, MAX_VAL_SIZE); if (!field_len || field_len >= MAX_VAL_SIZE) { - ESP_LOGE(TAG, "Invalid Value"); - break; + ESP_LOGE(TAG, "Invalid value for tag %s", tag_); + continue; } /* Advance buf_finger to end of group */ diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp index 1adbd9ce13..87cf0dea17 100644 --- a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp @@ -6,6 +6,6 @@ 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(" ", tag.c_str(), this); } +void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Teleinfo Text Sensor", this); } } // namespace teleinfo } // namespace esphome 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/number/__init__.py b/esphome/components/template/number/__init__.py index 887f6b15ad..3dec7066d3 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -35,6 +35,9 @@ def validate(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." @@ -80,8 +83,7 @@ async def to_code(config): else: cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - if CONF_INITIAL_VALUE in config: - cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + 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])) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 5c739e1d0a..e0fc6af19c 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -49,6 +49,7 @@ 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) @@ -79,26 +80,21 @@ async def prepend_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) -def validate_substitute(value): - if isinstance(value, dict): - return cv.Schema( - { - cv.Required(CONF_FROM): cv.string, - cv.Required(CONF_TO): cv.string, - } - )(value) - value = cv.string(value) - if "->" not in value: - raise cv.Invalid("Substitute mapping must contain '->'") - a, b = value.split("->", 1) - a, b = a.strip(), b.strip() - return validate_substitute({CONF_FROM: cv.string(a), CONF_TO: cv.string(b)}) +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.All(cv.ensure_list(validate_substitute), cv.Length(min=2)), + "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] @@ -106,8 +102,17 @@ async def substitute_filter_to_code(config, filter_id): 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]) + ) + + icon = cv.icon + TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 14df6238ff..c6cbcd7c1e 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -64,11 +64,17 @@ optional PrependFilter::new_value(std::string value) { return this- // Substitute optional SubstituteFilter::new_value(std::string value) { std::size_t pos; - for (int i = 0; i < this->from_strings_.size(); i++) + 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 index 6a1d9ab04e..38f35e6172 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -4,6 +4,7 @@ #include "esphome/core/helpers.h" #include #include +#include namespace esphome { namespace text_sensor { @@ -108,5 +109,15 @@ class SubstituteFilter : public Filter { 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 0bcab90843..5d47e7465a 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -62,7 +62,7 @@ void TextSensor::add_on_raw_state_callback(std::function call std::string TextSensor::get_state() const { return this->state; } std::string TextSensor::get_raw_state() const { return this->raw_state; } void TextSensor::internal_send_state_to_frontend(const std::string &state) { - this->state = this->raw_state; + this->state = state; this->has_state_ = true; ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 7b5ee7c624..20565e811c 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -431,7 +431,8 @@ async def to_code(config): heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config + 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]) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 5c2155d764..2d73d0aef9 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,7 +1,6 @@ import logging from importlib import resources from typing import Optional -from datetime import timezone import tzlocal @@ -66,14 +65,11 @@ def _extract_tz_string(tzfile: bytes) -> str: def detect_tz() -> str: - localzone = tzlocal.get_localzone() - if localzone is timezone.utc: - return "UTC0" - if not hasattr(localzone, "key"): + iana_key = tzlocal.get_localzone_name() + if iana_key is None: raise cv.Invalid( "Could not automatically determine timezone, please set timezone manually." ) - iana_key = localzone.key _LOGGER.info("Detected timezone '%s'", iana_key) tzfile = _load_tzdata(iana_key) if tzfile is None: diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 064e6f899c..6f6739d293 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -35,7 +35,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { } auto time = this->now(); - ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + 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(); diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 59fb9f98ed..bd62f8de6d 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -1,6 +1,7 @@ #include "tlc59208f_output.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace tlc59208f { diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index 7999029f5a..609c62fd10 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -8,6 +8,8 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_INTENSITY, + CONF_INVERTED, + CONF_LENGTH, ) CODEOWNERS = ["@glmnet"] @@ -22,6 +24,8 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( 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, } @@ -39,6 +43,8 @@ async def to_code(config): 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_ = await cg.process_lambda( diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 488f3b6727..a21d2d438d 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -130,7 +130,9 @@ 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); @@ -173,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_(); @@ -241,14 +249,27 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *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--; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 63b30ac13e..9b2f014ff9 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.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,6 +64,8 @@ class TM1637Display : public PollingComponent { GPIOPin *dio_pin_; GPIOPin *clk_pin_; uint8_t intensity_; + uint8_t length_; + bool inverted_; optional writer_{}; uint8_t buffer_[6] = {0}; }; diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp index 4ba591f9c4..5cd086938e 100644 --- a/esphome/components/tof10120/tof10120_sensor.cpp +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -50,7 +50,7 @@ void TOF10120Sensor::update() { ESP_LOGW(TAG, "Distance measurement out of range"); this->publish_state(NAN); } else { - this->publish_state(distance_mm / 1000.0); + this->publish_state(distance_mm / 1000.0f); } this->status_clear_warning(); } diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 25528abbe1..975a149b52 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -580,13 +580,13 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { temperature_code = (message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG); if (message[15] & RAC_PT1411HWRU_FLAG_FAH) { - for (uint8_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_F.size(); i++) { + 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 (uint8_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_C.size(); i++) { + 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; } diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 6a8a416b81..1af8db8332 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -4,10 +4,13 @@ from esphome.components import sensor, time from esphome.const import ( CONF_ICON, CONF_ID, + CONF_RESTORE, CONF_TIME_ID, DEVICE_CLASS_ENERGY, CONF_METHOD, STATE_CLASS_TOTAL_INCREASING, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, ) from esphome.core.entity_helpers import inherit_property_from @@ -26,6 +29,15 @@ TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component ) + +def inherit_unit_of_measurement(uom, config): + return uom + "h" + + +def inherit_accuracy_decimals(decimals, config): + return decimals + 2 + + CONFIG_SCHEMA = ( sensor.sensor_schema( device_class=DEVICE_CLASS_ENERGY, @@ -36,6 +48,7 @@ CONFIG_SCHEMA = ( 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, @@ -52,11 +65,19 @@ FINAL_VALIDATE_SCHEMA = cv.All( { cv.Required(CONF_ID): cv.use_id(TotalDailyEnergy), cv.Optional(CONF_ICON): cv.icon, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): sensor.validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): sensor.validate_accuracy_decimals, cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), }, extra=cv.ALLOW_EXTRA, ), inherit_property_from(CONF_ICON, CONF_POWER_ID), + inherit_property_from( + CONF_UNIT_OF_MEASUREMENT, CONF_POWER_ID, transform=inherit_unit_of_measurement + ), + inherit_property_from( + CONF_ACCURACY_DECIMALS, CONF_POWER_ID, transform=inherit_accuracy_decimals + ), ) @@ -70,5 +91,6 @@ async def to_code(config): cg.add(var.set_parent(sens)) time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) + cg.add(var.set_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 178dc7cbe0..3746301715 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -7,14 +7,14 @@ namespace total_daily_energy { static const char *const TAG = "total_daily_energy"; void TotalDailyEnergy::setup() { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + 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_; diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index fedceafbd3..a35edfd11b 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -17,6 +17,7 @@ enum TotalDailyEnergyMethod { 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; } @@ -24,8 +25,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { 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"; } - int8_t accuracy_decimals() override { return this->parent_->get_accuracy_decimals() + 2; } void loop() override; void publish_state_and_save(float state); @@ -41,6 +40,7 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { 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}; }; diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.cpp b/esphome/components/ttp229_lsf/ttp229_lsf.cpp index 21c7b02740..773d51b76e 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.cpp +++ b/esphome/components/ttp229_lsf/ttp229_lsf.cpp @@ -35,7 +35,7 @@ void TTP229LSFComponent::loop() { } touched = i2c::i2ctohs(touched); this->status_clear_warning(); - touched = reverse_bits_16(touched); + touched = reverse_bits(touched); for (auto *channel : this->channels_) { channel->process(touched); } diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 436759979a..965893e012 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -1,16 +1,84 @@ 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, CONF_TIME_ID +from esphome.const import CONF_ID, CONF_TIME_ID, CONF_TRIGGER_ID, CONF_SENSOR_DATAPOINT DEPENDENCIES = ["uart"] CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints" +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 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( @@ -20,6 +88,18 @@ CONFIG_SCHEMA = ( 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) @@ -37,3 +117,10 @@ async def to_code(config): 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 index 65f13ea422..cd4a2db89f 100644 --- a/esphome/components/tuya/binary_sensor/__init__.py +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -1,14 +1,12 @@ from esphome.components import binary_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] CODEOWNERS = ["@jesserockz"] -CONF_SENSOR_DATAPOINT = "sensor_datapoint" - TuyaBinarySensor = tuya_ns.class_( "TuyaBinarySensor", binary_sensor.BinarySensor, cg.Component ) diff --git a/esphome/components/tuya/cover/__init__.py b/esphome/components/tuya/cover/__init__.py index 5a654841f7..f886c7030f 100644 --- a/esphome/components/tuya/cover/__init__.py +++ b/esphome/components/tuya/cover/__init__.py @@ -5,16 +5,27 @@ 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]: @@ -29,10 +40,16 @@ CONFIG_SCHEMA = cv.All( { 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, @@ -44,9 +61,16 @@ async def to_code(config): 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 index 7da1312938..b63eb9109d 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -4,48 +4,122 @@ 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_; - if (this->position_id_.has_value()) { - this->parent_->register_listener(*this->position_id_, [this](const TuyaDatapoint &datapoint) { - auto pos = float(datapoint.value_uint - this->min_value_) / this->value_range_; - if (this->invert_position_) - pos = 1.0f - pos; - this->position = pos; - this->publish_state(); - }); + + 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()) { - auto pos = this->position; - if (this->invert_position_) + 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_; + 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); + parent_->set_integer_datapoint_value(*this->position_id_, position_int); + } } if (call.get_position().has_value()) { auto pos = *call.get_position(); - if (this->invert_position_) + 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_; + 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); + 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() { diff --git a/esphome/components/tuya/cover/tuya_cover.h b/esphome/components/tuya/cover/tuya_cover.h index c3b0c3e069..87c72b0e66 100644 --- a/esphome/components/tuya/cover/tuya_cover.h +++ b/esphome/components/tuya/cover/tuya_cover.h @@ -7,22 +7,37 @@ 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_position_id(uint8_t dimmer_id) { this->position_id_ = dimmer_id; } + 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_; diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 133ee1e557..ecd3802839 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -37,9 +37,9 @@ void TuyaLight::setup() { } if (rgb_id_.has_value()) { this->parent_->register_listener(*this->rgb_id_, [this](const TuyaDatapoint &datapoint) { - auto red = parse_hex(datapoint.value_string, 0, 2); - auto green = parse_hex(datapoint.value_string, 2, 2); - auto blue = parse_hex(datapoint.value_string, 4, 2); + 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); @@ -48,9 +48,9 @@ void TuyaLight::setup() { }); } else if (hsv_id_.has_value()) { this->parent_->register_listener(*this->hsv_id_, [this](const TuyaDatapoint &datapoint) { - auto hue = parse_hex(datapoint.value_string, 0, 4); - auto saturation = parse_hex(datapoint.value_string, 4, 4); - auto value = parse_hex(datapoint.value_string, 8, 4); + 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); 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 index d87a2e7ce4..441400fa43 100644 --- a/esphome/components/tuya/sensor/__init__.py +++ b/esphome/components/tuya/sensor/__init__.py @@ -1,14 +1,12 @@ from esphome.components import sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] CODEOWNERS = ["@jesserockz"] -CONF_SENSOR_DATAPOINT = "sensor_datapoint" - TuyaSensor = tuya_ns.class_("TuyaSensor", sensor.Sensor, cg.Component) CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( 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 4f65fa7118..7ff8c66c44 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -34,7 +34,7 @@ void Tuya::dump_config() { } for (auto &info : this->datapoints_) { if (info.type == TuyaDatapointType::RAW) - ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, hexencode(info.value_raw).c_str()); + 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) @@ -104,7 +104,7 @@ 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, - hexencode(message_data, length).c_str(), static_cast(this->init_state_)); + 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 @@ -143,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; @@ -195,6 +195,7 @@ 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; @@ -252,7 +253,7 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t 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, hexencode(datapoint.value_raw).c_str()); + 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) { @@ -339,8 +340,6 @@ void Tuya::send_raw_command_(TuyaCommand command) { this->expected_response_ = TuyaCommandType::CONF_QUERY; break; case TuyaCommandType::DATAPOINT_DELIVER: - this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; - break; case TuyaCommandType::DATAPOINT_QUERY: this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; break; @@ -349,7 +348,7 @@ void Tuya::send_raw_command_(TuyaCommand command) { } ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), - version, hexencode(command.payload).c_str(), static_cast(this->init_state_)); + version, format_hex_pretty(command.payload).c_str(), static_cast(this->init_state_)); this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo}); if (!command.payload.empty()) @@ -441,53 +440,51 @@ void Tuya::send_local_time_() { #endif void Tuya::set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value) { - ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, hexencode(value).c_str()); - optional datapoint = this->get_datapoint_(datapoint_id); - if (!datapoint.has_value()) { - ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); - } else if (datapoint->type != TuyaDatapointType::RAW) { - ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); - return; - } else if (datapoint->value_raw == value) { - ESP_LOGV(TAG, "Not sending unchanged value"); - return; - } - this->send_datapoint_command_(datapoint_id, TuyaDatapointType::RAW, value); + 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); + 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); + 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) { - ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str()); - optional datapoint = this->get_datapoint_(datapoint_id); - if (!datapoint.has_value()) { - ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); - } else if (datapoint->type != TuyaDatapointType::STRING) { - ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); - return; - } else if (datapoint->value_string == value) { - ESP_LOGV(TAG, "Not sending unchanged value"); - return; - } - std::vector data; - for (char const &c : value) { - data.push_back(c); - } - this->send_datapoint_command_(datapoint_id, TuyaDatapointType::STRING, data); + 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); + 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); + 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) { @@ -498,7 +495,7 @@ optional Tuya::get_datapoint_(uint8_t datapoint_id) { } void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value, - uint8_t length) { + 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()) { @@ -506,7 +503,7 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType } else if (datapoint->type != datapoint_type) { ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); return; - } else if (datapoint->value_uint == value) { + } else if (!forced && datapoint->value_uint == value) { ESP_LOGV(TAG, "Not sending unchanged value"); return; } @@ -528,6 +525,40 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType 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); @@ -552,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 785399502b..c46d61119e 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/uart/uart.h" #ifdef USE_TIME @@ -81,12 +82,22 @@ class Tuya : public Component, public uart::UARTDevice { 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); @@ -100,7 +111,9 @@ class Tuya : public Component, public uart::UARTDevice { 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); + 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_(); @@ -122,6 +135,7 @@ class Tuya : public Component, public uart::UARTDevice { std::vector command_queue_; optional expected_response_{}; uint8_t wifi_status_ = -1; + CallbackManager initialized_callback_{}; }; } // namespace tuya diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 6e0b6343d1..fefcc8f4d5 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -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); diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 35af3eedf7..a63b220fc7 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -3,6 +3,7 @@ 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, @@ -13,7 +14,19 @@ from esphome.const import ( 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 @@ -31,6 +44,8 @@ ESP8266UartComponent = uart_ns.class_( 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 @@ -53,6 +68,19 @@ def validate_rx_pin(value): return value +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 _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) @@ -75,6 +103,59 @@ 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( { @@ -91,12 +172,39 @@ CONFIG_SCHEMA = cv.All( 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]) @@ -115,6 +223,9 @@ async def to_code(config): 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( diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index c368f9ed6b..d41dbe26e6 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,17 +45,17 @@ class UARTDevice { // Compat APIs int read() { uint8_t data; - if (!read_byte(&data)) + if (!this->read_byte(&data)) return -1; return data; } size_t write(uint8_t data) { - write_byte(data); + this->write_byte(data); return 1; } int peek() { uint8_t data; - if (!peek_byte(&data)) + if (!this->peek_byte(&data)) return -1; return data; } diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index de85cd2ca3..42702cf5b8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -2,9 +2,13 @@ #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 { @@ -15,6 +19,14 @@ enum UARTParityOptions { 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 { @@ -40,6 +52,7 @@ class UARTComponent { 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_; } @@ -50,6 +63,12 @@ class UARTComponent { 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); @@ -61,6 +80,9 @@ class UARTComponent { uint8_t stop_bits_; uint8_t data_bits_; UARTParityOptions parity_; +#ifdef USE_UART_DEBUGGER + CallbackManager debug_callback_{}; +#endif }; } // namespace uart diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 1b1ce382f2..95cdde4a43 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -117,26 +117,32 @@ void ESP32ArduinoUARTComponent::dump_config() { 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++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[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++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[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..."); diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 973306cde2..370adad779 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -45,6 +45,11 @@ uint32_t ESP8266UartComponent::get_config() { 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; } @@ -130,9 +135,11 @@ void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) { 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++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } bool ESP8266UartComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) @@ -153,10 +160,11 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { 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++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } int ESP8266UartComponent::available() { @@ -206,9 +214,7 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { /* 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->read_bit_(&wait, start); - else if (arg->parity_ == UART_CONFIG_PARITY_ODD) + if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD) arg->read_bit_(&wait, start); // Stop bit diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 1cccd5821e..4d6a6af0fc 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -130,10 +130,13 @@ 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++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[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; @@ -152,6 +155,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *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)) @@ -165,12 +169,12 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } 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++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } 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/uptime/sensor.py b/esphome/components/uptime/sensor.py index 7989f3befc..16a1e4c125 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, @@ -17,6 +18,7 @@ CONFIG_SCHEMA = ( icon=ICON_TIMER, accuracy_decimals=0, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) .extend( { diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index e67f881d32..4835caf35b 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,7 +1,14 @@ 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, CONF_HIDE_TIMESTAMP +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_( @@ -13,6 +20,9 @@ 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) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 64f5597a65..1d1644dc25 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -1,6 +1,6 @@ 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, @@ -10,6 +10,7 @@ from esphome.const import ( CONF_LAMBDA, CONF_MODEL, CONF_PAGES, + CONF_RESET_DURATION, CONF_RESET_PIN, ) @@ -95,6 +96,10 @@ CONFIG_SCHEMA = cv.All( 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")) @@ -135,3 +140,5 @@ async def to_code(config): 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 92fa289cfa..322c375f0e 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -360,15 +360,18 @@ void HOT WaveshareEPaperTypeA::display() { // COMMAND DISPLAY UPDATE CONTROL 2 this->command(0x22); - if (this->model_ == WAVESHARE_EPAPER_2_9_IN_V2 || this->model_ == WAVESHARE_EPAPER_1_54_IN_V2) { - this->data(full_update ? 0xF7 : 0xFF); - } else if (this->model_ == TTGO_EPAPER_2_13_IN_B73) { - this->data(0xC7); - } else if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { - // this->data(0xC7); - this->data(full_update ? 0xF7 : 0xFF); - } else { - this->data(0xC4); + 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 @@ -381,20 +384,14 @@ void HOT WaveshareEPaperTypeA::display() { int WaveshareEPaperTypeA::get_width_internal() { switch (this->model_) { case WAVESHARE_EPAPER_1_54_IN: - return 200; 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: case TTGO_EPAPER_2_13_IN_B74: - return 128; case TTGO_EPAPER_2_13_IN_B1: - return 128; case WAVESHARE_EPAPER_2_9_IN: - return 128; case WAVESHARE_EPAPER_2_9_IN_V2: return 128; } @@ -403,20 +400,15 @@ int WaveshareEPaperTypeA::get_width_internal() { int WaveshareEPaperTypeA::get_height_internal() { switch (this->model_) { case WAVESHARE_EPAPER_1_54_IN: - return 200; 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: - return 250; case TTGO_EPAPER_2_13_IN_B1: return 250; case WAVESHARE_EPAPER_2_9_IN: - return 296; case WAVESHARE_EPAPER_2_9_IN_V2: return 296; } @@ -433,11 +425,10 @@ void WaveshareEPaperTypeA::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } -int WaveshareEPaperTypeA::idle_timeout_() { +uint32_t WaveshareEPaperTypeA::idle_timeout_() { switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: return 2500; - break; default: return WaveshareEPaper::idle_timeout_(); } @@ -646,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); @@ -825,7 +816,7 @@ void HOT WaveshareEPaper4P2InBV2::display() { // COMMAND DATA START TRANSMISSION 2 (RED data) this->command(0x13); 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(0xFF); this->end_data_(); delay(2); @@ -1183,7 +1174,7 @@ static const uint8_t PART_UPDATE_LUT_TTGO_DKE[LUT_SIZE_TTGO_DKE_PART] = { 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 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, @@ -1293,7 +1284,7 @@ void HOT WaveshareEPaper2P13InDKE::display() { int WaveshareEPaper2P13InDKE::get_width_internal() { return 128; } int WaveshareEPaper2P13InDKE::get_height_internal() { return 250; } -int WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } +uint32_t WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } void WaveshareEPaper2P13InDKE::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index b50596643d..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); @@ -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,7 +63,7 @@ class WaveshareEPaper : public PollingComponent, GPIOPin *reset_pin_{nullptr}; GPIOPin *dc_pin_; GPIOPin *busy_pin_{nullptr}; - virtual int idle_timeout_() { return 1000; } // NOLINT(readability-identifier-naming) + virtual uint32_t idle_timeout_() { return 1000u; } // NOLINT(readability-identifier-naming) }; enum WaveshareEPaperTypeAModel { @@ -110,7 +112,7 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { uint32_t full_update_every_{30}; uint32_t at_update_{0}; WaveshareEPaperTypeAModel model_; - int idle_timeout_() override; + uint32_t idle_timeout_() override; }; enum WaveshareEPaperTypeBModel { @@ -346,7 +348,7 @@ class WaveshareEPaper2P13InDKE : public WaveshareEPaper { int get_height_internal() override; - int idle_timeout_() override; + uint32_t idle_timeout_() override; uint32_t full_update_every_{30}; uint32_t at_update_{0}; diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 240ba7c8a0..62d5ec6f14 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( CONF_AUTH, CONF_USERNAME, CONF_PASSWORD, + CONF_INCLUDE_INTERNAL, + CONF_OTA, ) from esphome.core import CORE, coroutine_with_priority @@ -20,29 +22,38 @@ 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) -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 - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + 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), + cv.only_with_arduino, +) @coroutine_with_priority(40.0) @@ -52,10 +63,14 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) 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(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) @@ -69,3 +84,4 @@ async def to_code(config): 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 e99431be36..7413af67c4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -31,10 +31,10 @@ static const char *const TAG = "web_server"; void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, const std::function &action_func = nullptr) { - if (obj->is_internal()) - return; stream->print("print(klass.c_str()); + if (obj->is_internal()) + stream->print(" internal"); stream->print("\" id=\""); stream->print(klass.c_str()); stream->print("-"); @@ -83,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) { @@ -92,55 +92,55 @@ 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 (!obj->is_internal()) + 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 (!obj->is_internal()) + if (this->include_internal_ || !obj->is_internal()) client->send(this->select_json(obj, obj->state).c_str(), "state"); #endif }); @@ -152,7 +152,9 @@ 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); }); } @@ -186,64 +188,93 @@ 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()) - write_row(stream, obj, "number", ""); + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) { + number::Number *number = (number::Number *) obj; + stream.print(R"(traits.get_min_value()); + stream.print(R"(" max=")"); + stream.print(number->traits.get_max_value()); + stream.print(R"(" step=")"); + stream.print(number->traits.get_step()); + stream.print(R"(" value=")"); + stream.print(number->state); + stream.print(R"("/>)"); + }); #endif #ifdef USE_SELECT for (auto *obj : App.get_selects()) - write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { - select::Select *select = (select::Select *) obj; - stream.print(""); - }); + 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(""));
@@ -287,8 +318,6 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
 }
 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);
@@ -298,7 +327,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
   request->send(404);
 }
 std::string WebServer::sensor_json(sensor::Sensor *obj, float value) {
-  return json::build_json([obj, value](JsonObject &root) {
+  return json::build_json([obj, value](JsonObject root) {
     root["id"] = "sensor-" + obj->get_object_id();
     std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals());
     if (!obj->get_unit_of_measurement().empty())
@@ -315,8 +344,6 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s
 }
 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);
@@ -326,7 +353,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
   request->send(404);
 }
 std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) {
-  return json::build_json([obj, value](JsonObject &root) {
+  return json::build_json([obj, value](JsonObject root) {
     root["id"] = "text_sensor-" + obj->get_object_id();
     root["state"] = value;
     root["value"] = value;
@@ -339,7 +366,7 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
   this->events_.send(this->switch_json(obj, state).c_str(), "state");
 }
 std::string WebServer::switch_json(switch_::Switch *obj, bool value) {
-  return json::build_json([obj, value](JsonObject &root) {
+  return json::build_json([obj, value](JsonObject root) {
     root["id"] = "switch-" + obj->get_object_id();
     root["state"] = value ? "ON" : "OFF";
     root["value"] = value;
@@ -347,8 +374,6 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value) {
 }
 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;
 
@@ -373,14 +398,30 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
 }
 #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) {
-  return json::build_json([obj, value](JsonObject &root) {
+  return json::build_json([obj, value](JsonObject root) {
     root["id"] = "binary_sensor-" + obj->get_object_id();
     root["state"] = value ? "ON" : "OFF";
     root["value"] = value;
@@ -388,8 +429,6 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
 }
 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);
@@ -401,19 +440,17 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
 #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) {
+  return json::build_json([obj](JsonObject root) {
     root["id"] = "fan-" + obj->get_object_id();
     root["state"] = obj->state ? "ON" : "OFF";
     root["value"] = obj->state;
     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)
@@ -426,6 +463,7 @@ std::string WebServer::fan_json(fan::FanState *obj) {
           root["speed"] = "high";
           break;
       }
+#pragma GCC diagnostic pop
     }
     if (obj->get_traits().supports_oscillation())
       root["oscillation"] = obj->oscillating;
@@ -433,8 +471,6 @@ std::string WebServer::fan_json(fan::FanState *obj) {
 }
 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;
 
@@ -448,11 +484,14 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
       auto call = obj->turn_on();
       if (request->hasParam("speed")) {
         String speed = request->getParam("speed")->value();
+#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_int(speed_level.c_str());
+        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;
@@ -492,15 +531,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
 #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::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;
 
@@ -558,7 +591,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
   request->send(404);
 }
 std::string WebServer::light_json(light::LightState *obj) {
-  return json::build_json([obj](JsonObject &root) {
+  return json::build_json([obj](JsonObject root) {
     root["id"] = "light-" + obj->get_object_id();
     root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
     light::LightJSONSchema::dump_json(*obj, root);
@@ -567,15 +600,9 @@ std::string WebServer::light_json(light::LightState *obj) {
 #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::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;
 
@@ -616,7 +643,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
   request->send(404);
 }
 std::string WebServer::cover_json(cover::Cover *obj) {
-  return json::build_json([obj](JsonObject &root) {
+  return json::build_json([obj](JsonObject root) {
     root["id"] = "cover-" + obj->get_object_id();
     root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN";
     root["value"] = obj->position;
@@ -634,22 +661,40 @@ void WebServer::on_number_update(number::Number *obj, float state) {
 }
 void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (auto *obj : App.get_numbers()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
-    std::string data = this->number_json(obj, obj->state);
-    request->send(200, "text/json", data.c_str());
+
+    if (request->method() == HTTP_GET) {
+      std::string data = this->number_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("value")) {
+      String value = request->getParam("value")->value();
+      optional value_f = parse_number(value.c_str());
+      if (value_f.has_value())
+        call.set_value(*value_f);
+    }
+
+    this->defer([call]() mutable { call.perform(); });
+    request->send(200);
     return;
   }
   request->send(404);
 }
 std::string WebServer::number_json(number::Number *obj, float value) {
-  return json::build_json([obj, value](JsonObject &root) {
+  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;
+    std::string state = str_sprintf("%f", value);
+    root["state"] = state;
     root["value"] = value;
   });
 }
@@ -661,8 +706,6 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state)
 }
 void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
   for (auto *obj : App.get_selects()) {
-    if (obj->is_internal())
-      continue;
     if (obj->get_object_id() != match.id)
       continue;
 
@@ -691,7 +734,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
   request->send(404);
 }
 std::string WebServer::select_json(select::Select *obj, const std::string &value) {
-  return json::build_json([obj, value](JsonObject &root) {
+  return json::build_json([obj, value](JsonObject root) {
     root["id"] = "select-" + obj->get_object_id();
     root["state"] = value;
     root["value"] = value;
@@ -726,6 +769,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;
@@ -752,7 +800,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
 #endif
 
 #ifdef USE_NUMBER
-  if (request->method() == HTTP_GET && match.domain == "number")
+  if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "number")
     return true;
 #endif
 
@@ -798,6 +846,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);
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 021d5a0646..8edb4237a2 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -58,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.
@@ -100,6 +112,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   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;
 
@@ -182,6 +199,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   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
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
index 95d59a863e..14fb033a56 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -28,4 +28,4 @@ async def to_code(config):
         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", "1.3.0")
+    cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0")
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index 19e4046711..a24791b458 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -140,7 +140,8 @@ def final_validate(config):
     has_sta = bool(config.get(CONF_NETWORKS, True))
     has_ap = CONF_AP in config
     has_improv = "esp32_improv" in fv.full_config.get()
-    if (not has_sta) and (not has_ap) and (not has_improv):
+    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."
         )
@@ -159,8 +160,15 @@ def final_validate_power_esp32_ble(value):
         "esp32_ble_server",
         "esp32_ble_tracker",
     ]:
+        if conflicting not in fv.full_config.get():
+            continue
+
         try:
-            cv.require_framework_version(esp32_arduino=cv.Version(1, 0, 5))(None)
+            # 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:
@@ -213,10 +221,22 @@ 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
@@ -326,7 +346,8 @@ async def to_code(config):
     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]
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 703afa99bc..36944e3633 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -239,8 +239,6 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
   sta.set_ssid(ssid);
   sta.set_password(password);
   this->set_sta(sta);
-
-  this->start_scanning();
 }
 
 void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp
index 7f71b7078c..5a81fd0a39 100644
--- a/esphome/components/wifi/wifi_component_esp_idf.cpp
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -67,9 +67,9 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
   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) {
+  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) {
+  } 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));
@@ -79,13 +79,13 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
     memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t));
   } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
     memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t));
-  } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) {
+  } 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) {
+  } 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) {
+  } 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));
@@ -375,8 +375,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
         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);
+    err = esp_wifi_sta_wpa2_ent_enable();
     if (err != ESP_OK) {
       ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err);
     }
@@ -431,7 +430,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
   info.netmask.addr = static_cast(manual_ip->subnet);
 
   err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA);
-  if (err != ESP_OK) {
+  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;
   }
diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py
index 1922502204..706a8967be 100644
--- a/esphome/components/wifi_info/text_sensor.py
+++ b/esphome/components/wifi_info/text_sensor.py
@@ -3,11 +3,13 @@ import esphome.config_validation as cv
 from esphome.components import text_sensor
 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"]
@@ -32,26 +34,41 @@ 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,
             }
         ),
     }
diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py
index 37bee75928..2097c21bd7 100644
--- a/esphome/components/wifi_signal/sensor.py
+++ b/esphome/components/wifi_signal/sensor.py
@@ -4,6 +4,7 @@ from esphome.components import sensor
 from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_SIGNAL_STRENGTH,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_DECIBEL_MILLIWATT,
 )
@@ -20,6 +21,7 @@ CONFIG_SCHEMA = (
         accuracy_decimals=0,
         device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
         state_class=STATE_CLASS_MEASUREMENT,
+        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
     )
     .extend(
         {
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 884969f793..583b68a77b 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -171,10 +171,8 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service
     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
-    result.type = XiaomiParseResult::TYPE_CGG1;
-    result.name = "CGG1";
-  } else if ((raw[2] == 0x48) && (raw[3] == 0x0B)) {  // Qingping-branded, round body, e-ink display — with bindkeys
+  } 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
@@ -219,7 +217,7 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service
 bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) {
   if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) {
     ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size());
-    ESP_LOGVV(TAG, "  Packet : %s", hexencode(raw.data(), raw.size()).c_str());
+    ESP_LOGVV(TAG, "  Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
     return false;
   }
 
@@ -276,12 +274,12 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c
     memcpy(mac_address + 4, mac_reverse + 1, 1);
     memcpy(mac_address + 5, mac_reverse, 1);
     ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed.");
-    ESP_LOGVV(TAG, "  MAC address : %s", hexencode(mac_address, 6).c_str());
-    ESP_LOGVV(TAG, "       Packet : %s", hexencode(raw.data(), raw.size()).c_str());
-    ESP_LOGVV(TAG, "          Key : %s", hexencode(vector.key, vector.keysize).c_str());
-    ESP_LOGVV(TAG, "           Iv : %s", hexencode(vector.iv, vector.ivsize).c_str());
-    ESP_LOGVV(TAG, "       Cipher : %s", hexencode(vector.ciphertext, vector.datasize).c_str());
-    ESP_LOGVV(TAG, "          Tag : %s", hexencode(vector.tag, vector.tagsize).c_str());
+    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;
   }
@@ -297,7 +295,7 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c
   raw[0] &= ~0x08;
 
   ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed.");
-  ESP_LOGVV(TAG, "  Plaintext : %s, Packet : %d", hexencode(raw.data() + cipher_pos, vector.datasize).c_str(),
+  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);
diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py
index 774c87fee9..5b88121d7c 100644
--- a/esphome/components/xiaomi_cgd1/sensor.py
+++ b/esphome/components/xiaomi_cgd1/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp
index 97bbd6e6d6..baf9cb8075 100644
--- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp
+++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_cgd1";
 
 void XiaomiCGD1::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi CGD1");
-  ESP_LOGCONFIG(TAG, "  Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
+  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_);
diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py
index d4e7230fd0..ac487d87fc 100644
--- a/esphome/components/xiaomi_cgdk2/sensor.py
+++ b/esphome/components/xiaomi_cgdk2/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
index a97ca93206..c74794f4f4 100644
--- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
+++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_cgdk2";
 
 void XiaomiCGDK2::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi CGDK2");
-  ESP_LOGCONFIG(TAG, "  Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
+  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_);
diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py
index 4e606d95f8..a4f9a39aff 100644
--- a/esphome/components/xiaomi_cgg1/sensor.py
+++ b/esphome/components/xiaomi_cgg1/sensor.py
@@ -11,6 +11,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
index e1f83e4ddd..c20c7578d0 100644
--- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
+++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_cgg1";
 
 void XiaomiCGG1::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi CGG1");
-  ESP_LOGCONFIG(TAG, "  Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
+  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_);
diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py
index a7f6c41225..7f0aac873d 100644
--- a/esphome/components/xiaomi_cgpr1/binary_sensor.py
+++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py
@@ -10,6 +10,8 @@ from esphome.const import (
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_ILLUMINANCE,
+    DEVICE_CLASS_MOTION,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     ICON_EMPTY,
     UNIT_PERCENT,
     CONF_IDLE_TIME,
@@ -37,13 +39,21 @@ CONFIG_SCHEMA = cv.All(
             cv.Required(CONF_BINDKEY): cv.bind_key,
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(
-                CONF_DEVICE_CLASS, default="motion"
+                CONF_DEVICE_CLASS,
+                default=DEVICE_CLASS_MOTION,
             ): binary_sensor.device_class,
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                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_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY
+                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
diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py
index 1818731a0f..535316e246 100644
--- a/esphome/components/xiaomi_hhccjcy01/sensor.py
+++ b/esphome/components/xiaomi_hhccjcy01/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_TEMPERATURE,
     DEVICE_CLASS_ILLUMINANCE,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     ICON_WATER_PERCENT,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
@@ -63,6 +64,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py
index 40991c3d0f..f4d2b342fd 100644
--- a/esphome/components/xiaomi_jqjcy01ym/sensor.py
+++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -54,6 +55,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py
index 339c5e673a..20629a0a9c 100644
--- a/esphome/components/xiaomi_lywsd02/sensor.py
+++ b/esphome/components/xiaomi_lywsd02/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -45,6 +46,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py
index f27cee3800..b2784e58fc 100644
--- a/esphome/components/xiaomi_lywsd03mmc/sensor.py
+++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -49,6 +50,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp
index 547cc7c114..d0319c9474 100644
--- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp
+++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_lywsd03mmc";
 
 void XiaomiLYWSD03MMC::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC");
-  ESP_LOGCONFIG(TAG, "  Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
+  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_);
diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py
index 39a207327e..80f24ac0ef 100644
--- a/esphome/components/xiaomi_lywsdcgq/sensor.py
+++ b/esphome/components/xiaomi_lywsdcgq/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -45,6 +46,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py
index 57b2190150..9e92e34230 100644
--- a/esphome/components/xiaomi_mhoc401/sensor.py
+++ b/esphome/components/xiaomi_mhoc401/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -48,6 +49,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp
index 0cad5c67b2..9ec2b10e12 100644
--- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp
+++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_mhoc401";
 
 void XiaomiMHOC401::dump_config() {
   ESP_LOGCONFIG(TAG, "Xiaomi MHOC401");
-  ESP_LOGCONFIG(TAG, "  Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
+  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_);
diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
index fd4bae60c1..1bedae26cf 100644
--- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
+++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     CONF_BATTERY_LEVEL,
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_ILLUMINANCE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     STATE_CLASS_NONE,
     UNIT_PERCENT,
@@ -51,6 +52,7 @@ CONFIG_SCHEMA = cv.All(
                 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,
diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py
index d2b353beff..8667794923 100644
--- a/esphome/components/xiaomi_wx08zm/binary_sensor.py
+++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_TABLET,
     DEVICE_CLASS_BATTERY,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_BUG,
@@ -40,6 +41,7 @@ CONFIG_SCHEMA = cv.All(
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp
index 11643a5c23..621439aa0c 100644
--- a/esphome/components/zyaura/zyaura.cpp
+++ b/esphome/components/zyaura/zyaura.cpp
@@ -57,38 +57,46 @@ void IRAM_ATTR ZaSensorStore::interrupt(ZaSensorStore *arg) {
 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 (std::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 2b9e3fbb35..85c31ec75a 100644
--- a/esphome/components/zyaura/zyaura.h
+++ b/esphome/components/zyaura/zyaura.h
@@ -42,9 +42,9 @@ class ZaDataProcessor {
 
 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(InternalGPIOPin *pin_clock, InternalGPIOPin *pin_data);
   static void interrupt(ZaSensorStore *arg);
@@ -79,7 +79,7 @@ class ZyAuraSensor : public PollingComponent {
   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_validation.py b/esphome/config_validation.py
index fcec74b245..8df74ba861 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -12,12 +12,14 @@ from string import ascii_letters, digits
 import voluptuous as vol
 
 from esphome import core
+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,
@@ -35,6 +37,9 @@ from esphome.const import (
     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,
@@ -291,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):
@@ -903,21 +910,9 @@ def validate_bytes(value):
 
 def hostname(value):
     value = string(value)
-    warned_underscore = False
-    if len(value) > 63:
-        raise Invalid("Hostnames can only be 63 characters long")
-    for c in value:
-        if not (c.isalnum() or c in "-_"):
-            raise Invalid("Hostname can only have alphanumeric characters and -")
-        if c in "_" and not warned_underscore:
-            _LOGGER.warning(
-                "'%s': Using the '_' (underscore) character in the hostname is discouraged "
-                "as it can cause problems with some DHCP and local name services. "
-                "For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name",
-                value,
-            )
-            warned_underscore = True
-    return value
+    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):
@@ -1406,7 +1401,7 @@ def typed_schema(schemas, **kwargs):
         if schema_option is None:
             raise Invalid(f"{key} not specified!")
         key_v = key_validator(schema_option)
-        value = schemas[key_v](value)
+        value = Schema(schemas[key_v])(value)
         value[key] = key_v
         return value
 
@@ -1563,6 +1558,17 @@ def maybe_simple_value(*validators, **kwargs):
     return validate
 
 
+_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,
+}
+
+
+def entity_category(value):
+    return enum(_ENTITY_CATEGORIES, lower=True)(value)
+
+
 MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema(
     {
         Required(CONF_TOPIC): subscribe_topic,
@@ -1594,6 +1600,7 @@ ENTITY_BASE_SCHEMA = Schema(
         Optional(CONF_INTERNAL): boolean,
         Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
         Optional(CONF_ICON): icon,
+        Optional(CONF_ENTITY_CATEGORY): entity_category,
     }
 )
 
@@ -1680,6 +1687,25 @@ def version_number(value):
         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,
diff --git a/esphome/const.py b/esphome/const.py
index 54d677a62e..c80ee85983 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,35 +1,18 @@
 """Constants used by esphome."""
 
-__version__ = "2021.10.0-dev"
+__version__ = "2022.2.0-dev"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 
-TARGET_PLATFORMS = ["esp32", "esp8266"]
-TARGET_FRAMEWORKS = ["arduino", "esp-idf"]
+PLATFORM_ESP32 = "esp32"
+PLATFORM_ESP8266 = "esp8266"
+
+TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266]
 
-# See also https://github.com/platformio/platform-espressif8266/releases
-ARDUINO_VERSION_ESP8266 = {
-    "dev": "https://github.com/platformio/platform-espressif8266.git",
-    "3.0.1": "platformio/espressif8266@3.1.0",
-    "3.0.0": "platformio/espressif8266@3.0.0",
-    "2.7.4": "platformio/espressif8266@2.6.2",
-    "2.7.3": "platformio/espressif8266@2.6.1",
-    "2.7.2": "platformio/espressif8266@2.6.0",
-    "2.7.1": "platformio/espressif8266@2.5.3",
-    "2.7.0": "platformio/espressif8266@2.5.0",
-    "2.6.3": "platformio/espressif8266@2.4.0",
-    "2.6.2": "platformio/espressif8266@2.3.1",
-    "2.6.1": "platformio/espressif8266@2.3.0",
-    "2.5.2": "platformio/espressif8266@2.2.3",
-    "2.5.1": "platformio/espressif8266@2.1.1",
-    "2.5.0": "platformio/espressif8266@2.0.4",
-    "2.4.2": "platformio/espressif8266@1.8.0",
-    "2.4.1": "platformio/espressif8266@1.7.3",
-    "2.4.0": "platformio/espressif8266@1.6.0",
-    "2.3.0": "platformio/espressif8266@1.5.0",
-}
 SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
 HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
+SECRETS_FILES = {"secrets.yaml", "secrets.yml"}
+
 
 CONF_ABOVE = "above"
 CONF_ACCELERATION = "acceleration"
@@ -44,6 +27,7 @@ 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"
@@ -56,6 +40,7 @@ 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"
@@ -90,6 +75,7 @@ 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"
@@ -161,6 +147,7 @@ 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"
@@ -168,6 +155,7 @@ 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"
@@ -181,6 +169,7 @@ 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"
@@ -189,6 +178,8 @@ 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"
@@ -200,6 +191,7 @@ 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"
@@ -295,6 +287,7 @@ 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"
@@ -410,6 +403,7 @@ 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"
@@ -526,6 +520,7 @@ 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"
@@ -547,6 +542,7 @@ 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"
@@ -587,6 +583,7 @@ 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"
@@ -603,6 +600,7 @@ CONF_SHOW_VALUES = "show_values"
 CONF_SHUNT_RESISTANCE = "shunt_resistance"
 CONF_SHUNT_VOLTAGE = "shunt_voltage"
 CONF_SHUTDOWN_MESSAGE = "shutdown_message"
+CONF_SIGNAL_STRENGTH = "signal_strength"
 CONF_SINGLE_LIGHT_ID = "single_light_id"
 CONF_SIZE = "size"
 CONF_SLEEP_DURATION = "sleep_duration"
@@ -688,6 +686,7 @@ CONF_TOLERANCE = "tolerance"
 CONF_TOPIC = "topic"
 CONF_TOPIC_PREFIX = "topic_prefix"
 CONF_TOTAL = "total"
+CONF_TOTAL_POWER = "total_power"
 CONF_TRACES = "traces"
 CONF_TRANSITION_LENGTH = "transition_length"
 CONF_TRIGGER_ID = "trigger_id"
@@ -707,6 +706,7 @@ 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"
@@ -837,6 +837,7 @@ 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"
@@ -866,10 +867,11 @@ 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_UPDATE = "update"
+DEVICE_CLASS_TAMPER = "tamper"
 DEVICE_CLASS_VIBRATION = "vibration"
 DEVICE_CLASS_WINDOW = "window"
 # device classes of both binary_sensor and sensor component
@@ -901,6 +903,11 @@ 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 = ""
@@ -915,3 +922,12 @@ 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/__init__.py b/esphome/core/__init__.py
index 8bdef3a4ea..addecf1326 100644
--- a/esphome/core/__init__.py
+++ b/esphome/core/__init__.py
@@ -10,6 +10,7 @@ from esphome.const import (
     CONF_USE_ADDRESS,
     CONF_ETHERNET,
     CONF_WIFI,
+    CONF_PORT,
     KEY_CORE,
     KEY_TARGET_FRAMEWORK,
     KEY_TARGET_PLATFORM,
@@ -519,6 +520,19 @@ class EsphomeCore:
 
         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:
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index a4d61f819c..a423397453 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -37,6 +37,7 @@ void Application::setup() {
 
     component->call();
     this->scheduler.process_to_add();
+    this->feed_wdt();
     if (component->can_proceed())
       continue;
 
@@ -46,14 +47,15 @@ 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();
-      this->feed_wdt();
     } while (!component->can_proceed());
   }
 
@@ -65,6 +67,7 @@ void Application::loop() {
   uint32_t new_app_state = 0;
 
   this->scheduler.call();
+  this->feed_wdt();
   for (Component *component : this->looping_components_) {
     {
       WarnIfComponentBlockingGuard guard{component};
@@ -94,7 +97,7 @@ 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
@@ -109,8 +112,8 @@ void Application::loop() {
 
 void IRAM_ATTR HOT Application::feed_wdt() {
   static uint32_t last_feed = 0;
-  uint32_t now = millis();
-  if (now - last_feed > 3) {
+  uint32_t now = micros();
+  if (now - last_feed > 3000) {
     arch_feed_wdt();
     last_feed = now;
 #ifdef USE_STATUS_LED
diff --git a/esphome/core/application.h b/esphome/core/application.h
index 5c1483d301..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
@@ -44,6 +48,7 @@ namespace esphome {
 class Application {
  public:
   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);
@@ -67,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
@@ -167,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) {
@@ -260,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
@@ -290,7 +311,7 @@ class Application {
   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};
 };
 
diff --git a/esphome/core/automation.h b/esphome/core/automation.h
index 6d79480f0f..f43fb98f20 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.
  *
diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h
index d97d369d33..e87a4a2765 100644
--- a/esphome/core/base_automation.h
+++ b/esphome/core/base_automation.h
@@ -224,6 +224,39 @@ template class WhileAction : public Action {
   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) {}
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index 5692194a91..591c9943b5 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -55,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));
 }
@@ -87,7 +96,7 @@ 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:
@@ -120,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; }
diff --git a/esphome/core/component.h b/esphome/core/component.h
index a1afc17c2c..c3a4ac3782 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -61,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.
@@ -180,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.
    *
@@ -198,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.
diff --git a/esphome/core/config.py b/esphome/core/config.py
index bbdfcf124c..68c253f7b4 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -23,11 +23,13 @@ from esphome.const import (
     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
@@ -53,6 +55,24 @@ 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)
@@ -77,42 +97,47 @@ def valid_project_name(value: str):
 
 
 CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash"
-CONFIG_SCHEMA = cv.Schema(
-    {
-        cv.Required(CONF_NAME): cv.hostname,
-        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,
-            }
-        ),
-    }
+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(
@@ -140,7 +165,7 @@ def preload_core_config(config, result):
     CORE.data[KEY_CORE] = {}
 
     if CONF_BUILD_PATH not in conf:
-        conf[CONF_BUILD_PATH] = CORE.name
+        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
@@ -181,10 +206,16 @@ def preload_core_config(config, result):
         if CONF_BOARD_FLASH_MODE in conf:
             plat_conf[CONF_BOARD_FLASH_MODE] = conf.pop(CONF_BOARD_FLASH_MODE)
         if CONF_ARDUINO_VERSION in conf:
-            plat_conf[CONF_FRAMEWORK] = {
-                CONF_TYPE: "arduino",
-                CONF_VERSION: conf.pop(CONF_ARDUINO_VERSION),
-            }
+            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
@@ -203,6 +234,32 @@ def include_file(path, basename):
         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
@@ -284,6 +341,9 @@ async def to_code(config):
     cg.add_build_flag("-Wno-unused-but-set-variable")
     cg.add_build_flag("-Wno-sign-compare")
 
+    if CORE.using_arduino:
+        CORE.add_job(add_arduino_global_workaround)
+
     if config[CONF_INCLUDES]:
         CORE.add_job(add_includes, config[CONF_INCLUDES])
 
diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp
index 1d25be41f2..6d3a76a292 100644
--- a/esphome/core/controller.cpp
+++ b/esphome/core/controller.cpp
@@ -4,64 +4,64 @@
 
 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())
+    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 (!obj->is_internal())
+    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 (!obj->is_internal())
+    if (include_internal || !obj->is_internal())
       obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); });
   }
 #endif
diff --git a/esphome/core/controller.h b/esphome/core/controller.h
index 0de8f7ea19..0c3722855c 100644
--- a/esphome/core/controller.h
+++ b/esphome/core/controller.h
@@ -22,6 +22,9 @@
 #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
@@ -36,7 +39,7 @@ 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
diff --git a/esphome/core/datatypes.h b/esphome/core/datatypes.h
new file mode 100644
index 0000000000..5356be6b52
--- /dev/null
+++ b/esphome/core/datatypes.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include 
+
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+
+namespace internal {
+
+/// Wrapper class for memory using big endian data layout, transparently converting it to native order.
+template class BigEndianLayout {
+ public:
+  constexpr14 operator T() { return convert_big_endian(val_); }
+
+ private:
+  T val_;
+} __attribute__((packed));
+
+/// Wrapper class for memory using big endian data layout, transparently converting it to native order.
+template class LittleEndianLayout {
+ public:
+  constexpr14 operator T() { return convert_little_endian(val_); }
+
+ private:
+  T val_;
+} __attribute__((packed));
+
+}  // namespace internal
+
+/// 24-bit unsigned integer type, transparently converting to 32-bit.
+struct uint24_t {  // NOLINT(readability-identifier-naming)
+  operator uint32_t() { return val; }
+  uint32_t val : 24;
+} __attribute__((packed));
+
+/// 24-bit signed integer type, transparently converting to 32-bit.
+struct int24_t {  // NOLINT(readability-identifier-naming)
+  operator int32_t() { return val; }
+  int32_t val : 24;
+} __attribute__((packed));
+
+// Integer types in big or little endian data layout.
+using uint64_be_t = internal::BigEndianLayout;
+using uint32_be_t = internal::BigEndianLayout;
+using uint24_be_t = internal::BigEndianLayout;
+using uint16_be_t = internal::BigEndianLayout;
+using int64_be_t = internal::BigEndianLayout;
+using int32_be_t = internal::BigEndianLayout;
+using int24_be_t = internal::BigEndianLayout;
+using int16_be_t = internal::BigEndianLayout;
+using uint64_le_t = internal::LittleEndianLayout;
+using uint32_le_t = internal::LittleEndianLayout;
+using uint24_le_t = internal::LittleEndianLayout;
+using uint16_le_t = internal::LittleEndianLayout;
+using int64_le_t = internal::LittleEndianLayout;
+using int32_le_t = internal::LittleEndianLayout;
+using int24_le_t = internal::LittleEndianLayout;
+using int16_le_t = internal::LittleEndianLayout;
+
+}  // namespace esphome
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index 7c2261920a..a74755f651 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -9,16 +9,17 @@
 #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_API_NOISE
 #define USE_API_PLAINTEXT
 #define USE_BINARY_SENSOR
+#define USE_BUTTON
 #define USE_CLIMATE
 #define USE_COVER
 #define USE_DEEP_SLEEP
-#define USE_ESP8266_PREFERENCES_FLASH
 #define USE_FAN
 #define USE_GRAPH
 #define USE_HOMEASSISTANT_TIME
@@ -26,15 +27,16 @@
 #define USE_LOGGER
 #define USE_MDNS
 #define USE_NUMBER
+#define USE_OTA_PASSWORD
 #define USE_OTA_STATE_CALLBACK
 #define USE_POWER_SUPPLY
-#define USE_PROMETHEUS
 #define USE_SELECT
 #define USE_SENSOR
 #define USE_STATUS_LED
 #define USE_SWITCH
 #define USE_TEXT_SENSOR
 #define USE_TIME
+#define USE_UART_DEBUGGER
 #define USE_WIFI
 
 // Arduino-specific feature flags
@@ -43,7 +45,10 @@
 #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
@@ -62,6 +67,7 @@
 // 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
diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp
index bc94da85fe..a9e1414018 100644
--- a/esphome/core/entity_base.cpp
+++ b/esphome/core/entity_base.cpp
@@ -26,12 +26,16 @@ void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disab
 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_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST);
+  this->object_id_ = str_sanitize(str_snake_case(this->name_));
   // FNV-1 hash
   this->object_id_hash_ = fnv1_hash(this->object_id_);
 }
diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h
index 263747b721..c489d71910 100644
--- a/esphome/core/entity_base.h
+++ b/esphome/core/entity_base.h
@@ -5,6 +5,12 @@
 
 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:
@@ -31,6 +37,10 @@ class EntityBase {
   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);
@@ -45,6 +55,7 @@ class EntityBase {
   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
index b2dbe2116e..f921711ec2 100644
--- a/esphome/core/entity_helpers.py
+++ b/esphome/core/entity_helpers.py
@@ -3,29 +3,54 @@ import esphome.final_validate as fv
 from esphome.const import CONF_ID
 
 
-def inherit_property_from(property_to_inherit, parent_id_property):
+def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
     """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
+    property_to_inherit -- the name or path of the property to inherit, e.g. CONF_ICON or [CONF_SENSOR, 0, CONF_ICON]
+                           (the parent must exist, otherwise nothing is done).
+    parent_id_property -- the name or path of the property that holds the ID of the parent, e.g. CONF_POWER_ID or
+                          [CONF_SENSOR, 1, CONF_POWER_ID].
     """
 
+    def _walk_config(config, path):
+        walk = [path] if not isinstance(path, list) else path
+        for item_or_index in walk:
+            config = config[item_or_index]
+        return config
+
     def inherit_property(config):
-        if property_to_inherit not in config:
+        # Split the property into its path and name
+        if not isinstance(property_to_inherit, list):
+            property_path, property = [], property_to_inherit
+        else:
+            property_path, property = property_to_inherit[:-1], property_to_inherit[-1]
+
+        # Check if the property to inherit is accessible
+        try:
+            config_part = _walk_config(config, property_path)
+        except KeyError:
+            return config
+
+        # Only inherit the property if it does not exist yet
+        if property not in config_part:
             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)
+            parent_id = _walk_config(config, parent_id_property)
+            parent_path = fconf.get_path_for_id(parent_id)[:-1]
+            parent_config = fconf.get_config_for_path(parent_path)
 
             # If parent sensor has the property set, inherit it
-            if property_to_inherit in parent_config:
+            if property 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]
+                this_config = _walk_config(
+                    fconf.get_config_for_path(path), property_path
+                )
+                value = parent_config[property]
+                if transform:
+                    value = transform(value, config)
+                this_config[property] = value
 
         return config
 
diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h
index 1d3fb89805..04658d567c 100644
--- a/esphome/core/gpio.h
+++ b/esphome/core/gpio.h
@@ -70,6 +70,7 @@ class ISRInternalGPIOPin {
   bool digital_read();
   void digital_write(bool value);
   void clear_interrupt();
+  void pin_mode(gpio::Flags flags);
 
  protected:
   void *arg_ = nullptr;
diff --git a/esphome/core/hal.h b/esphome/core/hal.h
index a86dbf2534..034f9d692f 100644
--- a/esphome/core/hal.h
+++ b/esphome/core/hal.h
@@ -39,6 +39,7 @@ 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();
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 780df3ca6d..5f29abe579 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -2,12 +2,15 @@
 #include "esphome/core/defines.h"
 #include 
 #include 
+#include 
 #include 
 #include 
 
 #if defined(USE_ESP8266)
-#include 
 #include 
+#include 
+// for xt_rsil()/xt_wsr_ps()
+#include 
 #elif defined(USE_ESP32_FRAMEWORK_ARDUINO)
 #include 
 #elif defined(USE_ESP_IDF)
@@ -28,8 +31,8 @@ namespace esphome {
 static const char *const TAG = "helpers";
 
 void get_mac_address_raw(uint8_t *mac) {
-#ifdef USE_ESP32
-#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC
+#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,
@@ -38,26 +41,21 @@ void get_mac_address_raw(uint8_t *mac) {
 #else
   esp_efuse_mac_get_default(mac);
 #endif
-#endif
-#ifdef USE_ESP8266
-  WiFi.macAddress(mac);
+#elif defined(USE_ESP8266)
+  wifi_get_macaddr(STATION_IF, mac);
 #endif
 }
 
 std::string get_mac_address() {
-  char tmp[20];
   uint8_t mac[6];
   get_mac_address_raw(mac);
-  sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-  return std::string(tmp);
+  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];
   get_mac_address_raw(mac);
-  sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-  return std::string(tmp);
+  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
@@ -66,45 +64,6 @@ void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
 
 std::string generate_hostname(const std::string &base) { return base + std::string("-") + get_mac_address(); }
 
-uint32_t random_uint32() {
-#ifdef USE_ESP32
-  return esp_random();
-#elif defined(USE_ESP8266)
-  return os_random();
-#endif
-}
-
-double random_double() { return random_uint32() / double(UINT32_MAX); }
-
-float random_float() { return float(random_double()); }
-
-void fill_random(uint8_t *data, size_t len) {
-#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO)
-  esp_fill_random(data, len);
-#elif defined(USE_ESP8266)
-  int err = os_get_random(data, len);
-  assert(err == 0);
-#else
-#error "No random source for this system config"
-#endif
-}
-
-static uint32_t fast_random_seed = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-
-void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; }
-uint32_t fast_random_32() {
-  fast_random_seed = (fast_random_seed * 2654435769ULL) + 40503ULL;
-  return fast_random_seed;
-}
-uint16_t fast_random_16() {
-  uint32_t rand32 = fast_random_32();
-  return (rand32 & 0xFFFF) + (rand32 >> 16);
-}
-uint8_t fast_random_8() {
-  uint8_t rand32 = fast_random_32();
-  return (rand32 & 0xFF) + ((rand32 >> 8) & 0xFF);
-}
-
 float gamma_correct(float value, float gamma) {
   if (value <= 0.0f)
     return 0.0f;
@@ -122,31 +81,6 @@ float gamma_uncorrect(float value, float gamma) {
   return powf(value, 1 / gamma);
 }
 
-std::string to_lowercase_underscore(std::string s) {
-  std::transform(s.begin(), s.end(), s.begin(), ::tolower);
-  std::replace(s.begin(), s.end(), ' ', '_');
-  return s;
-}
-
-std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist) {
-  std::string out(s);
-  out.erase(std::remove_if(out.begin(), out.end(),
-                           [&allowlist](const char &c) { return allowlist.find(c) == std::string::npos; }),
-            out.end());
-  return out;
-}
-
-std::string sanitize_hostname(const std::string &hostname) {
-  std::string s = sanitize_string_allowlist(hostname, HOSTNAME_CHARACTER_ALLOWLIST);
-  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;
-}
-
 std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
   if (accuracy_decimals < 0) {
     auto multiplier = powf(10.0f, accuracy_decimals);
@@ -157,18 +91,6 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
   snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
   return std::string(tmp);
 }
-std::string uint64_to_string(uint64_t num) {
-  char buffer[17];
-  auto *address16 = reinterpret_cast(&num);
-  snprintf(buffer, sizeof(buffer), "%04X%04X%04X%04X", address16[3], address16[2], address16[1], address16[0]);
-  return std::string(buffer);
-}
-std::string uint32_to_string(uint32_t num) {
-  char buffer[9];
-  auto *address16 = reinterpret_cast(&num);
-  snprintf(buffer, sizeof(buffer), "%04X%04X", address16[1], address16[0]);
-  return std::string(buffer);
-}
 
 ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
   if (on == nullptr && strcasecmp(str, "on") == 0)
@@ -185,8 +107,6 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
   return PARSE_NONE;
 }
 
-const char *const HOSTNAME_CHARACTER_ALLOWLIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
-
 uint8_t crc8(uint8_t *data, uint8_t len) {
   uint8_t crc = 0;
 
@@ -203,120 +123,18 @@ uint8_t crc8(uint8_t *data, uint8_t len) {
   return crc;
 }
 
-void delay_microseconds_accurate(uint32_t usec) {
-  if (usec == 0)
-    return;
-  if (usec < 5000UL) {
-    delayMicroseconds(usec);
-    return;
+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
   }
-  uint32_t start = micros();
-  while (micros() - start < usec) {
-    delay(0);
-  }
-}
-
-uint8_t reverse_bits_8(uint8_t x) {
-  x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
-  x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);
-  x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4);
-  return x;
-}
-
-uint16_t reverse_bits_16(uint16_t x) {
-  return uint16_t(reverse_bits_8(x & 0xFF) << 8) | uint16_t(reverse_bits_8(x >> 8));
-}
-std::string to_string(const std::string &val) { return val; }
-std::string to_string(int val) {
-  char buf[64];
-  sprintf(buf, "%d", val);
-  return buf;
-}
-std::string to_string(long val) {  // NOLINT
-  char buf[64];
-  sprintf(buf, "%ld", val);
-  return buf;
-}
-std::string to_string(long long val) {  // NOLINT
-  char buf[64];
-  sprintf(buf, "%lld", val);
-  return buf;
-}
-std::string to_string(unsigned val) {  // NOLINT
-  char buf[64];
-  sprintf(buf, "%u", val);
-  return buf;
-}
-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) {  // NOLINT
-  char buf[64];
-  sprintf(buf, "%llu", val);
-  return buf;
-}
-std::string to_string(float val) {
-  char buf[64];
-  sprintf(buf, "%f", val);
-  return buf;
-}
-std::string to_string(double val) {
-  char buf[64];
-  sprintf(buf, "%f", val);
-  return buf;
-}
-std::string to_string(long double val) {
-  char buf[64];
-  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;
-}
-optional parse_int(const std::string &str) {
-  char *end;
-  int value = ::strtol(str.c_str(), &end, 10);
-  if (end == nullptr || end != str.end().base())
-    return {};
-  return value;
-}
-
-optional parse_hex(const char chr) {
-  int out = chr;
-  if (out >= '0' && out <= '9')
-    return (out - '0');
-  if (out >= 'A' && out <= 'F')
-    return (10 + (out - 'A'));
-  if (out >= 'a' && out <= 'f')
-    return (10 + (out - 'a'));
-  return {};
-}
-
-optional parse_hex(const std::string &str, size_t start, size_t length) {
-  if (str.length() < start) {
-    return {};
-  }
-  size_t end = start + length;
-  if (str.length() < end) {
-    return {};
-  }
-  int out = 0;
-  for (size_t i = start; i < end; i++) {
-    char chr = str[i];
-    auto digit = parse_hex(chr);
-    if (!digit.has_value()) {
-      ESP_LOGW(TAG, "Can't convert '%s' to number, invalid character %c!", str.substr(start, length).c_str(), chr);
-      return {};
-    }
-    out = (out << 4) | *digit;
-  }
-  return out;
+  while (micros() - start < us)  // fine delay the remaining usecs
+    ;
 }
 
 uint32_t fnv1_hash(const std::string &str) {
@@ -331,10 +149,6 @@ bool str_equals_case_insensitive(const std::string &a, const std::string &b) {
   return strcasecmp(a.c_str(), b.c_str()) == 0;
 }
 
-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;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void HighFrequencyLoopRequester::start() {
@@ -351,22 +165,26 @@ void HighFrequencyLoopRequester::stop() {
 }
 bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; }
 
-template T clamp(const T val, const T min, const T max) {
-  if (val < min)
-    return min;
-  if (val > max)
-    return max;
-  return val;
-}
-template float clamp(float, float, float);
-template int clamp(int, int, int);
-
 float lerp(float completion, float start, float end) { return start + (end - start) * completion; }
 
 bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; }
 bool str_endswith(const std::string &full, const std::string &ending) {
   return full.rfind(ending) == (full.size() - ending.size());
 }
+std::string str_snprintf(const char *fmt, size_t length, ...) {
+  std::string str;
+  va_list args;
+
+  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;
@@ -383,33 +201,6 @@ std::string str_sprintf(const char *fmt, ...) {
   return str;
 }
 
-uint16_t encode_uint16(uint8_t msb, uint8_t lsb) { return (uint16_t(msb) << 8) | uint16_t(lsb); }
-std::array decode_uint16(uint16_t value) {
-  uint8_t msb = (value >> 8) & 0xFF;
-  uint8_t lsb = (value >> 0) & 0xFF;
-  return {msb, lsb};
-}
-
-uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb) {
-  return (uint32_t(msb) << 24) | (uint32_t(byte2) << 16) | (uint32_t(byte3) << 8) | uint32_t(lsb);
-}
-
-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;
-  }
-  sprintf(buf, "(%u)", len);
-  res += buf;
-  return res;
-}
-
 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);
@@ -482,4 +273,113 @@ IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
 IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
 #endif
 
+// ---------------------------------------------------------------------------------------------------------------------
+
+// Mathematics
+
+uint32_t random_uint32() {
+#ifdef USE_ESP32
+  return esp_random();
+#elif defined(USE_ESP8266)
+  return os_random();
+#else
+#error "No random source available for this configuration."
+#endif
+}
+float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); }
+bool random_bytes(uint8_t *data, size_t len) {
+#ifdef USE_ESP32
+  esp_fill_random(data, len);
+  return true;
+#elif defined(USE_ESP8266)
+  return os_get_random(data, len) == 0;
+#else
+#error "No random source available for this configuration."
+#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)); }
+// wrapper around std::transform to run safely on functions from the ctype.h header
+// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes
+template std::string str_ctype_transform(const std::string &str) {
+  std::string result;
+  result.resize(str.length());
+  std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); });
+  return result;
+}
+std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); }
+std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); }
+std::string str_snake_case(const std::string &str) {
+  std::string result;
+  result.resize(str.length());
+  std::transform(str.begin(), str.end(), result.begin(), ::tolower);
+  std::replace(result.begin(), result.end(), ' ', '_');
+  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(const 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(const 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 61cc9a9e4a..c9a27a2fab 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -1,13 +1,16 @@
 #pragma once
 
+#include 
+#include 
+
 #include 
 #include 
 #include 
 #include 
 #include 
 
-#ifdef USE_ESP32_FRAMEWORK_ARDUINO
-#include "esp32-hal-psram.h"
+#ifdef USE_ESP32
+#include 
 #endif
 
 #include "esphome/core/optional.h"
@@ -17,23 +20,23 @@
 #define ALWAYS_INLINE __attribute__((always_inline))
 #define PACKED __attribute__((packed))
 
-#define xSemaphoreWait(semaphore, wait_time) \
-  xSemaphoreTake(semaphore, wait_time); \
-  xSemaphoreGive(semaphore);
+// Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement).
+// Define a substitute constexpr keyword for those functions, until we can drop C++11 support.
+#if __cplusplus >= 201402L
+#define constexpr14 constexpr
+#else
+#define constexpr14 inline  // constexpr implies inline
+#endif
 
 namespace esphome {
 
-/// The characters that are allowed in a hostname.
-extern const char *const HOSTNAME_CHARACTER_ALLOWLIST;
-
-/// Read the raw MAC address into the provided byte array (6 bytes).
+/// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
 void get_mac_address_raw(uint8_t *mac);
 
-/// Get the MAC address as a string, using lower case hex notation.
-/// This can be used as way to identify this ESP.
+/// Get the device MAC address as a string, in lowercase hex notation.
 std::string get_mac_address();
 
-/// Get the MAC address as a string, using colon-separated upper case hex notation.
+/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
 std::string get_mac_address_pretty();
 
 #ifdef USE_ESP32
@@ -41,35 +44,15 @@ std::string get_mac_address_pretty();
 void set_mac_address(uint8_t *mac);
 #endif
 
-std::string to_string(const std::string &val);
-std::string to_string(int val);
-std::string to_string(long val);                // NOLINT
-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);
-optional parse_int(const std::string &str);
-optional parse_hex(const std::string &str, size_t start, size_t length);
-optional parse_hex(char chr);
-/// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars.
-std::string sanitize_hostname(const std::string &hostname);
-
-/// 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);
 
-/// sprintf-like function returning std::string instead of writing to char array.
+/// 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 {
@@ -83,15 +66,6 @@ class HighFrequencyLoopRequester {
   bool started_{false};
 };
 
-/** Clamp the value between min and max.
- *
- * @param val The value.
- * @param min The minimum value.
- * @param max The maximum value.
- * @return val clamped in between min and max.
- */
-template T clamp(T val, T min, T max);
-
 /** Linearly interpolate between end start and end by completion.
  *
  * @tparam T The input/output typename.
@@ -112,25 +86,6 @@ template std::unique_ptr make_unique(Args &&...
 }
 #endif
 
-/// Return a random 32 bit unsigned integer.
-uint32_t random_uint32();
-
-/** Returns a random double between 0 and 1.
- *
- * Note: This function probably doesn't provide a truly uniform distribution.
- */
-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();
-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.
@@ -139,31 +94,16 @@ 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);
 
-/// Convert a uint64_t to a hex string
-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 allowlist.
-std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist);
-
-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);
-/// Encode a 32-bit unsigned integer given four bytes in MSB -> LSB order
-uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb);
-
 /// Convert RGB floats (0-1) to hue (0-360) & saturation/value percentage (0-1)
 void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value);
 /// Convert hue (0-360) & saturation/value percentage (0-1) to RGB floats (0-1)
 void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue);
 
+/// Convert degrees Celsius to degrees Fahrenheit.
+static inline float celsius_to_fahrenheit(float value) { return value * 1.8f + 32.0f; }
+/// Convert degrees Fahrenheit to degrees Celsius.
+static inline float fahrenheit_to_celsius(float value) { return (value - 32.0f) / 1.8f; }
+
 /***
  * An interrupt helper class.
  *
@@ -209,10 +149,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
@@ -244,75 +180,7 @@ template class CallbackManager {
   std::vector> callbacks_;
 };
 
-// https://stackoverflow.com/a/37161919/8924614
-template
-struct is_callable  // NOLINT
-{
-  template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type());
-
-  template static auto test(...) -> decltype(std::false_type());
-
-  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:
@@ -346,19 +214,395 @@ 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)
-  }
+// ---------------------------------------------------------------------------------------------------------------------
+
+/// @name STL backports
+///@{
+
+// std::to_string() from C++11, available from libstdc++/g++ 8
+// See https://github.com/espressif/esp-idf/issues/1445
+#if _GLIBCXX_RELEASE >= 8
+using std::to_string;
 #else
-  buffer = new T[length];  // NOLINT(cppcoreguidelines-owning-memory)
+inline std::string to_string(int value) { return str_snprintf("%d", 32, value); }                   // NOLINT
+inline std::string to_string(long value) { return str_snprintf("%ld", 32, value); }                 // NOLINT
+inline std::string to_string(long long value) { return str_snprintf("%lld", 32, value); }           // NOLINT
+inline std::string to_string(unsigned value) { return str_snprintf("%u", 32, value); }              // NOLINT
+inline std::string to_string(unsigned long value) { return str_snprintf("%lu", 32, value); }        // NOLINT
+inline std::string to_string(unsigned long long value) { return str_snprintf("%llu", 32, value); }  // NOLINT
+inline std::string to_string(float value) { return str_snprintf("%f", 32, value); }
+inline std::string to_string(double value) { return str_snprintf("%f", 32, value); }
+inline std::string to_string(long double value) { return str_snprintf("%Lf", 32, value); }
 #endif
 
-  return buffer;
+// std::is_trivially_copyable from C++11, implemented in libstdc++/g++ 5.1 (but minor releases can't be detected)
+#if _GLIBCXX_RELEASE >= 6
+using std::is_trivially_copyable;
+#else
+// Implementing this is impossible without compiler intrinsics, so don't bother. Invalid usage will be detected on
+// other variants that use a newer compiler anyway.
+// NOLINTNEXTLINE(readability-identifier-naming)
+template struct is_trivially_copyable : public std::integral_constant {};
+#endif
+
+// std::clamp from C++17
+#if __cpp_lib_clamp >= 201603
+using std::clamp;
+#else
+template constexpr const T &clamp(const T &v, const T &lo, const T &hi, Compare comp) {
+  return comp(v, lo) ? lo : comp(hi, v) ? hi : v;
+}
+template constexpr const T &clamp(const T &v, const T &lo, const T &hi) {
+  return clamp(v, lo, hi, std::less{});
+}
+#endif
+
+// std::is_invocable from C++17
+#if __cpp_lib_is_invocable >= 201703
+using std::is_invocable;
+#else
+// https://stackoverflow.com/a/37161919/8924614
+template struct is_invocable {  // NOLINT(readability-identifier-naming)
+  template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type());
+  template static auto test(...) -> decltype(std::false_type());
+  static constexpr auto value = decltype(test(nullptr))::value;  // NOLINT
+};
+#endif
+
+// std::bit_cast from C++20
+#if __cpp_lib_bit_cast >= 201806
+using std::bit_cast;
+#else
+/// Convert data between types, without aliasing issues or undefined behaviour.
+template<
+    typename To, typename From,
+    enable_if_t::value && is_trivially_copyable::value,
+                int> = 0>
+To bit_cast(const From &src) {
+  To dst;
+  memcpy(&dst, &src, sizeof(To));
+  return dst;
+}
+#endif
+
+// std::byteswap from C++23
+template constexpr14 T byteswap(T n) {
+  T m;
+  for (size_t i = 0; i < sizeof(T); i++)
+    reinterpret_cast(&m)[i] = reinterpret_cast(&n)[sizeof(T) - 1 - i];
+  return m;
+}
+template<> constexpr14 uint8_t byteswap(uint8_t n) { return n; }
+template<> constexpr14 uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); }
+template<> constexpr14 uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); }
+template<> constexpr14 uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); }
+template<> constexpr14 int8_t byteswap(int8_t n) { return n; }
+template<> constexpr14 int16_t byteswap(int16_t n) { return __builtin_bswap16(n); }
+template<> constexpr14 int32_t byteswap(int32_t n) { return __builtin_bswap32(n); }
+template<> constexpr14 int64_t byteswap(int64_t n) { return __builtin_bswap64(n); }
+
+///@}
+
+/// @name Mathematics
+///@{
+
+/// Return a random 32-bit unsigned integer.
+uint32_t random_uint32();
+/// Return a random float between 0 and 1.
+float random_float();
+/// Generate \p len number of random bytes.
+bool random_bytes(uint8_t *data, size_t len);
+
+///@}
+
+/// @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>
+constexpr14 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>
+constexpr14 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>
+constexpr14 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;
+}
+
+/// Reverse the order of 8 bits.
+inline uint8_t reverse_bits(uint8_t x) {
+  x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
+  x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);
+  x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4);
+  return x;
+}
+/// Reverse the order of 16 bits.
+inline uint16_t reverse_bits(uint16_t x) {
+  return (reverse_bits(static_cast(x & 0xFF)) << 8) | reverse_bits(static_cast((x >> 8) & 0xFF));
+}
+/// Reverse the order of 32 bits.
+inline uint32_t reverse_bits(uint32_t x) {
+  return (reverse_bits(static_cast(x & 0xFFFF)) << 16) |
+         reverse_bits(static_cast((x >> 16) & 0xFFFF));
+}
+
+/// Convert a value between host byte order and big endian (most significant byte first) order.
+template constexpr14 T convert_big_endian(T val) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+  return byteswap(val);
+#else
+  return val;
+#endif
+}
+
+/// Convert a value between host byte order and little endian (least significant byte first) order.
+template constexpr14 T convert_little_endian(T val) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+  return val;
+#else
+  return byteswap(val);
+#endif
+}
+
+///@}
+
+/// @name Strings
+///@{
+
+/// Convert the value to a string (added as extra overload so that to_string() can be used on all stringifiable types).
+inline std::string to_string(const std::string &val) { return val; }
+
+/// 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 lower case.
+std::string str_lower_case(const std::string &str);
+/// Convert the string to upper case.
+std::string str_upper_case(const std::string &str);
+/// Convert 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(const 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(const 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 constexpr 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 Memory management
+///@{
+
+/** An STL allocator that uses SPI RAM.
+ *
+ * By setting flags, it can be configured to don't try main memory if SPI RAM is full or unavailable, and to return
+ * `nulllptr` instead of aborting when no memory is available.
+ */
+template class ExternalRAMAllocator {
+ public:
+  using value_type = T;
+
+  enum Flags {
+    NONE = 0,
+    REFUSE_INTERNAL = 1 << 0,  ///< Refuse falling back to internal memory when external RAM is full or unavailable.
+    ALLOW_FAILURE = 1 << 1,    ///< Don't abort when memory allocation fails.
+  };
+
+  ExternalRAMAllocator() = default;
+  ExternalRAMAllocator(Flags flags) : flags_{flags} {}
+  template constexpr ExternalRAMAllocator(const ExternalRAMAllocator &other) : flags_{other.flags} {}
+
+  T *allocate(size_t n) {
+    size_t size = n * sizeof(T);
+    T *ptr = nullptr;
+#ifdef USE_ESP32
+    ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM));
+#endif
+    if (ptr == nullptr && (this->flags_ & Flags::REFUSE_INTERNAL) == 0)
+      ptr = static_cast(malloc(size));  // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
+    if (ptr == nullptr && (this->flags_ & Flags::ALLOW_FAILURE) == 0)
+      abort();
+    return ptr;
+  }
+
+  void deallocate(T *p, size_t n) {
+    free(p);  // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
+  }
+
+ private:
+  Flags flags_{Flags::NONE};
+};
+
+/// @}
+
+/// @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.h b/esphome/core/log.h
index 590ad26032..1e93ed4219 100644
--- a/esphome/core/log.h
+++ b/esphome/core/log.h
@@ -33,7 +33,7 @@ namespace esphome {
 #define ESPHOME_LOG_LEVEL_VERY_VERBOSE 7
 
 #ifndef ESPHOME_LOG_LEVEL
-#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG
+#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_NONE
 #endif
 
 #define ESPHOME_LOG_COLOR_BLACK "30"
diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h
index ad45cd9684..2b13061a59 100644
--- a/esphome/core/preferences.h
+++ b/esphome/core/preferences.h
@@ -2,7 +2,8 @@
 
 #include 
 #include 
-#include 
+
+#include "esphome/core/helpers.h"
 
 namespace esphome {
 
@@ -45,20 +46,12 @@ class ESPPreferences {
    */
   virtual bool sync() = 0;
 
-#ifndef USE_ESP8266
-  template::value, bool>::type = true>
-#else
-  // esp8266 toolchain doesn't have is_trivially_copyable
-  template
-#endif
+  template::value, bool> = true>
   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
+
+  template::value, bool> = true>
   ESPPreferenceObject make_preference(uint32_t type) {
     return this->make_preference(sizeof(T), type);
   }
diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
index a6d3e0307e..3fe07f94b5 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -32,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));
 }
@@ -65,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 {};
@@ -95,10 +129,9 @@ void IRAM_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));
@@ -129,6 +162,7 @@ void IRAM_ATTR HOT Scheduler::call() {
   }
 
   while (!this->empty_()) {
+    RetryResult retry_result = RETRY;
     // use scoping to indicate visibility of `item` variable
     {
       // Don't copy-by value yet
@@ -147,17 +181,19 @@ void IRAM_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
       {
         WarnIfComponentBlockingGuard guard{item->component};
-        item->f();
+        if (item->type == SchedulerItem::RETRY)
+          retry_result = item->retry_callback();
+        else
+          item->void_callback();
       }
     }
 
@@ -175,13 +211,16 @@ void IRAM_ATTR HOT Scheduler::call() {
         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));
       }
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
index d1839cb4a7..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_();
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index 5b081698ad..9127f88e39 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -2,6 +2,7 @@ import logging
 
 from esphome.const import (
     CONF_DISABLED_BY_DEFAULT,
+    CONF_ENTITY_CATEGORY,
     CONF_ICON,
     CONF_INTERNAL,
     CONF_NAME,
@@ -102,6 +103,8 @@ async def setup_entity(var, 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):
diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py
index 888c319024..806a2d832c 100644
--- a/esphome/cpp_types.py
+++ b/esphome/cpp_types.py
@@ -27,10 +27,10 @@ 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")
+JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")
 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 eb698a7de1..c68d037fe6 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -9,6 +9,7 @@ import json
 import logging
 import multiprocessing
 import os
+from pathlib import Path
 import secrets
 import shutil
 import subprocess
@@ -26,7 +27,7 @@ import tornado.process
 import tornado.web
 import tornado.websocket
 
-from esphome import const, util
+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,
@@ -398,17 +399,57 @@ 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
+
+        elif type == "firmware-factory.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}-factory.bin"
+            path = storage_json.firmware_bin_path.replace(
+                "firmware.bin", "firmware-factory.bin"
+            )
+
+        else:
+            args = ["esphome", "idedata", settings.rel_path(configuration)]
+            rc, stdout, _ = run_system_command(*args)
+
+            if rc != 0:
+                self.send_error(404 if rc == 2 else 500)
+                return
+
+            idedata = platformio_api.IDEData(json.loads(stdout))
+
+            found = False
+            for image in idedata.extra_flash_images:
+                if image.path.endswith(type):
+                    path = image.path
+                    filename = type
+                    found = True
+                    break
+
+            if not found:
+                self.send_error(404)
+                return
+
+        self.set_header("Content-Type", "application/octet-stream")
+        self.set_header("Content-Disposition", f'attachment; filename="{filename}"')
+        self.set_header("Cache-Control", "no-cache")
+        if not Path(path).is_file():
+            self.send_error(404)
             return
 
-        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:
             while True:
                 data = f.read(16384)
@@ -418,6 +459,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]
@@ -448,6 +521,12 @@ 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:
@@ -508,6 +587,7 @@ class ListDevicesHandler(BaseHandler):
                             "path": entry.path,
                             "comment": entry.comment,
                             "address": entry.address,
+                            "web_port": entry.web_port,
                             "target_platform": entry.target_platform,
                         }
                         for entry in entries
@@ -533,10 +613,10 @@ class MainRequestHandler(BaseHandler):
         begin = bool(self.get_argument("begin", False))
 
         self.render(
-            get_template_path("index"),
+            "index.template.html",
             begin=begin,
             **template_args(),
-            login_enabled=settings.using_auth,
+            login_enabled=settings.using_password,
         )
 
 
@@ -710,7 +790,7 @@ class LoginHandler(BaseHandler):
 
     def render_login_page(self, error=None):
         self.render(
-            get_template_path("login"),
+            "login.template.html",
             error=error,
             hassio=settings.using_hassio_auth,
             has_username=bool(settings.username),
@@ -768,6 +848,28 @@ class LogoutHandler(BaseHandler):
         self.redirect("./login")
 
 
+class SecretKeysRequestHandler(BaseHandler):
+    @authenticated
+    def get(self):
+
+        filename = None
+
+        for secret_filename in const.SECRETS_FILES:
+            relative_filename = settings.rel_path(secret_filename)
+            if os.path.isfile(relative_filename):
+                filename = relative_filename
+                break
+
+        if filename is None:
+            self.send_error(404)
+            return
+
+        secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False))
+
+        self.set_header("content-type", "application/json")
+        self.write(json.dumps(secret_keys))
+
+
 def get_base_frontend_path():
     if ENV_DEV not in os.environ:
         import esphome_dashboard
@@ -782,10 +884,6 @@ def get_base_frontend_path():
     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)
 
@@ -843,6 +941,7 @@ def make_app(debug=get_bool_env(ENV_DEV)):
         "cookie_secret": settings.cookie_secret,
         "log_function": log_function,
         "websocket_ping_interval": 30.0,
+        "template_path": get_base_frontend_path(),
     }
     rel = settings.relative_url
     app = tornado.web.Application(
@@ -862,6 +961,7 @@ def make_app(debug=get_bool_env(ENV_DEV)):
             (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),
@@ -870,6 +970,7 @@ def make_app(debug=get_bool_env(ENV_DEV)):
             (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}),
             (f"{rel}devices", ListDevicesHandler),
             (f"{rel}import", ImportRequestHandler),
+            (f"{rel}secret_keys", SecretKeysRequestHandler),
         ],
         **app_settings,
     )
@@ -901,16 +1002,17 @@ def start_web_server(args):
         server.add_socket(socket)
     else:
         _LOGGER.info(
-            "Starting dashboard web server on port %s and configuration dir %s...",
+            "Starting dashboard web server on http://%s:%s and configuration dir %s...",
+            args.address,
             args.port,
             settings.config_dir,
         )
-        app.listen(args.port)
+        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/espota2.py b/esphome/espota2.py
index f8a2fab94c..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__)
 
 
@@ -170,11 +174,9 @@ def send_check(sock, data, msg):
 
 
 def perform_ota(sock, password, file_handle, filename):
-    file_md5 = hashlib.md5(file_handle.read()).hexdigest()
-    file_size = file_handle.tell()
+    file_contents = file_handle.read()
+    file_size = len(file_contents)
     _LOGGER.info("Uploading %s (%s bytes)", filename, file_size)
-    file_handle.seek(0)
-    _LOGGER.debug("MD5 of binary is %s", file_md5)
 
     # Enable nodelay, we need it for phase 1
     sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@@ -185,8 +187,16 @@ def perform_ota(sock, password, file_handle, filename):
         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]
+
+    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]
@@ -213,16 +223,20 @@ def perform_ota(sock, password, file_handle, filename):
         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")
+    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")
+    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
@@ -236,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)
@@ -247,7 +261,7 @@ def perform_ota(sock, password, file_handle, filename):
             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
diff --git a/esphome/git.py b/esphome/git.py
index 12c6b41648..64c8d6a6b7 100644
--- a/esphome/git.py
+++ b/esphome/git.py
@@ -2,6 +2,7 @@ from pathlib import Path
 import subprocess
 import hashlib
 import logging
+import urllib.parse
 
 from datetime import datetime
 
@@ -36,19 +37,39 @@ def _compute_destination_path(key: str, domain: str) -> Path:
 
 
 def clone_or_update(
-    *, url: str, ref: str = None, refresh: TimePeriodSeconds, domain: str
+    *,
+    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:
+        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")
diff --git a/esphome/log.py b/esphome/log.py
index abefcf6308..e7ba0fdd82 100644
--- a/esphome/log.py
+++ b/esphome/log.py
@@ -49,8 +49,10 @@ def color(col: str, msg: str, reset: bool = True) -> bool:
 
 
 class ESPHomeLogFormatter(logging.Formatter):
-    def __init__(self):
-        super().__init__(fmt="%(asctime)s %(levelname)s %(message)s", style="%")
+    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)
@@ -64,7 +66,9 @@ class ESPHomeLogFormatter(logging.Formatter):
         return f"{prefix}{formatted}{Style.RESET_ALL}"
 
 
-def setup_log(debug=False, quiet=False):
+def setup_log(
+    debug: bool = False, quiet: bool = False, include_timestamp: bool = False
+) -> None:
     import colorama
 
     if debug:
@@ -79,4 +83,6 @@ def setup_log(debug=False, quiet=False):
     logging.getLogger("urllib3").setLevel(logging.WARNING)
 
     colorama.init()
-    logging.getLogger().handlers[0].setFormatter(ESPHomeLogFormatter())
+    logging.getLogger().handlers[0].setFormatter(
+        ESPHomeLogFormatter(include_timestamp=include_timestamp)
+    )
diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py
index 054c0cb1b0..2072e25ec5 100644
--- a/esphome/platformio_api.py
+++ b/esphome/platformio_api.py
@@ -46,24 +46,31 @@ 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 Modes:.*",
     r"LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*",
-    r"LDF Modes: Finder ~ chain, Compatibility ~ soft.*",
     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"Found: https://platformio.org/lib/show/.*",
     r"Using cache: .*",
     r"Installing dependencies",
-    r".* @ .* is already installed",
+    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 .*",
 ]
 
 
@@ -118,7 +125,7 @@ def _run_idedata(config):
 
 def _load_idedata(config):
     platformio_ini = Path(CORE.relative_build_path("platformio.ini"))
-    temp_idedata = Path(CORE.relative_internal_path(CORE.name, "idedata.json"))
+    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():
diff --git a/esphome/storage_json.py b/esphome/storage_json.py
index 3262559116..207a3edf57 100644
--- a/esphome/storage_json.py
+++ b/esphome/storage_json.py
@@ -41,6 +41,7 @@ class StorageJSON:
         esphome_version,
         src_version,
         address,
+        web_port,
         target_platform,
         build_path,
         firmware_bin_path,
@@ -60,6 +61,9 @@ class StorageJSON:
         self.src_version = src_version  # type: int
         # 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.target_platform = target_platform  # type: str
         # The absolute path to the platformio project
@@ -78,6 +82,7 @@ class StorageJSON:
             "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,
@@ -101,6 +106,7 @@ class StorageJSON:
             esphome_version=const.__version__,
             src_version=1,
             address=esph.address,
+            web_port=esph.web_port,
             target_platform=esph.target_platform,
             build_path=esph.build_path,
             firmware_bin_path=esph.firmware_bin,
@@ -117,6 +123,7 @@ class StorageJSON:
             esphome_version=const.__version__,
             src_version=1,
             address=address,
+            web_port=None,
             target_platform=esp_platform,
             build_path=None,
             firmware_bin_path=None,
@@ -135,6 +142,7 @@ class StorageJSON:
         )
         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")
@@ -146,6 +154,7 @@ class StorageJSON:
             esphome_version,
             src_version,
             address,
+            web_port,
             esp_platform,
             build_path,
             firmware_bin_path,
diff --git a/esphome/util.py b/esphome/util.py
index 527e370ad8..b2ba0c22c3 100644
--- a/esphome/util.py
+++ b/esphome/util.py
@@ -178,7 +178,7 @@ def run_external_command(
     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)
+    _LOGGER.debug("Running:  %s", full_cmd)
 
     orig_stdout = sys.stdout
     sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
@@ -192,8 +192,8 @@ def run_external_command(
         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
@@ -214,27 +214,28 @@ def run_external_command(
 
 def run_external_process(*cmd, **kwargs):
     full_cmd = " ".join(shlex_quote(x) for x in cmd)
-    _LOGGER.info("Running:  %s", full_cmd)
+    _LOGGER.debug("Running:  %s", full_cmd)
     filter_lines = kwargs.get("filter_lines")
 
     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():
diff --git a/esphome/wizard.py b/esphome/wizard.py
index 5c35fac73a..c64ad3a583 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -45,9 +45,9 @@ OTA_BIG = r"""       ____ _______
 
 BASE_CONFIG = """esphome:
   name: {name}
-  platform: {platform}
-  board: {board}
+"""
 
+LOGGER_API_CONFIG = """
 # Enable logging
 logger:
 
@@ -55,6 +55,18 @@ logger:
 api:
 """
 
+ESP8266_CONFIG = """
+esp8266:
+  board: {board}
+"""
+
+ESP32_CONFIG = """
+esp32:
+  board: {board}
+  framework:
+    type: arduino
+"""
+
 
 def sanitize_double_quotes(value):
     return value.replace("\\", "\\\\").replace('"', '\\"')
@@ -71,6 +83,14 @@ def wizard_file(**kwargs):
 
     config = BASE_CONFIG.format(**kwargs)
 
+    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"
@@ -86,12 +106,11 @@ def wizard_file(**kwargs):
     config += "\n\nwifi:\n"
 
     if "ssid" in kwargs:
-        # pylint: disable=consider-using-f-string
-        config += """  ssid: "{ssid}"
-  password: "{psk}"
-""".format(
-            **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 += """  # ssid: "My SSID"
   # password: "mypassword"
@@ -141,7 +160,6 @@ if get_bool_env(ENV_QUICKWIZARD):
     def sleep(time):
         pass
 
-
 else:
     from time import sleep
 
@@ -367,10 +385,9 @@ def wizard(path):
     )
     safe_print()
     safe_print("Next steps:")
+    safe_print("  > Follow the rest of the getting started guide:")
     safe_print(
-        '  > Check your Home Assistant "integrations" screen. If all goes well, you '
-        "should see your ESP being discovered automatically."
+        "  > https://esphome.io/guides/getting_started_command_line.html#adding-some-features"
     )
-    safe_print("  > Then follow the rest of the getting started guide:")
-    safe_print("  > https://esphome.io/guides/getting_started_command_line.html")
+    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 29532d4f64..89a074683a 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -38,10 +38,8 @@ CPP_BASE_FORMAT = (
     """"
 
 void setup() {
-  // ===== DO NOT EDIT ANYTHING BELOW THIS LINE =====
   """,
     """
-  // ========= YOU CAN EDIT AFTER THIS LINE =========
   App.setup();
 }
 
@@ -59,10 +57,8 @@ lib_deps =
 build_flags =
 upload_flags =
 
-; ===== DO NOT EDIT ANYTHING BELOW THIS LINE =====
 """,
     """
-; ========= YOU CAN EDIT AFTER THIS LINE =========
 
 """,
 )
@@ -102,61 +98,6 @@ 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"',
-            f"{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
@@ -175,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()
@@ -277,12 +215,12 @@ 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.
 """
 
 
@@ -339,9 +277,7 @@ def copy_src_tree():
     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_build_path("README.txt"), ESPHOME_README_TXT)
     write_file_if_changed(
         CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s)
     )
@@ -354,6 +290,11 @@ def copy_src_tree():
 
         copy_files()
 
+    elif CORE.is_esp8266:
+        from esphome.components.esp8266 import copy_files
+
+        copy_files()
+
 
 def generate_defines_h():
     define_content_l = [x.as_macro for x in CORE.defines]
@@ -413,11 +354,6 @@ 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
 """
 
diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py
index bdadbbd43a..57009be57e 100644
--- a/esphome/yaml_util.py
+++ b/esphome/yaml_util.py
@@ -329,9 +329,10 @@ 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)
 
 
diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py
index a19fc143ec..1fbdf7e93f 100644
--- a/esphome/zeroconf.py
+++ b/esphome/zeroconf.py
@@ -13,8 +13,9 @@ from zeroconf import (
     RecordUpdateListener,
     Zeroconf,
     ServiceBrowser,
+    ServiceStateChange,
+    current_time_millis,
 )
-from zeroconf._services import ServiceStateChange
 
 _CLASS_IN = 1
 _FLAGS_QR_QUERY = 0x0000  # query
@@ -88,7 +89,7 @@ class DashboardStatus(threading.Thread):
         entries = self.zc.cache.entries_with_name(key)
         if not entries:
             return False
-        now = time.time() * 1000
+        now = current_time_millis()
 
         return any(
             (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries
@@ -99,7 +100,7 @@ class DashboardStatus(threading.Thread):
             self.on_update(
                 {key: self.host_status(host) for key, host in self.key_to_host.items()}
             )
-            now = time.time() * 1000
+            now = current_time_millis()
             for host in self.query_hosts:
                 entries = self.zc.cache.entries_with_name(host)
                 if not entries or all(
diff --git a/platformio.ini b/platformio.ini
index 9cc7477d51..589624a71d 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -4,30 +4,39 @@
 ; It's *not* used during runtime.
 
 [platformio]
-default_envs = esp8266, esp32, esp32-idf
+default_envs = esp8266-arduino, esp32-arduino, esp32-idf
+; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build
+; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this
+; being the root directory. Instead, set esphome/ as the source directory, all our sources are in
+; there anyway. Set the root directory as the include_dir, so that the esphome/ directory is on the
+; include path.
 src_dir = esphome
-include_dir =
+include_dir = .
 
-[runtime]
-; This are the flags as set by the runtime.
+; This are just the build flags as set by the runtime.
+[flags:runtime]
 build_flags =
-    -Wno-unused-variable
     -Wno-unused-but-set-variable
     -Wno-sign-compare
 
-[clangtidy]
-; This are the flags for clang-tidy.
+; This are just the build flags for clang-tidy.
+[flags:clangtidy]
 build_flags =
     -Wall
+    -Wextra
     -Wunreachable-code
     -Wfor-loop-analysis
     -Wshadow-field
     -Wshadow-field-in-constructor
+    -Wshadow-uncaptured-local
 
+; This are common settings for all environments.
 [common]
 lib_deps =
-    esphome/noise-c@0.1.3     ; api
-    makuna/NeoPixelBus@2.6.7  ; neopixelbus
+    esphome/noise-c@0.1.4         ; api
+    makuna/NeoPixelBus@2.6.9      ; neopixelbus
+    esphome/Improv@1.0.0          ; improv_serial / esp32_improv
+    bblanchon/ArduinoJson@6.18.5  ; json
 build_flags =
     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
 src_filter =
@@ -35,32 +44,35 @@ src_filter =
     +<../tests/dummy_main.cpp>
     +<../.temp/all-include.cpp>
 
+; This are common settings for all Arduino-framework based environments.
 [common:arduino]
 extends = common
 lib_deps =
     ${common.lib_deps}
-    ottowinter/AsyncMqttClient-esphome@0.8.4              ; mqtt
-    ottowinter/ArduinoJson-esphomelib@5.13.3              ; json
-    esphome/ESPAsyncWebServer-esphome@1.3.0               ; web_server_base
+    ottowinter/AsyncMqttClient-esphome@0.8.6              ; mqtt
+    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
-    seeed-studio/Grove - Laser PM2.5 Sensor HM3301@1.0.3  ; hm3301
     glmnet/Dsmr@0.5                                       ; dsmr
     rweather/Crypto@0.2.0                                 ; dsmr
     dudanov/MideaUART@1.1.8                               ; midea
-    tonia/HeatpumpIR@^1.0.15                              ; heatpumpir
+    ; 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
 
+; This are common settings for all IDF-framework based environments.
 [common:idf]
 extends = common
 build_flags =
     ${common.build_flags}
     -DUSE_ESP_IDF
 
-[common:esp8266]
+; This are common settings for the ESP8266 using Arduino.
+[common:esp8266-arduino]
 extends = common:arduino
 ; when changing this also copy it to esphome-docker-base images
 platform = platformio/espressif8266 @ 3.2.0
@@ -68,7 +80,6 @@ platform_packages =
     platformio/framework-arduinoespressif8266 @ ~3.30002.0
 
 framework = arduino
-board = nodemcuv2
 lib_deps =
     ${common:arduino.lib_deps}
     ESP8266WiFi                           ; wifi (Arduino built-in)
@@ -78,7 +89,9 @@ build_flags =
     ${common:arduino.build_flags}
     -DUSE_ESP8266
     -DUSE_ESP8266_FRAMEWORK_ARDUINO
+extra_scripts = post:esphome/components/esp8266/post_build.py
 
+; This are common settings for the ESP32 (all variants) using Arduino.
 [common:esp32-arduino]
 extends = common:arduino
 ; when changing this also copy it to esphome-docker-base images
@@ -95,7 +108,9 @@ build_flags =
     ${common:arduino.build_flags}
     -DUSE_ESP32
     -DUSE_ESP32_FRAMEWORK_ARDUINO
+extra_scripts = post:esphome/components/esp32/post_build.py
 
+; This are common settings for the ESP32 (all variants) using IDF.
 [common:esp32-idf]
 extends = common:idf
 ; when changing this also copy it to esphome-docker-base images
@@ -104,7 +119,6 @@ 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
@@ -113,41 +127,81 @@ build_flags =
     -Wno-nonnull-compare
     -DUSE_ESP32
     -DUSE_ESP32_FRAMEWORK_ESP_IDF
+extra_scripts = post:esphome/components/esp32/post_build.py
 
-[env:esp8266]
-extends = common:esp8266
+; All the actual environments are defined below.
+[env:esp8266-arduino]
+extends = common:esp8266-arduino
+board = nodemcuv2
 build_flags =
-    ${common:esp8266.build_flags}
-    ${runtime.build_flags}
+    ${common:esp8266-arduino.build_flags}
+    ${flags:runtime.build_flags}
 
-[env:esp8266-tidy]
-extends = common:esp8266
+[env:esp8266-arduino-tidy]
+extends = common:esp8266-arduino
+board = nodemcuv2
 build_flags =
-    ${common:esp8266.build_flags}
-    ${clangtidy.build_flags}
+    ${common:esp8266-arduino.build_flags}
+    ${flags:clangtidy.build_flags}
 
-[env:esp32]
+[env:esp32-arduino]
 extends = common:esp32-arduino
+board = esp32dev
 build_flags =
     ${common:esp32-arduino.build_flags}
-    ${runtime.build_flags}
+    ${flags:runtime.build_flags}
 
-[env:esp32-tidy]
+[env:esp32-arduino-tidy]
 extends = common:esp32-arduino
+board = esp32dev
 build_flags =
     ${common:esp32-arduino.build_flags}
-    ${clangtidy.build_flags}
+    ${flags:clangtidy.build_flags}
 
 [env:esp32-idf]
 extends = common:esp32-idf
+board = esp32dev
 board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf
 build_flags =
     ${common:esp32-idf.build_flags}
-    ${runtime.build_flags}
+    ${flags:runtime.build_flags}
 
 [env:esp32-idf-tidy]
 extends = common:esp32-idf
+board = esp32dev
 board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf-tidy
 build_flags =
     ${common:esp32-idf.build_flags}
-    ${clangtidy.build_flags}
+    ${flags:clangtidy.build_flags}
+
+[env:esp32c3-idf]
+extends = common:esp32-idf
+board = esp32-c3-devkitm-1
+board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf
+build_flags =
+    ${common:esp32-idf.build_flags}
+    ${flags:runtime.build_flags}
+
+[env:esp32c3-idf-tidy]
+extends = common:esp32-idf
+board = esp32-c3-devkitm-1
+board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf-tidy
+build_flags =
+    ${common:esp32-idf.build_flags}
+    ${flags:clangtidy.build_flags}
+
+[env:esp32s2-idf]
+extends = common:esp32-idf
+board = esp32-s2-kaluga-1
+board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf
+build_flags =
+    ${common:esp32-idf.build_flags}
+    ${flags:runtime.build_flags}
+
+[env:esp32s2-idf-tidy]
+extends = common:esp32-idf
+board = esp32-s2-kaluga-1
+board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf-tidy
+build_flags =
+    ${common:esp32-idf.build_flags}
+    ${flags:clangtidy.build_flags}
diff --git a/requirements.txt b/requirements.txt
index 23a00d3755..9add417bdf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,17 @@
 voluptuous==0.12.2
-PyYAML==5.4.1
-paho-mqtt==1.5.1
+PyYAML==6.0
+paho-mqtt==1.6.1
 colorama==0.4.4
 tornado==6.1
-tzlocal==3.0    # from time
+tzlocal==4.1    # from time
 tzdata>=2021.1  # from time
 pyserial==3.5
-platformio==5.2.1
-esptool==3.1
+platformio==5.2.4  # When updating platformio, also update Dockerfile
+esptool==3.2
 click==8.0.3
-esphome-dashboard==20211011.1
-aioesphomeapi==9.1.5
+esphome-dashboard==20220116.0
+aioesphomeapi==10.6.0
+zeroconf==0.37.0
 
 # esp-idf requires this, but doesn't bundle it by default
 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
diff --git a/requirements_test.txt b/requirements_test.txt
index 8ebcf24d4d..1203858c96 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,13 +1,12 @@
-pylint==2.11.1
+pylint==2.12.2
 flake8==4.0.1
-black==21.9b0
-pexpect==4.8.0
+black==21.12b0
 pre-commit
 
 # Unit tests
 pytest==6.2.5
 pytest-cov==3.0.0
 pytest-mock==3.6.1
-pytest-asyncio==0.15.1
+pytest-asyncio==0.17.2
 asyncmock==0.4.2
 hypothesis==5.49.0
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 7ccdc5a24e..016a0995b9 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -649,7 +649,7 @@ def build_message_type(desc):
             o += f" {dump[0]} "
         else:
             o += "\n"
-            o += f"  char buffer[64];\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'
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 8e9ca487a6..52ac4025ca 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -1,16 +1,16 @@
 #!/usr/bin/env python3
 
-from helpers import git_ls_files, filter_changed
+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
-import functools
-import argparse
 
 sys.path.append(os.path.dirname(__file__))
 
@@ -20,7 +20,7 @@ def find_all(a_str, sub):
         # Optimization: If str is not in whole text, then do not try
         # on each line
         return
-    for i, line in enumerate(a_str.splitlines()):
+    for i, line in enumerate(a_str.split('\n')):
         column = 0
         while True:
             column = line.find(sub, column)
@@ -30,6 +30,8 @@ def find_all(a_str, sub):
             column += len(sub)
 
 
+colorama.init()
+
 parser = argparse.ArgumentParser()
 parser.add_argument(
     "files", nargs="*", default=[], help="files to be processed (regex on path)"
@@ -170,7 +172,7 @@ def lint_re_check(regex, **kwargs):
     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):
@@ -183,6 +185,8 @@ def lint_content_find_check(find, **kwargs):
             for line, col in find_all(content, find_):
                 err = func(fname)
                 errors.append((line + 1, col + 1, err))
+                if only_first:
+                    break
             return errors
 
         return decor(new_func)
@@ -232,6 +236,7 @@ def lint_executable_bit(fname):
 
 @lint_content_find_check(
     "\t",
+    only_first=True,
     exclude=[
         "esphome/dashboard/static/ace.js",
         "esphome/dashboard/static/ext-searchbox.js",
@@ -241,9 +246,9 @@ 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"])
@@ -263,7 +268,11 @@ def highlight(s):
 @lint_re_check(
     r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL,
     include=cpp_include,
-    exclude=["esphome/core/log.h", "esphome/components/socket/headers.h"],
+    exclude=[
+        "esphome/core/log.h",
+        "esphome/components/socket/headers.h",
+        "esphome/core/defines.h",
+    ],
 )
 def lint_no_defines(fname, match):
     s = highlight(
@@ -597,6 +606,7 @@ def lint_inclusive_language(fname, match):
         "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",
@@ -653,10 +663,10 @@ for fname in files:
 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 lineno, col, msg in errs:
-        print(f"ERROR {f}:{lineno}:{col} - {msg}")
-    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 = []
diff --git a/script/clang-format b/script/clang-format
index d6588f1ccb..515df4c027 100755
--- a/script/clang-format
+++ b/script/clang-format
@@ -1,6 +1,9 @@
 #!/usr/bin/env python3
 
+from helpers import print_error_for_file, get_output, git_ls_files, filter_changed
 import argparse
+import click
+import colorama
 import multiprocessing
 import os
 import queue
@@ -9,11 +12,6 @@ import subprocess
 import sys
 import threading
 
-import click
-
-sys.path.append(os.path.dirname(__file__))
-from helpers import get_output, git_ls_files, filter_changed
-
 
 def run_format(args, queue, lock, failed_files):
     """Takes filenames out of queue and runs clang-format on them."""
@@ -29,11 +27,7 @@ def run_format(args, queue, lock, failed_files):
         proc = subprocess.run(invocation, capture_output=True, encoding='utf-8')
         if proc.returncode != 0:
             with lock:
-                print()
-                print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path))
-                print(proc.stdout)
-                print(proc.stderr)
-                print()
+                print_error_for_file(path, proc.stderr)
                 failed_files.append(path)
         queue.task_done()
 
@@ -43,6 +37,8 @@ def progress_bar_show(value):
 
 
 def main():
+    colorama.init()
+
     parser = argparse.ArgumentParser()
     parser.add_argument('-j', '--jobs', type=int,
                         default=multiprocessing.cpu_count(),
diff --git a/script/clang-tidy b/script/clang-tidy
index 87ba1c84b5..8a7d229887 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -1,7 +1,10 @@
 #!/usr/bin/env python3
 
+from helpers import print_error_for_file, get_output, filter_grep, \
+    build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, root_path, basepath
 import argparse
-import json
+import click
+import colorama
 import multiprocessing
 import os
 import queue
@@ -12,18 +15,21 @@ import sys
 import tempfile
 import threading
 
-import click
-import pexpect
-
-sys.path.append(os.path.dirname(__file__))
-from helpers import shlex_quote, get_output, filter_grep, \
-    build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, basepath
-
 
 def clang_options(idedata):
-    cmd = [
-        # target 32-bit arch (this prevents size mismatch errors on a 64-bit host)
-        '-m32',
+    cmd = []
+
+    # extract target architecture from triplet in g++ filename
+    triplet = os.path.basename(idedata['cxx_path'])[:-4]
+    if triplet.startswith("xtensa-"):
+        # clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler
+        cmd.append('-m32')
+        cmd.append('-D__XTENSA__')
+    else:
+        cmd.append(f'--target={triplet}')
+
+    # set flags
+    cmd.extend([
         # disable built-in include directories from the host
         '-nostdinc',
         '-nostdinc++',
@@ -32,6 +38,7 @@ def clang_options(idedata):
         '-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 *',
@@ -42,15 +49,13 @@ def clang_options(idedata):
         # suppress warning about attribute cannot be applied to type
         # https://github.com/esp8266/Arduino/pull/8258
         '-Ddeprecated(x)=',
-        # pretend we're an Xtensa compiler, which gates some features in the headers
-        '-D__XTENSA__',
         # allow to condition code on the presence of clang-tidy
         '-DCLANG_TIDY',
         # (esp-idf) Disable this header because they use asm with registers clang-tidy doesn't know
         '-D__XTENSA_API_H__',
         # (esp-idf) Fix __once_callable in some libstdc++ headers
         '-D_GLIBCXX_HAVE_TLS',
-    ]
+    ])
 
     # copy compiler flags, except those clang doesn't understand.
     cmd.extend(flag for flag in idedata['cxx_flags'].split(' ')
@@ -61,13 +66,21 @@ def clang_options(idedata):
     # defines
     cmd.extend(f'-D{define}' for define in idedata['defines'])
 
-    # add include directories, using -isystem for dependencies to suppress their errors
+    # add toolchain include directories using -isystem to suppress their errors
+    # idedata contains include directories for all toolchains of this platform, only use those from the one in use
+    toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../")
     for directory in idedata['includes']['toolchain']:
-        if 'xtensa-esp32s2-elf' not in directory:
+        if directory.startswith(toolchain_dir):
             cmd.extend(['-isystem', directory])
+
+    # add library include directories using -isystem to suppress their errors
     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])
+        # skip our own directories, we add those later
+        if not directory.startswith(f"{root_path}/") or directory.startswith(f"{root_path}/.pio/"):
+            cmd.extend(['-isystem', directory])
+
+    # add the esphome include directory using -I
+    cmd.extend(['-I', root_path])
 
     return cmd
 
@@ -86,23 +99,20 @@ def run_tidy(args, options, tmpdir, queue, lock, failed_files):
             invocation.append(name)
 
         if args.quiet:
-            invocation.append('-quiet')
+            invocation.append('--quiet')
+
+        if sys.stdout.isatty():
+            invocation.append('--use-color')
 
-        invocation.append(os.path.abspath(path))
         invocation.append(f"--header-filter={os.path.abspath(basepath)}/.*")
+        invocation.append(os.path.abspath(path))
         invocation.append('--')
         invocation.extend(options)
-        invocation_s = ' '.join(shlex_quote(x) for x in invocation)
 
-        # Use pexpect for a pseudy-TTY with colored output
-        output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8',
-                                 timeout=15 * 60)
-        if rc != 0:
+        proc = subprocess.run(invocation, capture_output=True, encoding='utf-8')
+        if proc.returncode != 0:
             with lock:
-                print()
-                print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path))
-                print(output)
-                print()
+                print_error_for_file(path, proc.stdout)
                 failed_files.append(path)
         queue.task_done()
 
@@ -118,12 +128,14 @@ def split_list(a, 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('-e', '--environment', default='esp32-arduino-tidy',
+                        help='the PlatformIO environment to use (as defined in platformio.ini)')
     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')
diff --git a/script/helpers.py b/script/helpers.py
index 430d8a8e7f..abf970b8a2 100644
--- a/script/helpers.py
+++ b/script/helpers.py
@@ -1,4 +1,4 @@
-import codecs
+import colorama
 import os.path
 import re
 import subprocess
@@ -11,13 +11,18 @@ 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():
diff --git a/script/lint-python b/script/lint-python
index 41885b9672..8ee038a661 100755
--- a/script/lint-python
+++ b/script/lint-python
@@ -1,15 +1,13 @@
 #!/usr/bin/env python3
 
 from __future__ import print_function
-from helpers import get_output, get_err, git_ls_files, filter_changed
-
+from helpers import styled, print_error_for_file, get_output, get_err, git_ls_files, filter_changed
 import argparse
+import colorama
 import os
 import re
 import sys
 
-sys.path.append(os.path.dirname(__file__))
-
 curfile = None
 
 
@@ -17,14 +15,18 @@ 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("{}:{} - {}".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)"
@@ -56,6 +58,7 @@ def main():
 
     cmd = ["black", "--verbose", "--check"] + files
     print("Running black...")
+    print()
     log = get_err(*cmd)
     for line in log.splitlines():
         WOULD_REFORMAT = "would reformat"
@@ -65,7 +68,9 @@ def main():
             errors += 1
 
     cmd = ["flake8"] + files
+    print()
     print("Running flake8...")
+    print()
     log = get_output(*cmd)
     for line in log.splitlines():
         line = line.split(":", 4)
@@ -78,7 +83,9 @@ def main():
         errors += 1
 
     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)
diff --git a/script/setup b/script/setup
index 6d095af46c..71828deeaa 100755
--- a/script/setup
+++ b/script/setup
@@ -5,6 +5,6 @@ set -e
 
 cd "$(dirname "$0")/.."
 pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
-pip3 install -e .
+pip3 install --no-use-pep517 -e .
 
 pre-commit install
diff --git a/sdkconfig.defaults b/sdkconfig.defaults
index 6b2d6f8f2e..72ca3f6e9c 100644
--- a/sdkconfig.defaults
+++ b/sdkconfig.defaults
@@ -8,6 +8,11 @@ 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
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/test1.yaml b/tests/test1.yaml
index 157ccfc5d1..d3351e3b12 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -94,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
@@ -192,6 +194,18 @@ uart:
     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
@@ -233,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
 
@@ -642,6 +657,15 @@ sensor:
         name: 'INA3221 Channel 1 Shunt Voltage'
     update_interval: 15s
     i2c_id: i2c_bus
+  - platform: kalman_combinator
+    name: "Kalman-filtered temperature"
+    process_std_dev: 0.00139
+    sources:
+      - source: scd30_temperature
+        error: !lambda |-
+          return 0.4 + std::abs(x - 25) * 0.023;
+      - source: scd4x_temperature
+        error: 1.5
   - platform: htu21d
     temperature:
       name: 'Living Room Temperature 6'
@@ -805,6 +829,7 @@ sensor:
     co2:
       name: 'Living Room CO2 9'
     temperature:
+      id: scd30_temperature
       name: 'Living Room Temperature 9'
     humidity:
       name: 'Living Room Humidity 9'
@@ -819,6 +844,7 @@ sensor:
     co2:
       name: "SCD4X CO2"
     temperature:
+      id: scd4x_temperature
       name: "SCD4X Temperature"
     humidity:
       name: "SCD4X Humidity"
@@ -1692,7 +1718,12 @@ climate:
     name: HeatpumpIR Climate
     min_temperature: 18
     max_temperature: 30
+  - platform: midea_ir
+    name: Midea IR
+    use_fahrenheit: true
   - platform: midea
+    on_state:
+      logger.log: "State changed!"
     id: midea_unit
     uart_id: uart0
     name: Midea Climate
@@ -2102,6 +2133,8 @@ display:
       mcp23xxx: mcp23017_hub
       number: 2
     intensity: 3
+    inverted: true
+    length: 4
     lambda: |-
       it.print("1234");
   - platform: pcd8544
@@ -2213,6 +2246,31 @@ display:
     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
@@ -2392,6 +2450,10 @@ globals:
     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
@@ -2410,6 +2472,11 @@ text_sensor:
           id: glob_int
           value: '0'
       - canbus.send:
+          canbus_id: mcp2515_can
+          can_id: 23
+          data: [0x10, 0x20, 0x30]
+      - canbus.send:
+          canbus_id: esp32_internal_can
           can_id: 23
           data: [0x10, 0x20, 0x30]
   - platform: template
@@ -2447,6 +2514,7 @@ rtttl:
 
 canbus:
   - platform: mcp2515
+    id: mcp2515_can
     cs_pin: GPIO17
     can_id: 4
     bit_rate: 50kbps
@@ -2463,9 +2531,48 @@ canbus:
                 lambda: 'return x[0] == 0x11;'
               then:
                 light.toggle: ${roomname}_lights
+  - platform: esp32_can
+    id: esp32_internal_can
+    rx_pin: GPIO04
+    tx_pin: GPIO05
+    can_id: 4
+    bit_rate: 50kbps
+    on_frame:
+      - can_id: 500
+        then:
+          - lambda: |-
+              std::string b(x.begin(), x.end());
+              ESP_LOGD("canid 500", "%s", &b[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 7e71d1ab4e..7920bf3fe3 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -39,6 +39,12 @@ 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
@@ -49,7 +55,10 @@ logger:
   level: DEBUG
 
 deep_sleep:
-  run_duration: 20s
+  run_duration:
+    default: 20s
+    gpio_wakeup_reason: 10s
+    touch_wakeup_reason: 15s
   sleep_duration: 50s
   wakeup_pin: GPIO39
   wakeup_pin_mode: INVERT_WAKEUP
@@ -298,6 +307,15 @@ sensor:
       name: "Wave Mini Pressure"
     tvoc:
       name: "Wave Mini VOC"
+  - platform: ina260
+    address: 0x40
+    current:
+      name: "INA260 Current"
+    power:
+      name: "INA260 Power"
+    bus_voltage:
+      name: "INA260 Voltage"
+    update_interval: 60s
 
 time:
   - platform: homeassistant
@@ -401,6 +419,11 @@ ble_client:
 
 airthings_ble:
 
+ruuvi_ble:
+
+xiaomi_ble:
+
+
 #esp32_ble_beacon:
 #  type: iBeacon
 #  uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98'
@@ -447,6 +470,16 @@ 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
@@ -499,3 +532,9 @@ interval:
 
 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 73e314c94c..607d985704 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -227,7 +227,9 @@ spi:
 
 uart:
   - id: uart1
-    tx_pin: GPIO1
+    tx_pin:
+      number: GPIO1
+      inverted: yes
     rx_pin: GPIO3
     baud_rate: 115200
   - id: uart2
@@ -250,6 +252,12 @@ uart:
     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
@@ -264,6 +272,8 @@ logger:
   level: DEBUG
   esp8266_store_log_strings_in_flash: true
 
+improv_serial:
+
 deep_sleep:
   run_duration: 20s
   sleep_duration: 50s
@@ -348,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
@@ -363,6 +378,10 @@ sensor:
             - 400 -> 500
             - -50 -> -1000
             - -100 -> -10000
+  - platform: cd74hc4067
+    id: cd74hc4067_0
+    number: 0
+    sensor: my_sensor
   - platform: resistance
     sensor: my_sensor
     configuration: DOWNSTREAM
@@ -403,7 +422,7 @@ sensor:
       name: Illuminance
     color_temperature:
       name: Color Temperature
-    integration_time: 700ms
+    integration_time: 614ms
     gain: 60x
   - platform: custom
     lambda: |-
@@ -440,15 +459,30 @@ sensor:
     active_power_b:
       name: ADE7953 Active Power B
       id: ade7953_active_power_b
+  - platform: bl0940
+    uart_id: uart3
+    voltage:
+      name: 'BL0940 Voltage'
+    current:
+      name: 'BL0940 Current'
+    power:
+      name: 'BL0940 Power'
+    energy:
+      name: 'BL0940 Energy'
+    internal_temperature:
+      name: 'BL0940 Internal temperature'
+    external_temperature:
+      name: 'BL0940 External temperature'
   - platform: pzem004t
     uart_id: uart3
     voltage:
-      name: 'PZEM00T Voltage'
+      name: 'PZEM004T Voltage'
     current:
       name: 'PZEM004T Current'
     power:
       name: 'PZEM004T Power'
   - platform: pzemac
+    id: pzemac1
     voltage:
       name: 'PZEMAC Voltage'
     current:
@@ -549,6 +583,18 @@ sensor:
       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:
@@ -735,6 +781,11 @@ binary_sensor:
     on_press:
       then:
         - cover.toggle: time_based_cover
+  - platform: template
+    id: 'pzemac_reset_energy'
+    on_press:
+      then:
+        - pzemac.reset_energy: pzemac1
 
 globals:
   - id: my_global_string
@@ -1150,7 +1201,7 @@ servo:
 ttp229_lsf:
 
 ttp229_bsf:
-  sdo_pin: D0
+  sdo_pin: D2
   scl_pin: D1
 
 sim800l:
@@ -1290,7 +1341,17 @@ fingerprint_grow:
 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
+
+cd74hc4067:
+  pin_s0: GPIO12
+  pin_s1: GPIO13
+  pin_s2: GPIO14
+  pin_s3: GPIO15
diff --git a/tests/test4.yaml b/tests/test4.yaml
index 4f2025ad74..eec1c2eb5e 100644
--- a/tests/test4.yaml
+++ b/tests/test4.yaml
@@ -45,9 +45,11 @@ logger:
   level: DEBUG
 
 web_server:
+  ota: false
   auth:
     username: admin
     password: admin
+  include_internal: true
 
 time:
   - platform: sntp
@@ -59,6 +61,13 @@ tuya:
 pipsolar:
     id: inverter0
 
+sx1509:
+  - id: sx1509_hub
+    address: 0x3E
+
+mcp3204:
+  cs_pin: GPIO23
+
 sensor:
   - platform: homeassistant
     entity_id: sensor.hello_world
@@ -209,6 +218,10 @@ sensor:
       - or:
         - throttle: "20min"
         - delta: 0.02
+  - platform: mcp3204
+    name: "MCP3204 Pin 1"
+    number: 1
+
 #
 # platform sensor.apds9960 requires component apds9960
 #
@@ -308,6 +321,11 @@ binary_sensor:
     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
@@ -401,6 +419,7 @@ display:
     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
@@ -428,7 +447,13 @@ display:
     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
@@ -471,6 +496,12 @@ esp32_camera:
   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
@@ -502,3 +533,11 @@ xpt2046:
             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
index 72df3ed212..d6acbf1e65 100644
--- a/tests/test5.yaml
+++ b/tests/test5.yaml
@@ -10,12 +10,16 @@ esp32:
   framework:
     type: esp-idf
     advanced:
-      ignore_efuse_mac_crc: true 
+      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:
 
@@ -68,6 +72,9 @@ output:
     channel: 0
     max_power: 0.8
 
+  - platform: mcp47a1
+    id: output_mcp47a1
+
 demo:
 
 esp32_ble:
@@ -97,6 +104,8 @@ number:
     max_value: 100
     min_value: 0
     step: 5
+    unit_of_measurement: '%'
+    mode: slider
 
 select:
   - platform: template
@@ -173,3 +182,20 @@ sensor:
     uart_id: uart2
     co2:
       name: CO2 Sensor
+
+  - platform: bmp3xx
+    temperature:
+      name: "BMP Temperature"
+      oversampling: 16x
+    pressure:
+      name: "BMP Pressure"
+    address: 0x77
+    iir_filter: 2X
+
+script:
+  - id: automation_test
+    then:
+      - repeat:
+          count: 5
+          then:
+            - logger.log: "looping!"
diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py
index 32d82b3062..3f32a117ff 100644
--- a/tests/unit_tests/test_codegen.py
+++ b/tests/unit_tests/test_codegen.py
@@ -68,8 +68,7 @@ from esphome import codegen as cg
         "optional",
         "arduino_json_ns",
         "JsonObject",
-        "JsonObjectRef",
-        "JsonObjectConstRef",
+        "JsonObjectConst",
         "Controller",
         "GPIOPin",
     ),
diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py
index e34c7064fa..9e9af52d00 100644
--- a/tests/unit_tests/test_config_validation.py
+++ b/tests/unit_tests/test_config_validation.py
@@ -40,28 +40,6 @@ def test_valid_name__invalid(value):
         config_validation.valid_name(value)
 
 
-@pytest.mark.parametrize("value", ("foo", "bar123", "foo-bar"))
-def test_hostname__valid(value):
-    actual = config_validation.hostname(value)
-
-    assert actual == value
-
-
-@pytest.mark.parametrize("value", ("foo bar", "foobar ", "foo#bar"))
-def test_hostname__invalid(value):
-    with pytest.raises(Invalid):
-        config_validation.hostname(value)
-
-
-def test_hostname__warning(caplog):
-    actual = config_validation.hostname("foo_bar")
-    assert actual == "foo_bar"
-    assert (
-        "Using the '_' (underscore) character in the hostname is discouraged"
-        in caplog.text
-    )
-
-
 @given(one_of(integers(), text()))
 def test_string__valid(value):
     actual = config_validation.string(value)
@@ -88,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)
@@ -97,7 +75,7 @@ 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")
 
 
diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py
index 18e040b0a6..59fcfbff60 100644
--- a/tests/unit_tests/test_wizard.py
+++ b/tests/unit_tests/test_wizard.py
@@ -11,7 +11,7 @@ def default_config():
     return {
         "name": "test-name",
         "platform": "test_platform",
-        "board": "test_board",
+        "board": "esp01_1m",
         "ssid": "test_ssid",
         "psk": "test_psk",
         "password": "",
@@ -105,6 +105,7 @@ 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
@@ -112,7 +113,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch):
 
     # Then
     generated_config = wz.write_file.call_args.args[1]
-    assert f"platform: {default_config['platform']}" in generated_config
+    assert "esp8266:" in generated_config
 
 
 def test_wizard_write_defaults_platform_from_board_esp8266(
@@ -132,7 +133,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266(
 
     # Then
     generated_config = wz.write_file.call_args.args[1]
-    assert "platform: ESP8266" in generated_config
+    assert "esp8266:" in generated_config
 
 
 def test_wizard_write_defaults_platform_from_board_esp32(
@@ -152,7 +153,7 @@ def test_wizard_write_defaults_platform_from_board_esp32(
 
     # Then
     generated_config = wz.write_file.call_args.args[1]
-    assert "platform: ESP32" in generated_config
+    assert "esp32:" in generated_config
 
 
 def test_safe_print_step_prints_step_number_and_description(monkeypatch):