diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3baaf02506..c32dc11b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,10 @@ jobs: file: tests/test6.yaml name: Test tests/test6.yaml pio_cache_key: test6 + - id: test + file: tests/test7.yaml + name: Test tests/test7.yaml + pio_cache_key: test7 - id: pytest name: Run pytest - id: clang-format diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 432aee9938..738bb5a6df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,10 @@ jobs: TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") today="$(date --utc '+%Y%m%d')" TAG="${TAG}${today}" + BRANCH=${GITHUB_REF#refs/heads/} + if [[ "$BRANCH" != "dev" ]]; then + TAG="${TAG}-${BRANCH}" + fi fi echo "tag=${TAG}" >> $GITHUB_OUTPUT # yamllint enable rule:line-length @@ -57,17 +61,30 @@ jobs: run: twine upload dist/* deploy-docker: - name: Build and publish docker containers + name: Build and publish ESPHome ${{ matrix.image.title}} if: github.repository == 'esphome/esphome' permissions: contents: read packages: write runs-on: ubuntu-latest + continue-on-error: ${{ matrix.image.title == 'lint' }} needs: [init] strategy: + fail-fast: false matrix: - arch: [amd64, armv7, aarch64] - build_type: ["ha-addon", "docker", "lint"] + image: + - title: "ha-addon" + suffix: "hassio" + target: "hassio" + baseimg: "hassio" + - title: "docker" + suffix: "" + target: "docker" + baseimg: "docker" + - title: "lint" + suffix: "lint" + target: "lint" + baseimg: "docker" steps: - uses: actions/checkout@v3 - name: Set up Python @@ -92,69 +109,47 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Generate short tags + id: tags + run: | + docker/generate_tags.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --suffix "${{ matrix.image.suffix }}" + - name: Build and push - run: | - docker/build.py \ - --tag "${{ needs.init.outputs.tag }}" \ - --arch "${{ matrix.arch }}" \ - --build-type "${{ matrix.build_type }}" \ - build \ - --push - - deploy-docker-manifest: - if: github.repository == 'esphome/esphome' - permissions: - contents: read - packages: write - runs-on: ubuntu-latest - needs: [init, deploy-docker] - strategy: - matrix: - build_type: ["ha-addon", "docker", "lint"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + uses: docker/build-push-action@v3 with: - python-version: "3.9" - - name: Enable experimental manifest support - run: | - mkdir -p ~/.docker - echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - - name: Log in to docker hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Log in to the GitHub container registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Run manifest - run: | - docker/build.py \ - --tag "${{ needs.init.outputs.tag }}" \ - --build-type "${{ matrix.build_type }}" \ - manifest + context: . + file: ./docker/Dockerfile + platforms: linux/amd64,linux/arm/v7,linux/arm64 + target: ${{ matrix.image.target }} + push: true + # yamllint disable rule:line-length + cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} + cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max + # yamllint enable rule:line-length + tags: ${{ steps.tags.outputs.tags }} + build-args: | + BASEIMGTYPE=${{ matrix.image.baseimg }} + BUILD_VERSION=${{ needs.init.outputs.tag }} deploy-ha-addon-repo: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest needs: [deploy-docker] steps: - - env: - TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} - # yamllint disable rule:line-length - run: | - TAG="${GITHUB_REF#refs/tags/}" - curl \ - -u ":$TOKEN" \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/esphome/home-assistant-addon/actions/workflows/bump-version.yml/dispatches \ - -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" - # yamllint enable rule:line-length + - name: Trigger Workflow + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} + script: | + github.rest.actions.createWorkflowDispatch({ + owner: "esphome", + repo: "home-assistant-addon", + workflow_id: "bump-version.yml", + ref: "main", + inputs: { + version: "${{ github.event.release.tag_name }}", + content: "${{ toJSON(github.event.release.body) }}" + } + }) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6cb3b659c4..a2ba086394 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +38,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: days-before-pr-stale: -1 days-before-pr-close: -1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8c0a22e81..e5ae80da3b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/ambv/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black args: diff --git a/CODEOWNERS b/CODEOWNERS index d30060fe3b..ca1da2f153 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,6 +41,8 @@ esphome/components/ble_client/* @buxtronix esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmp3xx/* @martgras +esphome/components/bp1658cj/* @Cossid +esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @MrEditor97 @@ -69,6 +71,7 @@ esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core @@ -100,9 +103,11 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey +esphome/components/hte501/* @Stock-M esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core esphome/components/i2s_audio/* @jesserockz +esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -111,13 +116,17 @@ esphome/components/integration/* @OttoWinter esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion +esphome/components/key_collector/* @ssieb +esphome/components/key_provider/* @ssieb esphome/components/lcd_menu/* @numo68 +esphome/components/ld2410/* @sebcaps esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny +esphome/components/matrix_keypad/* @ssieb esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger esphome/components/max7219digit/* @rspaargaren @@ -138,6 +147,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey @@ -164,6 +174,8 @@ esphome/components/nfc/* @jesserockz esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core +esphome/components/pca9554/* @hwstar +esphome/components/pcf85063/* @brogon esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie @@ -204,8 +216,12 @@ esphome/components/sgp4x/* @SenexCrenshaw @martgras esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet +esphome/components/sigma_delta_output/* @Cat-Ion esphome/components/sim800l/* @glmnet +esphome/components/sm10bit_base/* @Cossid esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/sm2235/* @Cossid +esphome/components/sm2335/* @Cossid esphome/components/sml/* @alengwenus esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz @@ -234,6 +250,7 @@ esphome/components/switch/* @esphome/core esphome/components/t6615/* @tylermenezes esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet +esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter @@ -258,12 +275,15 @@ esphome/components/uart/* @esphome/core esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter +esphome/components/vbus/* @ssieb esphome/components/version/* @esphome/core esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze +esphome/components/wiegand/* @ssieb esphome/components/wl_134/* @hobbypunk90 +esphome/components/x9c/* @EtienneMD esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/docker/Dockerfile b/docker/Dockerfile index a49ad5a9ef..66b708f522 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,12 +6,15 @@ ARG BASEIMGTYPE=docker # https://github.com/hassio-addons/addon-debian-base/releases -FROM ghcr.io/hassio-addons/debian-base:6.1.3 AS base-hassio +FROM ghcr.io/hassio-addons/debian-base:6.2.0 AS base-hassio # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye FROM debian:bullseye-20221024-slim AS base-docker FROM base-${BASEIMGTYPE} AS base +ARG TARGETARCH +ARG TARGETVARIANT + RUN \ apt-get update \ # Use pinned versions so that we get updates with build caching @@ -36,6 +39,14 @@ ENV \ # Store globally installed pio libs in /piolibs PLATFORMIO_GLOBALLIB_DIR=/piolibs +# Support legacy binaries on Debian multiarch system. There is no "correct" way +# to do this, other than using properly built toolchains... +# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian +RUN \ + if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ + fi + RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ @@ -128,7 +139,7 @@ RUN \ clang-tidy-11=1:11.0.1-2 \ patch=2.7.6-7 \ software-properties-common=0.96.20.2-2.1 \ - nano=5.4-2+deb11u1 \ + nano=5.4-2+deb11u2 \ build-essential=12.9 \ python3-dev=3.9.2-3 \ && rm -rf \ diff --git a/docker/build.py b/docker/build.py index ae977f87c1..47461ddf97 100755 --- a/docker/build.py +++ b/docker/build.py @@ -8,32 +8,49 @@ import re import sys -CHANNEL_DEV = 'dev' -CHANNEL_BETA = 'beta' -CHANNEL_RELEASE = 'release' +CHANNEL_DEV = "dev" +CHANNEL_BETA = "beta" +CHANNEL_RELEASE = "release" CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] -ARCH_AMD64 = 'amd64' -ARCH_ARMV7 = 'armv7' -ARCH_AARCH64 = 'aarch64' +ARCH_AMD64 = "amd64" +ARCH_ARMV7 = "armv7" +ARCH_AARCH64 = "aarch64" ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] -TYPE_DOCKER = 'docker' -TYPE_HA_ADDON = 'ha-addon' -TYPE_LINT = 'lint' +TYPE_DOCKER = "docker" +TYPE_HA_ADDON = "ha-addon" +TYPE_LINT = "lint" TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] parser = argparse.ArgumentParser() -parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag") -parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") -parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") -parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") -subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) +parser.add_argument( + "--tag", + type=str, + required=True, + help="The main docker tag to push to. If a version number also adds latest and/or beta tag", +) +parser.add_argument( + "--arch", choices=ARCHS, required=False, help="The architecture to build for" +) +parser.add_argument( + "--build-type", choices=TYPES, required=True, help="The type of build to run" +) +parser.add_argument( + "--dry-run", action="store_true", help="Don't run any commands, just print them" +) +subparsers = parser.add_subparsers( + help="Action to perform", dest="command", required=True +) build_parser = subparsers.add_parser("build", help="Build the image") build_parser.add_argument("--push", help="Also push the images", action="store_true") -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") +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" +) @dataclass(frozen=True) @@ -49,7 +66,7 @@ class DockerParams: prefix = { TYPE_DOCKER: "esphome/esphome", TYPE_HA_ADDON: "esphome/esphome-hassio", - TYPE_LINT: "esphome/esphome-lint" + TYPE_LINT: "esphome/esphome-lint", }[build_type] build_to = f"{prefix}-{arch}" baseimgtype = { @@ -128,13 +145,21 @@ def main(): # 3. build cmd = [ - "docker", "buildx", "build", - "--build-arg", f"BASEIMGTYPE={params.baseimgtype}", - "--build-arg", f"BUILD_VERSION={args.tag}", - "--cache-from", f"type=registry,ref={cache_img}", - "--file", "docker/Dockerfile", - "--platform", params.platform, - "--target", params.target, + "docker", + "buildx", + "build", + "--build-arg", + f"BASEIMGTYPE={params.baseimgtype}", + "--build-arg", + f"BUILD_VERSION={args.tag}", + "--cache-from", + f"type=registry,ref={cache_img}", + "--file", + "docker/Dockerfile", + "--platform", + params.platform, + "--target", + params.target, ] for img in imgs: cmd += ["--tag", img] @@ -160,9 +185,7 @@ def main(): run_command(*cmd) # 2. Push manifests for target in targets: - run_command( - "docker", "manifest", "push", target - ) + run_command("docker", "manifest", "push", target) if __name__ == "__main__": diff --git a/docker/generate_tags.py b/docker/generate_tags.py new file mode 100755 index 0000000000..71d0735526 --- /dev/null +++ b/docker/generate_tags.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +import re +import os +import argparse +import json + +CHANNEL_DEV = "dev" +CHANNEL_BETA = "beta" +CHANNEL_RELEASE = "release" + +parser = argparse.ArgumentParser() +parser.add_argument( + "--tag", + type=str, + required=True, + help="The main docker tag to push to. If a version number also adds latest and/or beta tag", +) +parser.add_argument( + "--suffix", + type=str, + required=True, + help="The suffix of the tag.", +) + + +def main(): + args = parser.parse_args() + + # detect channel from tag + match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) + major_minor_version = None + if match is None: + channel = CHANNEL_DEV + elif match.group(2) is None: + major_minor_version = match.group(1) + channel = CHANNEL_RELEASE + else: + channel = CHANNEL_BETA + + tags_to_push = [args.tag] + if channel == CHANNEL_DEV: + tags_to_push.append("dev") + elif channel == CHANNEL_BETA: + tags_to_push.append("beta") + elif channel == CHANNEL_RELEASE: + # Additionally push to beta + tags_to_push.append("beta") + tags_to_push.append("latest") + + if major_minor_version: + tags_to_push.append("stable") + tags_to_push.append(major_minor_version) + + suffix = f"-{args.suffix}" if args.suffix else "" + + with open(os.environ["GITHUB_OUTPUT"], "w") as f: + print(f"channel={channel}", file=f) + print(f"image=esphome/esphome{suffix}", file=f) + full_tags = [] + + for tag in tags_to_push: + full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] + full_tags += [f"esphome/esphome{suffix}:{tag}"] + print(f"tags={','.join(full_tags)}", file=f) + + +if __name__ == "__main__": + main() diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh b/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh deleted file mode 100755 index 544787d568..0000000000 --- a/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# This files check if all user configuration requirements are met -# ============================================================================== - -# Check SSL requirements, if enabled -if bashio::config.true 'ssl'; then - if ! bashio::config.has_value 'certfile'; then - bashio::log.fatal 'SSL is enabled, but no certfile was specified.' - bashio::exit.nok - fi - - if ! bashio::config.has_value 'keyfile'; then - bashio::log.fatal 'SSL is enabled, but no keyfile was specified' - bashio::exit.nok - fi - - - certfile="/ssl/$(bashio::config 'certfile')" - keyfile="/ssl/$(bashio::config 'keyfile')" - - if ! bashio::fs.file_exists "${certfile}"; then - if ! bashio::fs.file_exists "${keyfile}"; then - # Both files are missing, let's print a friendlier error message - bashio::log.fatal 'You enabled encrypted connections using the "ssl": true option.' - bashio::log.fatal "However, the SSL files '${certfile}' and '${keyfile}'" - bashio::log.fatal "were not found. If you're using Hass.io on your local network and don't want" - bashio::log.fatal 'to encrypt connections to the ESPHome dashboard, you can manually disable' - bashio::log.fatal 'SSL by setting "ssl" to false."' - bashio::exit.nok - fi - bashio::log.fatal "The configured certfile '${certfile}' was not found." - bashio::exit.nok - fi - - if ! bashio::fs.file_exists "/ssl/$(bashio::config 'keyfile')"; then - bashio::log.fatal "The configured keyfile '${keyfile}' was not found." - bashio::exit.nok - fi -fi diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/20-nginx.sh b/docker/ha-addon-rootfs/etc/cont-init.d/20-nginx.sh deleted file mode 100755 index 107a25c47a..0000000000 --- a/docker/ha-addon-rootfs/etc/cont-init.d/20-nginx.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# Configures NGINX for use with ESPHome -# ============================================================================== - -declare certfile -declare keyfile -declare direct_port -declare ingress_interface -declare ingress_port - -mkdir -p /var/log/nginx - -direct_port=$(bashio::addon.port 6052) -if bashio::var.has_value "${direct_port}"; then - if bashio::config.true 'ssl'; then - certfile=$(bashio::config 'certfile') - keyfile=$(bashio::config 'keyfile') - - mv /etc/nginx/servers/direct-ssl.disabled /etc/nginx/servers/direct.conf - sed -i "s/%%certfile%%/${certfile}/g" /etc/nginx/servers/direct.conf - sed -i "s/%%keyfile%%/${keyfile}/g" /etc/nginx/servers/direct.conf - else - mv /etc/nginx/servers/direct.disabled /etc/nginx/servers/direct.conf - fi - - sed -i "s/%%port%%/${direct_port}/g" /etc/nginx/servers/direct.conf -fi - -ingress_port=$(bashio::addon.ingress_port) -ingress_interface=$(bashio::addon.ip_address) -sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf -sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/30-dirs.sh b/docker/ha-addon-rootfs/etc/cont-init.d/30-dirs.sh deleted file mode 100755 index 1073a2fa45..0000000000 --- a/docker/ha-addon-rootfs/etc/cont-init.d/30-dirs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# This files creates all directories used by esphome -# ============================================================================== - -pio_cache_base=/data/cache/platformio - -mkdir -p "${pio_cache_base}" diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf index c00b4800e8..a1ebb5079a 100644 --- a/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf +++ b/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf @@ -1,9 +1,9 @@ -proxy_http_version 1.1; -proxy_ignore_client_abort off; -proxy_read_timeout 86400s; -proxy_redirect off; -proxy_send_timeout 86400s; -proxy_max_temp_file_size 0; +proxy_http_version 1.1; +proxy_ignore_client_abort off; +proxy_read_timeout 86400s; +proxy_redirect off; +proxy_send_timeout 86400s; +proxy_max_temp_file_size 0; proxy_set_header Accept-Encoding ""; proxy_set_header Connection $connection_upgrade; diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf index 479dfa10f6..debdf83a8c 100644 --- a/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf +++ b/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf @@ -1,5 +1,7 @@ -root /dev/null; -server_name $hostname; +root /dev/null; +server_name $hostname; + +client_max_body_size 512m; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf index 6f15005998..e6789cbb9b 100644 --- a/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf +++ b/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf @@ -1,7 +1,6 @@ -ssl_protocols TLSv1.2; -ssl_prefer_server_ciphers on; -ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA; -ssl_ecdh_curve secp384r1; +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers off; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf b/docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf new file mode 100644 index 0000000000..8e782bdc88 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf @@ -0,0 +1,3 @@ +upstream esphome { + server unix:/var/run/esphome.sock; +} diff --git a/docker/ha-addon-rootfs/etc/nginx/nginx.conf b/docker/ha-addon-rootfs/etc/nginx/nginx.conf index 8ebf572816..497427596d 100644 --- a/docker/ha-addon-rootfs/etc/nginx/nginx.conf +++ b/docker/ha-addon-rootfs/etc/nginx/nginx.conf @@ -2,7 +2,6 @@ daemon off; user root; pid /var/run/nginx.pid; worker_processes 1; -# Hass.io addon log error_log /proc/1/fd/1 error; events { worker_connections 1024; @@ -10,24 +9,22 @@ events { http { include /etc/nginx/includes/mime.types; - access_log stdout; - default_type application/octet-stream; - gzip on; - keepalive_timeout 65; - sendfile on; - server_tokens off; + + access_log off; + default_type application/octet-stream; + gzip on; + keepalive_timeout 65; + sendfile on; + server_tokens off; + + tcp_nodelay on; + tcp_nopush on; map $http_upgrade $connection_upgrade { default upgrade; '' close; } - # Use Hass.io supervisor as resolver - resolver 172.30.32.2; - - upstream esphome { - server unix:/var/run/esphome.sock; - } - + include /etc/nginx/includes/upstream.conf; include /etc/nginx/servers/*.conf; } diff --git a/docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep b/docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep new file mode 100644 index 0000000000..85ad51be5f --- /dev/null +++ b/docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep @@ -0,0 +1 @@ +Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley) diff --git a/docker/ha-addon-rootfs/etc/nginx/servers/direct.disabled b/docker/ha-addon-rootfs/etc/nginx/servers/direct.disabled deleted file mode 100644 index 80300fc6aa..0000000000 --- a/docker/ha-addon-rootfs/etc/nginx/servers/direct.disabled +++ /dev/null @@ -1,12 +0,0 @@ -server { - listen %%port%% default_server; - - include /etc/nginx/includes/server_params.conf; - include /etc/nginx/includes/proxy_params.conf; - # Clear Hass.io Ingress header - proxy_set_header X-HA-Ingress ""; - - location / { - proxy_pass http://esphome; - } -} diff --git a/docker/ha-addon-rootfs/etc/nginx/servers/direct-ssl.disabled b/docker/ha-addon-rootfs/etc/nginx/templates/direct.gtpl similarity index 61% rename from docker/ha-addon-rootfs/etc/nginx/servers/direct-ssl.disabled rename to docker/ha-addon-rootfs/etc/nginx/templates/direct.gtpl index 4ebc435dbb..4fb0ca3f90 100644 --- a/docker/ha-addon-rootfs/etc/nginx/servers/direct-ssl.disabled +++ b/docker/ha-addon-rootfs/etc/nginx/templates/direct.gtpl @@ -1,20 +1,26 @@ server { - listen %%port%% default_server ssl http2; + {{ if not .ssl }} + listen 6052 default_server; + {{ else }} + listen 6052 default_server ssl http2; + {{ end }} include /etc/nginx/includes/server_params.conf; include /etc/nginx/includes/proxy_params.conf; + + {{ if .ssl }} include /etc/nginx/includes/ssl_params.conf; - ssl on; - ssl_certificate /ssl/%%certfile%%; - ssl_certificate_key /ssl/%%keyfile%%; - - # Clear Hass.io Ingress header - proxy_set_header X-HA-Ingress ""; + ssl_certificate /ssl/{{ .certfile }}; + ssl_certificate_key /ssl/{{ .keyfile }}; # Redirect http requests to https on the same port. # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/ error_page 497 https://$http_host$request_uri; + {{ end }} + + # Clear Home Assistant Ingress header + proxy_set_header X-HA-Ingress ""; location / { proxy_pass http://esphome; diff --git a/docker/ha-addon-rootfs/etc/nginx/servers/ingress.conf b/docker/ha-addon-rootfs/etc/nginx/templates/ingress.gtpl similarity index 69% rename from docker/ha-addon-rootfs/etc/nginx/servers/ingress.conf rename to docker/ha-addon-rootfs/etc/nginx/templates/ingress.gtpl index 9d0d2d3e66..105ddde710 100644 --- a/docker/ha-addon-rootfs/etc/nginx/servers/ingress.conf +++ b/docker/ha-addon-rootfs/etc/nginx/templates/ingress.gtpl @@ -1,14 +1,16 @@ server { - listen %%interface%%:%%port%% default_server; + listen 127.0.0.1:{{ .port }} default_server; + listen {{ .interface }}:{{ .port }} default_server; include /etc/nginx/includes/server_params.conf; include /etc/nginx/includes/proxy_params.conf; + # Set Home Assistant Ingress header proxy_set_header X-HA-Ingress "YES"; location / { - # Only allow from Hass.io supervisor allow 172.30.32.2; + allow 127.0.0.1; deny all; proxy_pass http://esphome; diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/esphome b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/esphome new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/nginx b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/nginx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run new file mode 100755 index 0000000000..111157d301 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run @@ -0,0 +1,32 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Add-on: ESPHome +# Sends discovery information to Home Assistant. +# ============================================================================== +declare config +declare port + +# We only disable it when disabled explicitly +if bashio::config.false 'home_assistant_dashboard_integration'; +then + bashio::log.info "Home Assistant discovery is disabled for this add-on." + bashio::exit.ok +fi + +port=$(bashio::addon.ingress_port) + +# Wait for NGINX to become available +bashio::net.wait_for "${port}" "127.0.0.1" 300 + +config=$(\ + bashio::var.json \ + host "127.0.0.1" \ + port "^${port}" \ +) + +if bashio::discovery "esphome" "${config}" > /dev/null; then + bashio::log.info "Successfully send discovery information to Home Assistant." +else + bashio::log.error "Discovery message to Home Assistant failed!" +fi diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/type b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/type new file mode 100644 index 0000000000..bdd22a1850 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/up b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/up new file mode 100644 index 0000000000..c51c2ba820 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/discovery/run diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/dependencies.d/base b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/dependencies.d/base new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish new file mode 100755 index 0000000000..6e0f8fe23a --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish @@ -0,0 +1,26 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: ESPHome +# Take down the S6 supervision tree when ESPHome dashboard fails +# ============================================================================== +declare exit_code +readonly exit_code_container=$( /run/s6-linux-init-container-results/exitcode + fi + [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/docker/ha-addon-rootfs/etc/services.d/esphome/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run similarity index 91% rename from docker/ha-addon-rootfs/etc/services.d/esphome/run rename to docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run index 747c64728e..277f26ea49 100755 --- a/docker/ha-addon-rootfs/etc/services.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run @@ -1,10 +1,19 @@ -#!/usr/bin/with-contenv bashio +#!/command/with-contenv bashio +# shellcheck shell=bash # ============================================================================== # Community Hass.io Add-ons: ESPHome # Runs the ESPHome dashboard # ============================================================================== +readonly pio_cache_base=/data/cache/platformio export ESPHOME_IS_HA_ADDON=true +export PLATFORMIO_GLOBALLIB_DIR=/piolibs + +# we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` +# setting `core_dir` would therefore prevent pio from accessing +export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" +export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" +export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" if bashio::config.true 'leave_front_door_open'; then export DISABLE_HA_AUTHENTICATION=true @@ -30,14 +39,7 @@ else fi fi -pio_cache_base=/data/cache/platformio -# we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` -# setting `core_dir` would therefore prevent pio from accessing -export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" -export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" -export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" - -export PLATFORMIO_GLOBALLIB_DIR=/piolibs +mkdir -p "${pio_cache_base}" bashio::log.info "Starting ESPHome dashboard..." exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/type b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/type new file mode 100644 index 0000000000..5883cff0cd --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/type @@ -0,0 +1 @@ +longrun diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/dependencies.d/base b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/dependencies.d/base new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run new file mode 100755 index 0000000000..2725f56670 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run @@ -0,0 +1,27 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Community Hass.io Add-ons: ESPHome +# Configures NGINX for use with ESPHome +# ============================================================================== +mkdir -p /var/log/nginx + +# Generate Ingress configuration +bashio::var.json \ + interface "$(bashio::addon.ip_address)" \ + port "^$(bashio::addon.ingress_port)" \ + | tempio \ + -template /etc/nginx/templates/ingress.gtpl \ + -out /etc/nginx/servers/ingress.conf + +# Generate direct access configuration, if enabled. +if bashio::var.has_value "$(bashio::addon.port 6052)"; then + bashio::config.require.ssl + bashio::var.json \ + certfile "$(bashio::config 'certfile')" \ + keyfile "$(bashio::config 'keyfile')" \ + ssl "^$(bashio::config 'ssl')" \ + | tempio \ + -template /etc/nginx/templates/direct.gtpl \ + -out /etc/nginx/servers/direct.conf +fi diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/type b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/type new file mode 100644 index 0000000000..bdd22a1850 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/up b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/up new file mode 100644 index 0000000000..b3b5b494b5 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-nginx/run diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/esphome b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/esphome new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/init-nginx b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/init-nginx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/finish b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/finish new file mode 100755 index 0000000000..bbd6d8fecf --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/finish @@ -0,0 +1,25 @@ +#!/command/with-contenv bashio +# ============================================================================== +# Community Hass.io Add-ons: ESPHome +# Take down the S6 supervision tree when NGINX fails +# ============================================================================== +declare exit_code +readonly exit_code_container=$( /run/s6-linux-init-container-results/exitcode + fi + [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/docker/ha-addon-rootfs/etc/services.d/nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run similarity index 73% rename from docker/ha-addon-rootfs/etc/services.d/nginx/run rename to docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run index 8582167b96..e96991cdd1 100755 --- a/docker/ha-addon-rootfs/etc/services.d/nginx/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -1,10 +1,11 @@ -#!/usr/bin/with-contenv bashio +#!/command/with-contenv bashio +# shellcheck shell=bash # ============================================================================== # Community Hass.io Add-ons: ESPHome # Runs the NGINX proxy # ============================================================================== -bashio::log.info "Waiting for dashboard to come up..." +bashio::log.info "Waiting for ESPHome dashboard to come up..." while [[ ! -S /var/run/esphome.sock ]]; do sleep 0.5 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type new file mode 100644 index 0000000000..5883cff0cd --- /dev/null +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type @@ -0,0 +1 @@ +longrun diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/discovery b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/discovery new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/esphome b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/esphome new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-nginx b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-nginx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/ha-addon-rootfs/etc/services.d/esphome/finish b/docker/ha-addon-rootfs/etc/services.d/esphome/finish deleted file mode 100755 index fed449ce61..0000000000 --- a/docker/ha-addon-rootfs/etc/services.d/esphome/finish +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/execlineb -S0 -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# Take down the S6 supervision tree when ESPHome fails -# ============================================================================== - -declare APP_EXIT_CODE=${1} - -if [[ "${APP_EXIT_CODE}" -ne 0 ]] && [[ "${APP_EXIT_CODE}" -ne 256 ]]; then - bashio::log.warning "Halt add-on with exit code ${APP_EXIT_CODE}" - echo "${APP_EXIT_CODE}" > /run/s6-linux-init-container-results/exitcode - exec /run/s6/basedir/bin/halt -fi - -bashio::log.info "Service restart after closing" diff --git a/docker/ha-addon-rootfs/etc/services.d/nginx/finish b/docker/ha-addon-rootfs/etc/services.d/nginx/finish deleted file mode 100755 index 8030841ec8..0000000000 --- a/docker/ha-addon-rootfs/etc/services.d/nginx/finish +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/execlineb -S0 -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# Take down the S6 supervision tree when NGINX fails -# ============================================================================== - -declare APP_EXIT_CODE=${1} - -if [[ "${APP_EXIT_CODE}" -ne 0 ]] && [[ "${APP_EXIT_CODE}" -ne 256 ]]; then - bashio::log.warning "Halt add-on with exit code ${APP_EXIT_CODE}" - echo "${APP_EXIT_CODE}" > /run/s6-linux-init-container-results/exitcode - exec /run/s6/basedir/bin/halt -fi - -bashio::log.info "Service restart after closing" diff --git a/esphome/__main__.py b/esphome/__main__.py index 9b6043ef50..24c2ce1d13 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -339,7 +339,7 @@ def command_config(args, config): _LOGGER.info("Configuration is valid!") if not CORE.verbose: config = strip_default_ids(config) - safe_print(yaml_util.dump(config)) + safe_print(yaml_util.dump(config, args.show_secrets)) return 0 @@ -665,6 +665,9 @@ def parse_args(argv): parser_config.add_argument( "configuration", help="Your YAML configuration file(s).", nargs="+" ) + parser_config.add_argument( + "--show-secrets", help="Show secrets in output.", action="store_true" + ) parser_compile = subparsers.add_parser( "compile", help="Read the configuration and compile a program." diff --git a/esphome/components/a4988/a4988.cpp b/esphome/components/a4988/a4988.cpp index 429fa25648..ae2df3233a 100644 --- a/esphome/components/a4988/a4988.cpp +++ b/esphome/components/a4988/a4988.cpp @@ -46,6 +46,7 @@ void A4988::loop() { return; this->dir_pin_->digital_write(dir == 1); + delayMicroseconds(50); this->step_pin_->digital_write(true); delayMicroseconds(5); this->step_pin_->digital_write(false); diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index beb379db93..c3f3c00c63 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "ads1115"; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; -static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; +static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; // 3300_SPS for ADS1015 void ADS1115Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); @@ -18,6 +18,9 @@ void ADS1115Component::setup() { this->mark_failed(); return; } + + ESP_LOGCONFIG(TAG, "Configuring ADS1115..."); + uint16_t config = 0; // Clear single-shot bit // 0b0xxxxxxxxxxxxxxx @@ -77,6 +80,7 @@ void ADS1115Component::dump_config() { LOG_SENSOR(" ", "Sensor", sensor); ESP_LOGCONFIG(TAG, " Multiplexer: %u", sensor->get_multiplexer()); ESP_LOGCONFIG(TAG, " Gain: %u", sensor->get_gain()); + ESP_LOGCONFIG(TAG, " Resolution: %u", sensor->get_resolution()); } } float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { @@ -127,27 +131,45 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { this->status_set_warning(); return NAN; } + + if (sensor->get_resolution() == ADS1015_12_BITS) { + bool negative = (raw_conversion >> 15) == 1; + + // shift raw_conversion as it's only 12-bits, left justified + raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); + + // check if number was negative in order to keep the sign + if (negative) { + // the number was negative + // 1) set the negative bit back + raw_conversion |= 0x8000; + // 2) reset the former (shifted) negative bit + raw_conversion &= 0xF7FF; + } + } + auto signed_conversion = static_cast(raw_conversion); float millivolts; + float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f; switch (sensor->get_gain()) { case ADS1115_GAIN_6P144: - millivolts = signed_conversion * 0.187500f; + millivolts = (signed_conversion * 6144) / divider; break; case ADS1115_GAIN_4P096: - millivolts = signed_conversion * 0.125000f; + millivolts = (signed_conversion * 4096) / divider; break; case ADS1115_GAIN_2P048: - millivolts = signed_conversion * 0.062500f; + millivolts = (signed_conversion * 2048) / divider; break; case ADS1115_GAIN_1P024: - millivolts = signed_conversion * 0.031250f; + millivolts = (signed_conversion * 1024) / divider; break; case ADS1115_GAIN_0P512: - millivolts = signed_conversion * 0.015625f; + millivolts = (signed_conversion * 512) / divider; break; case ADS1115_GAIN_0P256: - millivolts = signed_conversion * 0.007813f; + millivolts = (signed_conversion * 256) / divider; break; default: millivolts = NAN; diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index 17d5a910d8..0b8bfb339b 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -30,6 +30,11 @@ enum ADS1115Gain { ADS1115_GAIN_0P256 = 0b101, }; +enum ADS1115Resolution { + ADS1115_16_BITS = 16, + ADS1015_12_BITS = 12, +}; + class ADS1115Sensor; class ADS1115Component : public Component, public i2c::I2CDevice { @@ -58,15 +63,17 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol void update() override; void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } void set_gain(ADS1115Gain gain) { gain_ = gain; } - + void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; } float sample() override; uint8_t get_multiplexer() const { return multiplexer_; } uint8_t get_gain() const { return gain_; } + uint8_t get_resolution() const { return resolution_; } protected: ADS1115Component *parent_; ADS1115Multiplexer multiplexer_; ADS1115Gain gain_; + ADS1115Resolution resolution_; }; } // namespace ads1115 diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index 190e641ca3..f0d894e2af 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler from esphome.const import ( CONF_GAIN, CONF_MULTIPLEXER, + CONF_RESOLUTION, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -35,6 +36,12 @@ GAIN = { "0.256": ADS1115Gain.ADS1115_GAIN_0P256, } +ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution") +RESOLUTION = { + "16_BITS": ADS1115Resolution.ADS1115_16_BITS, + "12_BITS": ADS1115Resolution.ADS1015_12_BITS, +} + def validate_gain(value): if isinstance(value, float): @@ -63,6 +70,9 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), cv.Required(CONF_GAIN): validate_gain, + cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( + RESOLUTION, upper=True, space="_" + ), } ) .extend(cv.polling_component_schema("60s")) @@ -77,5 +87,6 @@ async def to_code(config): cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_resolution(config[CONF_RESOLUTION])) cg.add(paren.register_sensor(var)) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 87d72254e8..ce9f057496 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -117,7 +117,7 @@ async def to_code(config): data[pos] = rgb & 255 pos += 1 - elif config[CONF_TYPE] == "BINARY": + elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: width8 = ((width + 7) // 8) * 8 data = [0 for _ in range((height * width8 // 8) * frames)] for frameIndex in range(frames): diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 05091f3f7d..1c6ec5c14a 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -23,7 +23,7 @@ void APDS9960::setup() { return; } - if (id != 0xAB && id != 0x9C) { // APDS9960 all should have one of these IDs + if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs this->error_code_ = WRONG_ID; this->mark_failed(); return; diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e1bc7b0a57..ffb3bcb07e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -206,6 +206,8 @@ message DeviceInfoResponse { uint32 bluetooth_proxy_version = 11; string manufacturer = 12; + + string friendly_name = 13; } message ListEntitiesRequest { @@ -785,6 +787,7 @@ enum ClimateFanMode { CLIMATE_FAN_MIDDLE = 6; CLIMATE_FAN_FOCUS = 7; CLIMATE_FAN_DIFFUSE = 8; + CLIMATE_FAN_QUIET = 9; } enum ClimateSwingMode { CLIMATE_SWING_OFF = 0; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index aac58587d1..65659941d6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -930,6 +930,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; resp.uses_password = this->parent_->uses_password(); resp.name = App.get_name(); + resp.friendly_name = App.get_friendly_name(); resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index b19a55764f..c18e045a99 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -616,6 +616,9 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { struct iovec iov[2]; iov[0].iov_base = header; iov[0].iov_len = 3; + if (len == 0) { + return write_raw_(iov, 1); + } iov[1].iov_base = const_cast(data); iov[1].iov_len = len; @@ -913,6 +916,9 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay struct iovec iov[2]; iov[0].iov_base = &header[0]; iov[0].iov_len = header.size(); + if (payload_len == 0) { + return write_raw_(iov, 1); + } iov[1].iov_base = const_cast(payload); iov[1].iov_len = payload_len; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f108d38e8f..9df05d2978 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -235,6 +235,8 @@ template<> const char *proto_enum_to_string(enums::Climat return "CLIMATE_FAN_FOCUS"; case enums::CLIMATE_FAN_DIFFUSE: return "CLIMATE_FAN_DIFFUSE"; + case enums::CLIMATE_FAN_QUIET: + return "CLIMATE_FAN_QUIET"; default: return "UNKNOWN"; } @@ -628,6 +630,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->manufacturer = value.as_string(); return true; } + case 13: { + this->friendly_name = value.as_string(); + return true; + } default: return false; } @@ -645,6 +651,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(10, this->webserver_port); buffer.encode_uint32(11, this->bluetooth_proxy_version); buffer.encode_string(12, this->manufacturer); + buffer.encode_string(13, this->friendly_name); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -699,6 +706,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" manufacturer: "); out.append("'").append(this->manufacturer).append("'"); out.append("\n"); + + out.append(" friendly_name: "); + out.append("'").append(this->friendly_name).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8a78f1ad03..2db1c6fafa 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -99,6 +99,7 @@ enum ClimateFanMode : uint32_t { CLIMATE_FAN_MIDDLE = 6, CLIMATE_FAN_FOCUS = 7, CLIMATE_FAN_DIFFUSE = 8, + CLIMATE_FAN_QUIET = 9, }; enum ClimateSwingMode : uint32_t { CLIMATE_SWING_OFF = 0, @@ -276,6 +277,7 @@ class DeviceInfoResponse : public ProtoMessage { uint32_t webserver_port{0}; uint32_t bluetooth_proxy_version{0}; std::string manufacturer{}; + std::string friendly_name{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp index 02ac289e0e..e272241040 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.cpp +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -37,9 +37,13 @@ void BedJetFan::control(const fan::FanCall &call) { // ignore speed changes if not on or turning on if (this->state && call.get_speed().has_value()) { - this->speed = *call.get_speed(); - this->parent_->set_fan_index(this->speed); - did_change = true; + auto speed = *call.get_speed(); + if (speed >= 1) { + this->speed = speed; + // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1 + this->parent_->set_fan_index(this->speed - 1); + did_change = true; + } } if (did_change) { @@ -57,8 +61,9 @@ void BedJetFan::on_status(const BedjetStatusPacket *data) { did_change = true; } - if (data->fan_step != this->speed) { - this->speed = data->fan_step; + // BedjetStatusPacket.fan_step is in range 0-19, but Fan.speed wants 1-20. + if (data->fan_step + 1 != this->speed) { + this->speed = data->fan_step + 1; did_change = true; } diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index d8124f5dc3..786fc01d28 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -88,7 +88,10 @@ void BME280Component::setup() { // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. - this->component_state_ &= ~COMPONENT_STATE_FAILED; + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + } if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/bmp280/bmp280.cpp b/esphome/components/bmp280/bmp280.cpp index bda34e6c2b..a5b2517893 100644 --- a/esphome/components/bmp280/bmp280.cpp +++ b/esphome/components/bmp280/bmp280.cpp @@ -1,4 +1,5 @@ #include "bmp280.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -11,8 +12,11 @@ static const uint8_t BMP280_REGISTER_CONTROL = 0xF4; static const uint8_t BMP280_REGISTER_CONFIG = 0xF5; static const uint8_t BMP280_REGISTER_PRESSUREDATA = 0xF7; static const uint8_t BMP280_REGISTER_TEMPDATA = 0xFA; +static const uint8_t BMP280_REGISTER_RESET = 0xE0; static const uint8_t BMP280_MODE_FORCED = 0b01; +static const uint8_t BMP280_SOFT_RESET = 0xB6; +static const uint8_t BMP280_STATUS_IM_UPDATE = 0b01; inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } @@ -66,6 +70,28 @@ void BMP280Component::setup() { return; } + // Send a soft reset. + if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { + this->mark_failed(); + return; + } + // Wait until the NVM data has finished loading. + uint8_t status; + uint8_t retry = 5; + do { + delay(2); + if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { + ESP_LOGW(TAG, "Error reading status register."); + this->mark_failed(); + return; + } + } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); + if (status & BMP280_STATUS_IM_UPDATE) { + ESP_LOGW(TAG, "Timeout loading NVM."); + this->mark_failed(); + return; + } + // Read calibration this->calibration_.t1 = this->read_u16_le_(0x88); this->calibration_.t2 = this->read_s16_le_(0x8A); diff --git a/esphome/components/bp1658cj/__init__.py b/esphome/components/bp1658cj/__init__.py new file mode 100644 index 0000000000..8388b16df9 --- /dev/null +++ b/esphome/components/bp1658cj/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels" +CONF_MAX_POWER_WHITE_CHANNELS = "max_power_white_channels" + +AUTO_LOAD = ["output"] +bp1658cj_ns = cg.esphome_ns.namespace("bp1658cj") +BP1658CJ = bp1658cj_ns.class_("BP1658CJ", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BP1658CJ), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_MAX_POWER_COLOR_CHANNELS, default=2): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_MAX_POWER_WHITE_CHANNELS, default=4): cv.int_range( + min=0, max=15 + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS])) + cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS])) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp new file mode 100644 index 0000000000..5b9e4a5a2c --- /dev/null +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -0,0 +1,110 @@ +#include "bp1658cj.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bp1658cj { + +static const char *const TAG = "bp1658cj"; + +static const uint8_t BP1658CJ_MODEL_ID = 0x80; +static const uint8_t BP1658CJ_ADDR_STANDBY = 0x0; +static const uint8_t BP1658CJ_ADDR_START_3CH = 0x10; +static const uint8_t BP1658CJ_ADDR_START_2CH = 0x20; +static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30; + +void BP1658CJ::setup() { + ESP_LOGCONFIG(TAG, "Setting up BP1658CJ Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(false); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(false); + this->pwm_amounts_.resize(5, 0); +} +void BP1658CJ::dump_config() { + ESP_LOGCONFIG(TAG, "BP1658CJ:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Color Channels Max Power: %u", this->max_power_color_channels_); + ESP_LOGCONFIG(TAG, " White Channels Max Power: %u", this->max_power_white_channels_); +} + +void BP1658CJ::loop() { + if (!this->update_) + return; + + uint8_t data[12]; + if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Off / Sleep + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY; + for (int i = 1; i < 12; i++) + data[i] = 0; + this->write_buffer_(data, 12); + } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { + // Only data on white channels + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_2CH; + data[1] = 0 << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 12); + } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Only data on RGB channels + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_3CH; + data[1] = this->max_power_color_channels_ << 4 | 0; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 12); + } else { + // All channels + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_5CH; + data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 12); + } + + this->update_ = false; +} + +void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} +void BP1658CJ::write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); +} + +void BP1658CJ::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); +} + +void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); +} + +} // namespace bp1658cj +} // namespace esphome diff --git a/esphome/components/bp1658cj/bp1658cj.h b/esphome/components/bp1658cj/bp1658cj.h new file mode 100644 index 0000000000..778f49b3e9 --- /dev/null +++ b/esphome/components/bp1658cj/bp1658cj.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace bp1658cj { + +class BP1658CJ : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_max_power_color_channels(uint8_t max_power_color_channels) { + max_power_color_channels_ = max_power_color_channels; + } + void set_max_power_white_channels(uint8_t max_power_white_channels) { + max_power_white_channels_ = max_power_white_channels; + } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(BP1658CJ *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0x3FF); + this->parent_->set_channel_value_(this->channel_, amount); + } + + BP1658CJ *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint16_t value); + void write_bit_(bool value); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t max_power_color_channels_{4}; + uint8_t max_power_white_channels_{6}; + uint8_t update_channel_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace bp1658cj +} // namespace esphome diff --git a/esphome/components/bp1658cj/output.py b/esphome/components/bp1658cj/output.py new file mode 100644 index 0000000000..3b89518621 --- /dev/null +++ b/esphome/components/bp1658cj/output.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import BP1658CJ + +DEPENDENCIES = ["bp1658cj"] + +Channel = BP1658CJ.class_("Channel", output.FloatOutput) + +CONF_BP1658CJ_ID = "bp1658cj_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_BP1658CJ_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/bp5758d/__init__.py b/esphome/components/bp5758d/__init__.py new file mode 100644 index 0000000000..eeeab2a1bd --- /dev/null +++ b/esphome/components/bp5758d/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +AUTO_LOAD = ["output"] +bp5758d_ns = cg.esphome_ns.namespace("bp5758d") +BP5758D = bp5758d_ns.class_("BP5758D", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BP5758D), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp new file mode 100644 index 0000000000..111fd6b68e --- /dev/null +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -0,0 +1,147 @@ +#include "bp5758d.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bp5758d { + +static const char *const TAG = "bp5758d"; + +static const uint8_t BP5758D_MODEL_ID = 0b10000000; +static const uint8_t BP5758D_ADDR_STANDBY = 0b00000000; +// Note, channel start address seems ambiguous and mis-translated. +// Documentation states all are "invalid sleep" +// All 3 values appear to activate all 5 channels. Using the mapping +// from BP1658CJ ordering since it won't break anything. +static const uint8_t BP5758D_ADDR_START_3CH = 0b00010000; +static const uint8_t BP5758D_ADDR_START_2CH = 0b00100000; +static const uint8_t BP5758D_ADDR_START_5CH = 0b00110000; +static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111; + +void BP5758D::setup() { + ESP_LOGCONFIG(TAG, "Setting up BP5758D Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(false); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(false); + this->channel_current_.resize(5, 0); + this->pwm_amounts_.resize(5, 0); +} +void BP5758D::dump_config() { + ESP_LOGCONFIG(TAG, "BP5758D:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); +} + +void BP5758D::loop() { + if (!this->update_) + return; + + uint8_t data[17]; + if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Off / Sleep + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; + for (int i = 1; i < 16; i++) + data[i] = 0; + this->write_buffer_(data, 17); + } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { + // Only data on white channels + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_2CH; + data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT; + data[2] = 0; + data[3] = 0; + data[4] = 0; + data[5] = this->pwm_amounts_[3] > 0 ? correct_current_level_bits_(this->channel_current_[3]) : 0; + data[6] = this->pwm_amounts_[4] > 0 ? correct_current_level_bits_(this->channel_current_[4]) : 0; + for (int i = 7, j = 0; i <= 15; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 17); + } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Only data on RGB channels + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_3CH; + data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT; + data[2] = this->pwm_amounts_[0] > 0 ? correct_current_level_bits_(this->channel_current_[0]) : 0; + data[3] = this->pwm_amounts_[1] > 0 ? correct_current_level_bits_(this->channel_current_[1]) : 0; + data[4] = this->pwm_amounts_[2] > 0 ? correct_current_level_bits_(this->channel_current_[2]) : 0; + data[5] = 0; + data[6] = 0; + for (int i = 7, j = 0; i <= 15; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 17); + } else { + // All channels + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_5CH; + data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT; + data[2] = this->pwm_amounts_[0] > 0 ? correct_current_level_bits_(this->channel_current_[0]) : 0; + data[3] = this->pwm_amounts_[1] > 0 ? correct_current_level_bits_(this->channel_current_[1]) : 0; + data[4] = this->pwm_amounts_[2] > 0 ? correct_current_level_bits_(this->channel_current_[2]) : 0; + data[5] = this->pwm_amounts_[3] > 0 ? correct_current_level_bits_(this->channel_current_[3]) : 0; + data[6] = this->pwm_amounts_[4] > 0 ? correct_current_level_bits_(this->channel_current_[4]) : 0; + for (int i = 7, j = 0; i <= 15; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 17); + } + + this->update_ = false; +} + +uint8_t BP5758D::correct_current_level_bits_(uint8_t current) { + // Anything below 64 uses normal bitmapping. + if (current < 64) { + return current; + } + + // Anything above 63 needs to be offset by +34 because the driver remaps bit 7 (normally 64) to 30. + // (no idea why(!) but it is documented) + // Example: + // integer 64 would normally put out 0b01000000 but here 0b01000000 = 30 whereas everything lower + // is normal, so we add 34 to the integer where + // integer 98 = 0b01100010 which is 30 (7th bit adjusted) + 34 (1st-6th bits). + return current + 34; +} + +void BP5758D::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} + +void BP5758D::set_channel_current_(uint8_t channel, uint8_t current) { this->channel_current_[channel] = current; } + +void BP5758D::write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); +} + +void BP5758D::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); +} + +void BP5758D::write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); +} + +} // namespace bp5758d +} // namespace esphome diff --git a/esphome/components/bp5758d/bp5758d.h b/esphome/components/bp5758d/bp5758d.h new file mode 100644 index 0000000000..cc7cc3d5f8 --- /dev/null +++ b/esphome/components/bp5758d/bp5758d.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace bp5758d { + +class BP5758D : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(BP5758D *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + void set_current(uint8_t current) { current_ = current; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0x3FF); + // We're enforcing channels start at 1 to mach OUT1-OUT5, we must adjust + // to our 0-based array internally here by subtracting 1. + this->parent_->set_channel_value_(this->channel_ - 1, amount); + this->parent_->set_channel_current_(this->channel_ - 1, this->current_); + } + + BP5758D *parent_; + uint8_t channel_; + uint8_t current_; + }; + + protected: + uint8_t correct_current_level_bits_(uint8_t current); + void set_channel_value_(uint8_t channel, uint16_t value); + void set_channel_current_(uint8_t channel, uint8_t current); + void write_bit_(bool value); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t update_channel_; + std::vector channel_current_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace bp5758d +} // namespace esphome diff --git a/esphome/components/bp5758d/output.py b/esphome/components/bp5758d/output.py new file mode 100644 index 0000000000..d0083fb33f --- /dev/null +++ b/esphome/components/bp5758d/output.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_CURRENT +from . import BP5758D + +DEPENDENCIES = ["bp5758d"] + +Channel = BP5758D.class_("Channel", output.FloatOutput) + +CONF_BP5758D_ID = "bp5758d_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_BP5758D_ID): cv.use_id(BP5758D), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=5), + cv.Optional(CONF_CURRENT, default=10): cv.int_range(min=0, max=90), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_BP5758D_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_current(config[CONF_CURRENT])) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8a3cd38444..eaa87afcb1 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -22,6 +22,8 @@ from esphome.const import ( CONF_MODE_STATE_TOPIC, CONF_ON_STATE, CONF_PRESET, + CONF_PRESET_COMMAND_TOPIC, + CONF_PRESET_STATE_TOPIC, CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, @@ -73,6 +75,7 @@ CLIMATE_FAN_MODES = { "MIDDLE": ClimateFanMode.CLIMATE_FAN_MIDDLE, "FOCUS": ClimateFanMode.CLIMATE_FAN_FOCUS, "DIFFUSE": ClimateFanMode.CLIMATE_FAN_DIFFUSE, + "QUIET": ClimateFanMode.CLIMATE_FAN_QUIET, } validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) @@ -142,6 +145,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_MODE_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -216,7 +225,12 @@ async def setup_climate_core_(var, config): cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC])) if CONF_MODE_STATE_TOPIC in config: cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC])) - + if CONF_PRESET_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_preset_command_topic(config[CONF_PRESET_COMMAND_TOPIC]) + ) + if CONF_PRESET_STATE_TOPIC in config: + cg.add(mqtt_.set_custom_preset_state_topic(config[CONF_PRESET_STATE_TOPIC])) if CONF_SWING_MODE_COMMAND_TOPIC in config: cg.add( mqtt_.set_custom_swing_mode_command_topic( diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 512dbdd6dd..e1611d2fa9 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -174,6 +174,8 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { this->set_fan_mode(CLIMATE_FAN_FOCUS); } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { this->set_fan_mode(CLIMATE_FAN_DIFFUSE); + } else if (str_equals_case_insensitive(fan_mode, "QUIET")) { + this->set_fan_mode(CLIMATE_FAN_QUIET); } else { if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { this->custom_fan_mode_ = fan_mode; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index e46159a750..794f45ccd6 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -62,6 +62,8 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { return LOG_STR("FOCUS"); case climate::CLIMATE_FAN_DIFFUSE: return LOG_STR("DIFFUSE"); + case climate::CLIMATE_FAN_QUIET: + return LOG_STR("QUIET"); default: return LOG_STR("UNKNOWN"); } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 139400a08a..c5245812c7 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -62,6 +62,8 @@ enum ClimateFanMode : uint8_t { CLIMATE_FAN_FOCUS = 7, /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, + /// The fan mode is set to Quiet + CLIMATE_FAN_QUIET = 9, }; /// Enum for all modes a climate swing can be in diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 3ec51bc3c2..9da9bb7374 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -28,7 +28,7 @@ namespace climate { * - supports action - if the climate device supports reporting the active * current action of the device with the action property. * - supports fan modes - optionally, if it has a fan which can be configured in different ways: - * - on, off, auto, high, medium, low, middle, focus, diffuse + * - on, off, auto, high, medium, low, middle, focus, diffuse, quiet * - supports swing modes - optionally, if it has a swing which can be configured in different ways: * - off, both, vertical, horizontal * diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 4742c77785..e1bd6a7f08 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -1,14 +1,17 @@ +import base64 +import secrets from pathlib import Path +from typing import Optional + import requests import esphome.codegen as cg import esphome.config_validation as cv +from esphome import git from esphome.components.packages import validate_source_shorthand -from esphome.const import CONF_WIFI +from esphome.const import CONF_REF, CONF_WIFI from esphome.wizard import wizard_file from esphome.yaml_util import dump -from esphome import git - dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") @@ -21,19 +24,32 @@ CODEOWNERS = ["@esphome/core"] def validate_import_url(value): value = cv.string_strict(value) value = cv.Length(max=255)(value) - # ignore result, only check if it's a valid shorthand validate_source_shorthand(value) return value +def validate_full_url(config): + if not config[CONF_IMPORT_FULL_CONFIG]: + return config + source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL]) + if CONF_REF not in source: + raise cv.Invalid( + "Must specify a ref (branch or tag) to import from when importing full config" + ) + return config + + CONF_PACKAGE_IMPORT_URL = "package_import_url" CONF_IMPORT_FULL_CONFIG = "import_full_config" -CONFIG_SCHEMA = cv.Schema( - { - cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url, - cv.Optional(CONF_IMPORT_FULL_CONFIG, default=False): cv.boolean, - } +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url, + cv.Optional(CONF_IMPORT_FULL_CONFIG, default=False): cv.boolean, + } + ), + validate_full_url, ) WIFI_CONFIG = """ @@ -49,11 +65,17 @@ async def to_code(config): url = config[CONF_PACKAGE_IMPORT_URL] if config[CONF_IMPORT_FULL_CONFIG]: url += "?full_config" - cg.add(dashboard_import_ns.set_package_import_url(config[CONF_PACKAGE_IMPORT_URL])) + cg.add(dashboard_import_ns.set_package_import_url(url)) def import_config( - path: str, name: str, project_name: str, import_url: str, network: str = CONF_WIFI + path: str, + name: str, + friendly_name: Optional[str], + project_name: str, + import_url: str, + network: str = CONF_WIFI, + encryption: bool = False, ) -> None: p = Path(path) @@ -61,14 +83,21 @@ def import_config( raise FileExistsError if project_name == "esphome.web": + kwargs = { + "name": name, + "friendly_name": friendly_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", + } + if encryption: + noise_psk = secrets.token_bytes(32) + key = base64.b64encode(noise_psk).decode() + kwargs["api_encryption_key"] = key + 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", - ), + wizard_file(**kwargs), encoding="utf8", ) else: @@ -85,14 +114,21 @@ def import_config( p.write_text(req.text, encoding="utf8") else: + substitutions = {"name": name} + esphome_core = {"name": "${name}", "name_add_mac_suffix": False} + if friendly_name: + substitutions["friendly_name"] = friendly_name + esphome_core["friendly_name"] = "${friendly_name}" config = { - "substitutions": {"name": name}, + "substitutions": substitutions, "packages": {project_name: import_url}, - "esphome": { - "name": "${name}", - "name_add_mac_suffix": False, - }, + "esphome": esphome_core, } + if encryption: + noise_psk = secrets.token_bytes(32) + key = base64.b64encode(noise_psk).decode() + config["api"] = {"encryption": {"key": key}} + output = dump(config) if network == CONF_WIFI: diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 0cf48dd4ee..1ba80aabf5 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -111,6 +111,7 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE, + climate::CLIMATE_FAN_QUIET, }); traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); traits.set_supported_swing_modes({ diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 97c08dae24..cfd73509ea 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -452,7 +452,7 @@ int Font::match_next_glyph(const char *str, int *match_length) { } void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { *baseline = this->baseline_; - *height = this->bottom_; + *height = this->height_; int i = 0; int min_x = 0; bool has_char = false; @@ -483,7 +483,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *width = x - min_x; } const std::vector &Font::get_glyphs() const { return this->glyphs_; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int bottom) : baseline_(baseline), bottom_(bottom) { +Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { for (int i = 0; i < data_nr; ++i) glyphs_.emplace_back(data + i); } @@ -527,6 +527,7 @@ int Image::get_height() const { return this->height_; } ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height, ImageType type) : width_(width), height_(height), type_(type), data_start_(data_start) {} +int Image::get_current_frame() const { return 0; } bool Animation::get_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 41052b3ffd..b652989067 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -448,18 +448,20 @@ class Font { * @param baseline The y-offset from the top of the text to the baseline. * @param bottom The y-offset from the top of the text to the bottom (i.e. height). */ - Font(const GlyphData *data, int data_nr, int baseline, int bottom); + Font(const GlyphData *data, int data_nr, int baseline, int height); int match_next_glyph(const char *str, int *match_length); void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); + inline int get_baseline() { return this->baseline_; } + inline int get_height() { return this->height_; } const std::vector &get_glyphs() const; protected: std::vector glyphs_; int baseline_; - int bottom_; + int height_; }; class Image { @@ -473,6 +475,8 @@ class Image { int get_height() const; ImageType get_type() const; + virtual int get_current_frame() const; + protected: int width_; int height_; @@ -489,7 +493,7 @@ class Animation : public Image { Color get_grayscale_pixel(int x, int y) const override; int get_animation_frame_count() const; - int get_current_frame() const; + int get_current_frame() const override; void next_frame(); void prev_frame(); diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index eb66737fdb..d7326cdc65 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_ON_VALUE, CONF_COMMAND, + CONF_CUSTOM, CONF_NUMBER, CONF_FORMAT, CONF_MODE, @@ -32,7 +33,6 @@ CONF_BACK = "back" CONF_TEXT = "text" CONF_SELECT = "select" CONF_SWITCH = "switch" -CONF_CUSTOM = "custom" CONF_ITEMS = "items" CONF_ON_TEXT = "on_text" CONF_OFF_TEXT = "off_text" diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 284733cca6..f4f8305ba6 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CODEOWNERS = ["@glmnet", "@zuidwijk"] +MULTI_CONF = True + DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor", "text_sensor"] diff --git a/esphome/components/ee895/__init__.py b/esphome/components/ee895/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp new file mode 100644 index 0000000000..a7186ffbbc --- /dev/null +++ b/esphome/components/ee895/ee895.cpp @@ -0,0 +1,115 @@ +#include "ee895.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ee895 { + +static const char *const TAG = "ee895"; + +static const uint16_t CRC16_ONEWIRE_START = 0xFFFF; +static const uint8_t FUNCTION_CODE_READ = 0x03; +static const uint16_t SERIAL_NUMBER = 0x0000; +static const uint16_t TEMPERATURE_ADDRESS = 0x03EA; +static const uint16_t CO2_ADDRESS = 0x0424; +static const uint16_t PRESSURE_ADDRESS = 0x04B0; + +void EE895Component::setup() { + uint16_t crc16_check = 0; + ESP_LOGCONFIG(TAG, "Setting up EE895..."); + write_command_(SERIAL_NUMBER, 8); + uint8_t serial_number[20]; + this->read(serial_number, 20); + + crc16_check = (serial_number[19] << 8) + serial_number[18]; + if (crc16_check != calc_crc16_(serial_number, 19)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str()); +} + +void EE895Component::dump_config() { + ESP_LOGCONFIG(TAG, "EE895:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with EE895 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); +} + +float EE895Component::get_setup_priority() const { return setup_priority::DATA; } + +void EE895Component::update() { + write_command_(TEMPERATURE_ADDRESS, 2); + this->set_timeout(50, [this]() { + float temperature = read_float_(); + + write_command_(CO2_ADDRESS, 2); + float co2 = read_float_(); + + write_command_(PRESSURE_ADDRESS, 2); + float pressure = read_float_(); + ESP_LOGD(TAG, "Got temperature=%.1f°C co2=%.0fppm pressure=%.1f%mbar", temperature, co2, pressure); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + }); +} + +void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { + uint8_t address[7]; + uint16_t crc16 = 0; + address[0] = FUNCTION_CODE_READ; + address[1] = (addr >> 8) & 0xFF; + address[2] = addr & 0xFF; + address[3] = (reg_cnt >> 8) & 0xFF; + address[4] = reg_cnt & 0xFF; + crc16 = calc_crc16_(address, 6); + address[5] = crc16 & 0xFF; + address[6] = (crc16 >> 8) & 0xFF; + this->write(address, 7, true); +} + +float EE895Component::read_float_() { + uint16_t crc16_check = 0; + uint8_t i2c_response[8]; + this->read(i2c_response, 8); + crc16_check = (i2c_response[7] << 8) + i2c_response[6]; + if (crc16_check != calc_crc16_(i2c_response, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return 0; + } + uint32_t x = encode_uint32(i2c_response[4], i2c_response[5], i2c_response[2], i2c_response[3]); + float value; + memcpy(&value, &x, sizeof(value)); // convert uin32_t IEEE-754 format to float + return value; +} + +uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { + uint8_t crc_check_buf[22]; + for (int i = 0; i < len; i++) { + crc_check_buf[i + 1] = buf[i]; + } + crc_check_buf[0] = this->address_; + return crc16(crc_check_buf, len); +} +} // namespace ee895 +} // namespace esphome diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h new file mode 100644 index 0000000000..83bd7c6e82 --- /dev/null +++ b/esphome/components/ee895/ee895.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ee895 { + +/// This class implements support for the ee895 of temperature i2c sensors. +class EE895Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + void write_command_(uint16_t addr, uint16_t reg_cnt); + float read_float_(); + uint16_t calc_crc16_(const uint8_t buf[], uint8_t len); + sensor::Sensor *co2_sensor_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace ee895 +} // namespace esphome diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py new file mode 100644 index 0000000000..d06f9ca02f --- /dev/null +++ b/esphome/components/ee895/sensor.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + CONF_CO2, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_HECTOPASCAL, + UNIT_CELSIUS, + ICON_MOLECULE_CO2, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +ee895_ns = cg.esphome_ns.namespace("ee895") +EE895Component = ee895_ns.class_("EE895Component", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EE895Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5F)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3989b62842..f30fa9a7b2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -356,9 +356,14 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_MAC_CRC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_MAC_CRC") - add_idf_sdkconfig_option( - "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) + if (framework_ver.major, framework_ver.minor) >= (4, 4): + add_idf_sdkconfig_option( + "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) + else: + add_idf_sdkconfig_option( + "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) cg.add_define( "USE_ESP_IDF_VERSION_CODE", diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index d0805e4dd2..aafdf80726 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -95,7 +95,7 @@ void ESP32InternalGPIOPin::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)) { + if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; } else if (flags & gpio::FLAG_PULLUP) { pull_mode = GPIO_PULLUP_ONLY; @@ -128,7 +128,7 @@ 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)) { + if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; } else if (flags & gpio::FLAG_PULLUP) { pull_mode = GPIO_PULLUP_ONLY; diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index b97a5e0457..7848d1d552 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -123,11 +123,8 @@ def validate_gpio_pin(value): def validate_supports(value): mode = value[CONF_MODE] - is_input = mode[CONF_INPUT] is_output = mode[CONF_OUTPUT] is_open_drain = mode[CONF_OPEN_DRAIN] - is_pullup = mode[CONF_PULLUP] - is_pulldown = mode[CONF_PULLDOWN] variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: raise cv.Invalid(f"Unsupported ESP32 variant {variant}") @@ -138,26 +135,6 @@ def validate_supports(value): ) value = _esp32_validations[variant].usage_validation(value) - if CORE.using_arduino: - # (input, output, open_drain, pullup, pulldown) - supported_modes = { - # INPUT - (True, False, False, False, False), - # OUTPUT - (False, True, False, False, False), - # INPUT_PULLUP - (True, False, False, True, False), - # INPUT_PULLDOWN - (True, False, False, False, True), - # OUTPUT_OPEN_DRAIN - (False, True, True, False, False), - } - key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown) - if key not in supported_modes: - raise cv.Invalid( - "This pin mode is not supported on ESP32 for arduino frameworks", - [CONF_MODE], - ) return value diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 4b5c741ad9..3dc4af1058 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -2,15 +2,23 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant, const DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] + +CONF_BLE_ID = "ble_id" + +NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) +GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") +GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") +GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") + CONFIG_SCHEMA = cv.Schema( { @@ -19,6 +27,15 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +def validate_variant(_): + variant = get_esp32_variant() + if variant in NO_BLUTOOTH_VARIANTS: + raise cv.Invalid(f"{variant} does not support Bluetooth") + + +FINAL_VALIDATE_SCHEMA = validate_variant + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ecd591d169..160084b913 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -4,13 +4,13 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -#include -#include -#include #include -#include -#include +#include #include +#include +#include +#include +#include #ifdef USE_ARDUINO #include @@ -31,24 +31,17 @@ void ESP32BLE::setup() { return; } +#ifdef USE_ESP32_BLE_SERVER this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) this->advertising_->set_scan_response(true); this->advertising_->set_min_preferred_interval(0x06); this->advertising_->start(); +#endif // USE_ESP32_BLE_SERVER ESP_LOGD(TAG, "BLE setup complete"); } -void ESP32BLE::mark_failed() { - Component::mark_failed(); -#ifdef USE_ESP32_BLE_SERVER - if (this->server_ != nullptr) { - this->server_->mark_failed(); - } -#endif -} - bool ESP32BLE::ble_setup_() { esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { @@ -100,13 +93,16 @@ bool ESP32BLE::ble_setup_() { ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); return false; } - err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; + + if (!this->gap_event_handlers_.empty()) { + err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; + } } - if (this->has_server()) { + if (!this->gatts_event_handlers_.empty()) { err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); @@ -114,7 +110,7 @@ bool ESP32BLE::ble_setup_() { } } - if (this->has_client()) { + if (!this->gattc_event_handlers_.empty()) { err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); @@ -158,6 +154,10 @@ void ESP32BLE::loop() { this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, &ble_event->event_.gatts.gatts_param); break; + case BLEEvent::GATTC: + this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + break; case BLEEvent::GAP: this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); break; @@ -176,9 +176,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); - switch (event) { - default: - break; + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(event, param); } } @@ -191,14 +190,23 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); -#ifdef USE_ESP32_BLE_SERVER - this->server_->gatts_event_handler(event, gatts_if, param); -#endif + for (auto *gatts_handler : this->gatts_event_handlers_) { + gatts_handler->gatts_event_handler(event, gatts_if, param); + } } +void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) + global_ble->ble_events_.push(new_event); +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - // this->client_->gattc_event_handler(event, gattc_if, param); + ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); + for (auto *gattc_handler : this->gattc_event_handlers_) { + gattc_handler->gattc_event_handler(event, gattc_if, param); + } } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 0477dee070..5970b43688 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -5,17 +5,16 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "queue.h" -#ifdef USE_ESP32_BLE_SERVER -#include "esphome/components/esp32_ble_server/ble_server.h" -#endif +#include "queue.h" +#include "ble_event.h" #ifdef USE_ESP32 #include #include #include + namespace esphome { namespace esp32_ble { @@ -26,28 +25,36 @@ typedef struct { uint16_t mtu; } conn_status_t; +class GAPEventHandler { + public: + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; +}; + +class GATTcEventHandler { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; +}; + +class GATTsEventHandler { + public: + virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) = 0; +}; + class ESP32BLE : public Component { public: void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; - void mark_failed() override; - - bool has_server() { -#ifdef USE_ESP32_BLE_SERVER - return this->server_ != nullptr; -#else - return false; -#endif - } - bool has_client() { return false; } BLEAdvertising *get_advertising() { return this->advertising_; } -#ifdef USE_ESP32_BLE_SERVER - void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; } -#endif + void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } + void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + protected: static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); @@ -59,9 +66,10 @@ class ESP32BLE : public Component { bool ble_setup_(); -#ifdef USE_ESP32_BLE_SERVER - esp32_ble_server::BLEServer *server_{nullptr}; -#endif + std::vector gap_event_handlers_; + std::vector gattc_event_handlers_; + std::vector gatts_event_handlers_; + Queue ble_events_; BLEAdvertising *advertising_; }; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble/ble_event.h similarity index 50% rename from esphome/components/esp32_ble_tracker/queue.h rename to esphome/components/esp32_ble/ble_event.h index f1dcc337e8..1cf63b2fab 100644 --- a/esphome/components/esp32_ble_tracker/queue.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -1,69 +1,23 @@ #pragma once #ifdef USE_ESP32 -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" -#include -#include -#include #include #include #include -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal with various locking strategies, all incoming GAP and GATT - * events will simply be placed on a semaphore guarded queue. The next time the - * component runs loop(), these events are popped off the queue and handed at - * this safer time. - */ +#include namespace esphome { -namespace esp32_ble_tracker { - -template class Queue { - public: - Queue() { m_ = xSemaphoreCreateMutex(); } - - void push(T *element) { - if (element == nullptr) - return; - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - q_.push(element); - xSemaphoreGive(m_); - } - } - - T *pop() { - T *element = nullptr; - - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - if (!q_.empty()) { - element = q_.front(); - q_.pop(); - } - xSemaphoreGive(m_); - } - return element; - } - - protected: - std::queue q_; - SemaphoreHandle_t m_; -}; - -// Received GAP and GATTC events are only queued, and get processed in the main loop(). +namespace esp32_ble { +// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // This class stores each event in a single type. class BLEEvent { public: BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { this->event_.gap.gap_event = e; memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = 0; + this->type_ = GAP; }; BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { @@ -84,26 +38,57 @@ class BLEEvent { default: break; } - this->type_ = 1; + this->type_ = GATTC; + }; + + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); + // Need to also make a copy of relevant event data. + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->data.assign(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param.write.value = this->data.data(); + break; + default: + break; + } + this->type_ = GATTS; }; union { - struct gap_event { // NOLINT(readability-identifier-naming) + // NOLINTNEXTLINE(readability-identifier-naming) + struct gap_event { esp_gap_ble_cb_event_t gap_event; esp_ble_gap_cb_param_t gap_param; } gap; - struct gattc_event { // NOLINT(readability-identifier-naming) + // NOLINTNEXTLINE(readability-identifier-naming) + struct gattc_event { esp_gattc_cb_event_t gattc_event; esp_gatt_if_t gattc_if; esp_ble_gattc_cb_param_t gattc_param; } gattc; + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gatts_event { + esp_gatts_cb_event_t gatts_event; + esp_gatt_if_t gatts_if; + esp_ble_gatts_cb_param_t gatts_param; + } gatts; } event_; + std::vector data{}; - uint8_t type_; // 0=gap 1=gattc + // NOLINTNEXTLINE(readability-identifier-naming) + enum ble_event_t : uint8_t { + GAP, + GATTC, + GATTS, + } type_; }; -} // namespace esp32_ble_tracker +} // namespace esp32_ble } // namespace esphome #endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index 8556aa87df..a50d3dbd42 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -27,8 +27,7 @@ ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ESPBTUUID ret; ret.uuid_.len = ESP_UUID_LEN_128; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = data[i]; + memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128); return ret; } ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { @@ -91,10 +90,13 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { ESPBTUUID ret; ret.uuid_.len = uuid.len; - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + if (uuid.len == ESP_UUID_LEN_16) { + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + } else if (uuid.len == ESP_UUID_LEN_32) { + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + } else if (uuid.len == ESP_UUID_LEN_128) { + memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); + } return ret; } ESPBTUUID ESPBTUUID::as_128bit() const { @@ -158,30 +160,26 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { } return false; } -esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } -std::string ESPBTUUID::to_string() { - char sbuf[64]; +esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } +std::string ESPBTUUID::to_string() const { switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - break; + return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); case ESP_UUID_LEN_32: - sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), - (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); - break; + return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, + (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), + this->uuid_.uuid.uuid32 & 0xff); default: case ESP_UUID_LEN_128: - char *bpos = sbuf; + std::string buf; for (int8_t i = 15; i >= 0; i--) { - sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); - bpos += 2; + buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); if (i == 6 || i == 8 || i == 10 || i == 12) - sprintf(bpos++, "-"); + buf += "-"; } - sbuf[47] = '\0'; - break; + return buf; } - return sbuf; + return ""; } } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index f953f9fede..790a57c59d 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -32,9 +32,9 @@ class ESPBTUUID { bool operator==(const ESPBTUUID &uuid) const; bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - esp_bt_uuid_t get_uuid(); + esp_bt_uuid_t get_uuid() const; - std::string to_string(); + std::string to_string() const; protected: esp_bt_uuid_t uuid_; diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 8d05eca058..5b31b97ae2 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -2,16 +2,9 @@ #ifdef USE_ESP32 -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" - -#include #include -#include +#include -#include -#include -#include #include #include @@ -57,84 +50,6 @@ template class Queue { SemaphoreHandle_t m_; }; -// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). -// This class stores each event in a single type. -class BLEEvent { - public: - BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->event_.gap.gap_event = e; - memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = GAP; - }; - - BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); - // Need to also make a copy of notify event data. - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; - break; - default: - break; - } - this->type_ = GATTC; - }; - - BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); - // Need to also make a copy of write data. - switch (e) { - case ESP_GATTS_WRITE_EVT: - memcpy(this->event_.gatts.data, p->write.value, p->write.len); - this->event_.gatts.gatts_param.write.value = this->event_.gatts.data; - break; - default: - break; - } - this->type_ = GATTS; - }; - - union { - // NOLINTNEXTLINE(readability-identifier-naming) - struct gap_event { - esp_gap_ble_cb_event_t gap_event; - esp_ble_gap_cb_param_t gap_param; - } gap; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t gattc_param; - uint8_t data[64]; - } gattc; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t gatts_param; - uint8_t data[64]; - } gatts; - } event_; - // NOLINTNEXTLINE(readability-identifier-naming) - enum ble_event_t : uint8_t { - GAP, - GATTC, - GATTS, - } type_; -}; - } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index c3bd4ee418..311919dcd4 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER from esphome.core import CORE, TimePeriod from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components import esp32_ble DEPENDENCIES = ["esp32"] CONFLICTS_WITH = ["esp32_ble_tracker"] @@ -54,6 +55,8 @@ CONFIG_SCHEMA = cv.All( validate_config, ) +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant + async def to_code(config): uuid = config[CONF_UUID].hex diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 017d65573d..2793a74c5a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -40,7 +40,7 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { - if (device.address_uint64() != this->address_) + if (this->address_ == 0 || device.address_uint64() != this->address_) return false; if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) return false; @@ -132,16 +132,17 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->set_state(espbt::ClientState::IDLE); break; } - if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - this->set_state(espbt::ClientState::CONNECTED); - this->state_ = espbt::ClientState::ESTABLISHED; - break; - } auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); if (ret) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, this->address_str_.c_str(), ret); } + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + this->set_state(espbt::ClientState::CONNECTED); + this->state_ = espbt::ClientState::ESTABLISHED; + break; + } esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } @@ -189,6 +190,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_.c_str(), svc->start_handle, svc->end_handle); } + ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); this->set_state(espbt::ClientState::CONNECTED); this->state_ = espbt::ClientState::ESTABLISHED; break; diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 2fcc5c7743..0ddfa62c1b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -7,21 +7,25 @@ from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["esp32"] CONF_MANUFACTURER = "manufacturer" -CONF_BLE_ID = "ble_id" esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") -BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component) +BLEServer = esp32_ble_server_ns.class_( + "BLEServer", + cg.Component, + esp32_ble.GATTsEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), - cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, cv.Optional(CONF_MODEL): cv.string, } @@ -29,16 +33,18 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): - parent = await cg.get_variable(config[CONF_BLE_ID]) var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gatts_event_handler(var)) + cg.add(var.set_parent(parent)) + cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) if CONF_MODEL in config: cg.add(var.set_model(config[CONF_MODEL])) cg.add_define("USE_ESP32_BLE_SERVER") - cg.add(parent.set_server(var)) - if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 15bea07021..7cbf40c076 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -25,7 +25,8 @@ static const uint16_t VERSION_UUID = 0x2A26; static const uint16_t MANUFACTURER_UUID = 0x2A29; void BLEServer::setup() { - if (this->is_failed()) { + if (this->parent_->is_failed()) { + this->mark_failed(); ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE"); return; } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index f82e854090..ac759f2dcd 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -3,6 +3,7 @@ #include "ble_service.h" #include "ble_characteristic.h" +#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_advertising.h" #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/queue.h" @@ -32,7 +33,7 @@ class BLEServiceComponent { virtual void stop(); }; -class BLEServer : public Component { +class BLEServer : public Component, public GATTsEventHandler, public Parented { public: void setup() override; void loop() override; @@ -55,7 +56,8 @@ class BLEServer : public Component { uint32_t get_connected_client_count() { return this->connected_clients_; } const std::map &get_clients() { return this->clients_; } - void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) override; void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index a3938faff2..23f109b5ac 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -15,9 +15,11 @@ from esphome.const import ( CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, ) +from esphome.components import esp32_ble from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option +AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] CONF_ESP32_BLE_ID = "esp32_ble_id" @@ -26,7 +28,13 @@ CONF_WINDOW = "window" CONF_CONTINUOUS = "continuous" CONF_ON_SCAN_END = "on_scan_end" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") -ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESP32BLETracker = esp32_ble_tracker_ns.class_( + "ESP32BLETracker", + cg.Component, + esp32_ble.GAPEventHandler, + esp32_ble.GATTcEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") @@ -137,6 +145,7 @@ def as_reversed_hex_array(value): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLETracker), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( cv.Schema( { @@ -187,6 +196,8 @@ CONFIG_SCHEMA = cv.Schema( } ).extend(cv.COMPONENT_SCHEMA) +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant + ESP_BLE_DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ESP32_BLE_ID): cv.use_id(ESP32BLETracker), @@ -197,6 +208,12 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gap_event_handler(var)) + cg.add(parent.register_gattc_event_handler(var)) + cg.add(var.set_parent(parent)) + params = config[CONF_SCAN_PARAMETERS] cg.add(var.set_scan_duration(params[CONF_DURATION])) cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) @@ -245,6 +262,7 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + cg.add_define("USE_ESP32_BLE_CLIENT") ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index fb377e2be2..6b0f4dc897 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -7,14 +7,14 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include -#include #include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include #ifdef USE_OTA #include "esphome/components/ota/ota_component.h" @@ -45,17 +45,19 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { return u; } -float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; } +float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { + if (this->parent_->is_failed()) { + this->mark_failed(); + ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); + return; + } + global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); this->scan_end_lock_ = xSemaphoreCreateMutex(); this->scanner_idle_ = true; - if (!ESP32BLETracker::ble_setup()) { - this->mark_failed(); - return; - } #ifdef USE_OTA ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) { @@ -75,18 +77,6 @@ void ESP32BLETracker::setup() { } void ESP32BLETracker::loop() { - BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { - if (ble_event->type_) { - this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, - &ble_event->event_.gattc.gattc_param); - } else { - this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); - } - delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) - ble_event = this->ble_events_.pop(); - } - int connecting = 0; int discovered = 0; int searching = 0; @@ -238,85 +228,6 @@ void ESP32BLETracker::stop_scan() { this->cancel_timeout("scan"); } -bool ESP32BLETracker::ble_setup() { - // Initialize non-volatile storage for the bluetooth controller - esp_err_t err = nvs_flash_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); - return false; - } - -#ifdef USE_ARDUINO - if (!btStart()) { - ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); - return false; - } -#else - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - // start bt controller - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { - esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); - err = esp_bt_controller_init(&cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); - return false; - } - while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) - ; - } - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { - err = esp_bt_controller_enable(ESP_BT_MODE_BLE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err)); - return false; - } - } - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - ESP_LOGE(TAG, "esp bt controller enable failed"); - return false; - } - } -#endif - - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - - err = esp_bluedroid_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); - return false; - } - err = esp_bluedroid_enable(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); - return false; - } - err = esp_ble_gap_register_callback(ESP32BLETracker::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; - } - err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); - return false; - } - - // Empty name - esp_ble_gap_set_device_name(""); - - esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); - return false; - } - - // BLE takes some time to be fully set up, 200ms should be more than enough - delay(200); // NOLINT - - return true; -} - void ESP32BLETracker::start_scan_(bool first) { // The lock must be held when calling this function. if (xSemaphoreTake(this->scan_end_lock_, 0L)) { @@ -369,11 +280,6 @@ void ESP32BLETracker::register_client(ESPBTClient *client) { } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *gap_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) - global_esp32_ble_tracker->ble_events_.push(gap_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) - -void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: this->gap_scan_result_(param->scan_rst); @@ -428,204 +334,11 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) - global_esp32_ble_tracker->ble_events_.push(gattc_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) - -void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { for (auto *client : this->clients_) { client->gattc_event_handler(event, gattc_if, param); } } -ESPBTUUID::ESPBTUUID() : uuid_() {} -ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_128; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = data[i]; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { - ESPBTUUID ret; - if (data.length() == 4) { - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = 0; - for (int i = 0; i < data.length();) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4; - i += 2; - } - } else if (data.length() == 8) { - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = 0; - for (int i = 0; i < data.length();) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4; - i += 2; - } - } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be - // investigated (lack of time) - ret.uuid_.len = ESP_UUID_LEN_128; - memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); - } else if (data.length() == 36) { - // If the length of the string is 36 bytes then we will assume it is a long hex string in - // UUID format. - ret.uuid_.len = ESP_UUID_LEN_128; - int n = 0; - for (int i = 0; i < data.length();) { - if (data.c_str()[i] == '-') - i++; - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); - i += 2; - } - } else { - ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); - } - return ret; -} -ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = uuid.len; - if (uuid.len == ESP_UUID_LEN_16) { - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - } else if (uuid.len == ESP_UUID_LEN_32) { - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - } else if (uuid.len == ESP_UUID_LEN_128) { - memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); - } - return ret; -} -ESPBTUUID ESPBTUUID::as_128bit() const { - if (this->uuid_.len == ESP_UUID_LEN_128) { - return *this; - } - uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - uint32_t uuid32; - if (this->uuid_.len == ESP_UUID_LEN_32) { - uuid32 = this->uuid_.uuid.uuid32; - } else { - uuid32 = this->uuid_.uuid.uuid16; - } - for (uint8_t i = 0; i < this->uuid_.len; i++) { - data[12 + i] = ((uuid32 >> i * 8) & 0xFF); - } - return ESPBTUUID::from_raw(data); -} -bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { - if (this->uuid_.len == ESP_UUID_LEN_16) { - return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; - } else if (this->uuid_.len == ESP_UUID_LEN_32) { - for (uint8_t i = 0; i < 3; i++) { - bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; - bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; - if (a && b) - return true; - } - } else { - for (uint8_t i = 0; i < 15; i++) { - if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) - return true; - } - } - return false; -} -bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { - if (this->uuid_.len == uuid.uuid_.len) { - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { - return true; - } - break; - case ESP_UUID_LEN_32: - if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { - return true; - } - break; - case ESP_UUID_LEN_128: - for (int i = 0; i < ESP_UUID_LEN_128; i++) { - if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { - return false; - } - } - return true; - break; - } - } else { - return this->as_128bit() == uuid.as_128bit(); - } - return false; -} -esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } -std::string ESPBTUUID::to_string() const { - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - case ESP_UUID_LEN_32: - return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, - (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), - this->uuid_.uuid.uuid32 & 0xff); - default: - case ESP_UUID_LEN_128: - std::string buf; - for (int8_t i = 15; i >= 0; i--) { - buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); - if (i == 6 || i == 8 || i == 10 || i == 12) - buf += "-"; - } - return buf; - } - return ""; -} - -uint64_t ESPBTUUID::get_128bit_high() const { - esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); - return ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); -} -uint64_t ESPBTUUID::get_128bit_low() const { - esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); - return ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); -} - ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { if (!data.uuid.contains(0x4C, 0x00)) @@ -706,11 +419,7 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p while (offset + 2 < len) { const uint8_t field_length = payload[offset++]; // First byte is length of adv record if (field_length == 0) { - if (offset < param.adv_data_len && param.scan_rsp_len > 0) { // Zero padded advertisement data - offset = param.adv_data_len; - continue; - } - break; + continue; // Possible zero padded advertisement data } // first byte of adv record is adv record type diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index e6f7829353..d1f72cf78d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -1,9 +1,8 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "queue.h" #include #include @@ -15,40 +14,16 @@ #include #include +#include +#include + +#include "esphome/components/esp32_ble/ble.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + namespace esphome { namespace esp32_ble_tracker { -class ESPBTUUID { - public: - ESPBTUUID(); - - static ESPBTUUID from_uint16(uint16_t uuid); - - static ESPBTUUID from_uint32(uint32_t uuid); - - static ESPBTUUID from_raw(const uint8_t *data); - - static ESPBTUUID from_raw(const std::string &data); - - static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); - - ESPBTUUID as_128bit() const; - - bool contains(uint8_t data1, uint8_t data2) const; - - bool operator==(const ESPBTUUID &uuid) const; - bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - - esp_bt_uuid_t get_uuid() const; - - std::string to_string() const; - - uint64_t get_128bit_high() const; - uint64_t get_128bit_low() const; - - protected: - esp_bt_uuid_t uuid_; -}; +using namespace esp32_ble; using adv_data_t = std::vector; @@ -191,7 +166,7 @@ class ESPBTClient : public ESPBTDeviceListener { ClientState state_; }; -class ESP32BLETracker : public Component { +class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEventHandler, public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } @@ -218,16 +193,15 @@ class ESP32BLETracker : public Component { void start_scan(); void stop_scan(); + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + protected: - /// The FreeRTOS task managing the bluetooth interface. - static bool ble_setup(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); /// Called when a scan ends void end_of_scan_(); - /// Callback that will handle all GAP events and redistribute them to other callbacks. - static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. @@ -238,9 +212,6 @@ class ESP32BLETracker : public Component { void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); int app_id_; - /// Callback that will handle all GATTC events and redistribute them to other callbacks. - static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); - void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; @@ -263,8 +234,6 @@ class ESP32BLETracker : public Component { esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; - - Queue ble_events_; }; // NOLINTNEXTLINE diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 753b6ed9da..b3abbd5c13 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -156,7 +156,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index baae683988..3eb2d1f035 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -2,7 +2,7 @@ #include "esp32_can.h" #include "esphome/core/log.h" -#include +#include // WORKAROUND, because CAN_IO_UNUSED is just defined as (-1) in this version // of the framework which does not work with -fpermissive @@ -14,25 +14,25 @@ namespace esp32_can { static const char *const TAG = "esp32_can"; -static bool get_bitrate(canbus::CanSpeed bitrate, can_timing_config_t *t_config) { +static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) { switch (bitrate) { case canbus::CAN_50KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_50KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_50KBITS(); return true; case canbus::CAN_100KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_100KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_100KBITS(); return true; case canbus::CAN_125KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_125KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_125KBITS(); return true; case canbus::CAN_250KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_250KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_250KBITS(); return true; case canbus::CAN_500KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_500KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_500KBITS(); return true; case canbus::CAN_1000KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_1MBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1MBITS(); return true; default: return false; @@ -40,10 +40,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, can_timing_config_t *t_config) } 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; + twai_general_config_t g_config = + TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + twai_timing_config_t t_config; if (!get_bitrate(this->bit_rate_, &t_config)) { // invalid bit rate @@ -51,15 +51,15 @@ bool ESP32Can::setup_internal() { return false; } - // Install CAN driver - if (can_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + // Install TWAI driver + if (twai_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) { + // Start TWAI driver + if (twai_start() != ESP_OK) { // Failed to start driver this->mark_failed(); return false; @@ -72,15 +72,15 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { return canbus::ERROR_FAILTX; } - uint32_t flags = CAN_MSG_FLAG_NONE; + uint32_t flags = TWAI_MSG_FLAG_NONE; if (frame->use_extended_id) { - flags |= CAN_MSG_FLAG_EXTD; + flags |= TWAI_MSG_FLAG_EXTD; } if (frame->remote_transmission_request) { - flags |= CAN_MSG_FLAG_RTR; + flags |= TWAI_MSG_FLAG_RTR; } - can_message_t message = { + twai_message_t message = { .flags = flags, .identifier = frame->can_id, .data_length_code = frame->can_data_length_code, @@ -89,7 +89,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { memcpy(message.data, frame->data, frame->can_data_length_code); } - if (can_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) { + if (twai_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) { return canbus::ERROR_OK; } else { return canbus::ERROR_ALLTXBUSY; @@ -97,15 +97,15 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { } canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { - can_message_t message; + twai_message_t message; - if (can_receive(&message, 0) != ESP_OK) { + if (twai_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->use_extended_id = message.flags & TWAI_MSG_FLAG_EXTD; + frame->remote_transmission_request = message.flags & TWAI_MSG_FLAG_RTR; frame->can_data_length_code = message.data_length_code; if (!frame->remote_transmission_request) { diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1e50418e01..7170a6dabf 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] CONF_AUTHORIZED_DURATION = "authorized_duration" diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 5ff4b0827d..85013c006b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -195,7 +195,7 @@ void ESP32ImprovComponent::send_response_(std::vector &response) { } void ESP32ImprovComponent::start() { - if (this->state_ != improv::STATE_STOPPED) + if (this->should_start_ || this->state_ != improv::STATE_STOPPED) return; ESP_LOGD(TAG, "Setting Improv to start"); diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index b3614d8fcf..a0f8b557b0 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -33,6 +33,7 @@ ETHERNET_TYPES = { "RTL8201": EthernetType.ETHERNET_TYPE_RTL8201, "DP83848": EthernetType.ETHERNET_TYPE_DP83848, "IP101": EthernetType.ETHERNET_TYPE_IP101, + "JL1101": EthernetType.ETHERNET_TYPE_JL1101, } emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c new file mode 100644 index 0000000000..6011795033 --- /dev/null +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -0,0 +1,339 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef USE_ESP32 + +#include +#include +#include +#include "esp_log.h" +#include "esp_eth.h" +#include "eth_phy_regs_struct.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_rom_gpio.h" +#include "esp_rom_sys.h" + +static const char *TAG = "jl1101"; +#define PHY_CHECK(a, str, goto_tag, ...) \ + do { \ + if (!(a)) { \ + ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + goto goto_tag; \ + } \ + } while (0) + +/***************Vendor Specific Register***************/ + +/** + * @brief PSR(Page Select Register) + * + */ +typedef union { + struct { + uint16_t page_select : 8; /* Select register page, default is 0 */ + uint16_t reserved : 8; /* Reserved */ + }; + uint16_t val; +} psr_reg_t; +#define ETH_PHY_PSR_REG_ADDR (0x1F) + +typedef struct { + esp_eth_phy_t parent; + esp_eth_mediator_t *eth; + int addr; + uint32_t reset_timeout_ms; + uint32_t autonego_timeout_ms; + eth_link_t link_status; + int reset_gpio_num; +} phy_jl1101_t; + +static esp_err_t jl1101_page_select(phy_jl1101_t *jl1101, uint32_t page) { + esp_eth_mediator_t *eth = jl1101->eth; + psr_reg_t psr = {.page_select = page}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_PSR_REG_ADDR, psr.val) == ESP_OK, "write PSR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_update_link_duplex_speed(phy_jl1101_t *jl1101) { + esp_eth_mediator_t *eth = jl1101->eth; + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + bmcr_reg_t bmcr; + bmsr_reg_t bmsr; + uint32_t peer_pause_ability = false; + anlpar_reg_t anlpar; + PHY_CHECK(jl1101_page_select(jl1101, 0) == ESP_OK, "select page 0 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)) == ESP_OK, + "read ANLPAR failed", err); + eth_link_t link = bmsr.link_status ? ETH_LINK_UP : ETH_LINK_DOWN; + /* check if link status changed */ + if (jl1101->link_status != link) { + /* when link up, read negotiation result */ + if (link == ETH_LINK_UP) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.speed_select) { + speed = ETH_SPEED_100M; + } else { + speed = ETH_SPEED_10M; + } + if (bmcr.duplex_mode) { + duplex = ETH_DUPLEX_FULL; + } else { + duplex = ETH_DUPLEX_HALF; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *) speed) == ESP_OK, "change speed failed", err); + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *) duplex) == ESP_OK, "change duplex failed", err); + /* if we're in duplex mode, and peer has the flow control ability */ + if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { + peer_pause_ability = 1; + } else { + peer_pause_ability = 0; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *) peer_pause_ability) == ESP_OK, + "change pause ability failed", err); + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_LINK, (void *) link) == ESP_OK, "change link failed", err); + jl1101->link_status = link; + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_mediator(esp_eth_phy_t *phy, esp_eth_mediator_t *eth) { + PHY_CHECK(eth, "can't set mediator to null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->eth = eth; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_get_link(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + /* Updata information about link, speed, duplex */ + PHY_CHECK(jl1101_update_link_duplex_speed(jl1101) == ESP_OK, "update link duplex speed failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->link_status = ETH_LINK_DOWN; + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr = {.reset = 1}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for reset complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 50; to++) { + vTaskDelay(pdMS_TO_TICKS(50)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!bmcr.reset) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 50, "reset timeout", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + if (jl1101->reset_gpio_num >= 0) { + esp_rom_gpio_pad_select_gpio(jl1101->reset_gpio_num); + gpio_set_direction(jl1101->reset_gpio_num, GPIO_MODE_OUTPUT); + gpio_set_level(jl1101->reset_gpio_num, 0); + esp_rom_delay_us(100); // insert min input assert time + gpio_set_level(jl1101->reset_gpio_num, 1); + } + return ESP_OK; +} + +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* in case any link status has changed, let's assume we're in link down status */ + jl1101->link_status = ETH_LINK_DOWN; + /* Restart auto negotiation */ + bmcr_reg_t bmcr = { + .speed_select = 1, /* 100Mbps */ + .duplex_mode = 1, /* Full Duplex */ + .en_auto_nego = 1, /* Auto Negotiation */ + .restart_auto_nego = 1 /* Restart Auto Negotiation */ + }; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for auto negotiation complete */ + bmsr_reg_t bmsr; + uint32_t to = 0; + for (to = 0; to < jl1101->autonego_timeout_ms / 100; to++) { + vTaskDelay(pdMS_TO_TICKS(100)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + if (bmsr.auto_nego_complete) { + break; + } + } + /* Auto negotiation failed, maybe no network cable plugged in, so output a warning */ + if (to >= jl1101->autonego_timeout_ms / 100) { + ESP_LOGW(TAG, "auto negotiation timeout"); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_pwrctl(esp_eth_phy_t *phy, bool enable) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!enable) { + /* Enable IEEE Power Down Mode */ + bmcr.power_down = 1; + } else { + /* Disable IEEE Power Down Mode */ + bmcr.power_down = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + if (!enable) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + PHY_CHECK(bmcr.power_down == 1, "power down failed", err); + } else { + /* wait for power up complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 10; to++) { + vTaskDelay(pdMS_TO_TICKS(10)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.power_down == 0) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 10, "power up timeout", err); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_addr(esp_eth_phy_t *phy, uint32_t addr) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->addr = addr; + return ESP_OK; +} + +static esp_err_t jl1101_get_addr(esp_eth_phy_t *phy, uint32_t *addr) { + PHY_CHECK(addr, "addr can't be null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + *addr = jl1101->addr; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_del(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + free(jl1101); + return ESP_OK; +} + +static esp_err_t jl1101_advertise_pause_ability(esp_eth_phy_t *phy, uint32_t ability) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* Set PAUSE function ability */ + anar_reg_t anar; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, &(anar.val)) == ESP_OK, "read ANAR failed", + err); + if (ability) { + anar.asymmetric_pause = 1; + anar.symmetric_pause = 1; + } else { + anar.asymmetric_pause = 0; + anar.symmetric_pause = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, anar.val) == ESP_OK, "write ANAR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_init(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + // Detect PHY address + if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { + PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); + } + /* Power on Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); + /* Reset Ethernet PHY */ + PHY_CHECK(jl1101_reset(phy) == ESP_OK, "reset failed", err); + /* Check PHY ID */ + phyidr1_reg_t id1; + phyidr2_reg_t id2; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR1_REG_ADDR, &(id1.val)) == ESP_OK, "read ID1 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR2_REG_ADDR, &(id2.val)) == ESP_OK, "read ID2 failed", err); + PHY_CHECK(id1.oui_msb == 0x937C && id2.oui_lsb == 0x10 && id2.vendor_model == 0x2, "wrong chip ID", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_deinit(esp_eth_phy_t *phy) { + /* Power off Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, false) == ESP_OK, "power control failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { + PHY_CHECK(config, "can't set phy config to null", err); + phy_jl1101_t *jl1101 = calloc(1, sizeof(phy_jl1101_t)); + PHY_CHECK(jl1101, "calloc jl1101 failed", err); + jl1101->addr = config->phy_addr; + jl1101->reset_gpio_num = config->reset_gpio_num; + jl1101->reset_timeout_ms = config->reset_timeout_ms; + jl1101->link_status = ETH_LINK_DOWN; + jl1101->autonego_timeout_ms = config->autonego_timeout_ms; + jl1101->parent.reset = jl1101_reset; + jl1101->parent.reset_hw = jl1101_reset_hw; + jl1101->parent.init = jl1101_init; + jl1101->parent.deinit = jl1101_deinit; + jl1101->parent.set_mediator = jl1101_set_mediator; + jl1101->parent.negotiate = jl1101_negotiate; + jl1101->parent.get_link = jl1101_get_link; + jl1101->parent.pwrctl = jl1101_pwrctl; + jl1101->parent.get_addr = jl1101_get_addr; + jl1101->parent.set_addr = jl1101_set_addr; + jl1101->parent.advertise_pause_ability = jl1101_advertise_pause_ability; + jl1101->parent.del = jl1101_del; + + return &(jl1101->parent); +err: + return NULL; +} +#endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fa66d824be..a3f0ae715f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -71,6 +71,10 @@ void EthernetComponent::setup() { phy = esp_eth_phy_new_ip101(&phy_config); break; } + case ETHERNET_TYPE_JL1101: { + phy = esp_eth_phy_new_jl1101(&phy_config); + break; + } default: { this->mark_failed(); return; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index ed784e1bc0..a538a5c77d 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -18,6 +18,7 @@ enum EthernetType { ETHERNET_TYPE_RTL8201, ETHERNET_TYPE_DP83848, ETHERNET_TYPE_IP101, + ETHERNET_TYPE_JL1101, }; struct ManualIP { @@ -82,6 +83,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; +extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); } // namespace ethernet } // namespace esphome diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 4961595505..f95d679c3e 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -52,7 +52,7 @@ GrowattSolar = growatt_solar_ns.class_( PHASE_SENSORS = { CONF_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, ), CONF_CURRENT: sensor.sensor_schema( @@ -71,7 +71,7 @@ PHASE_SENSORS = { PV_SENSORS = { CONF_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, ), CONF_CURRENT: sensor.sensor_schema( @@ -135,13 +135,13 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=2, + accuracy_decimals=1, 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, + accuracy_decimals=1, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), diff --git a/esphome/components/hte501/__init__.py b/esphome/components/hte501/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp new file mode 100644 index 0000000000..68edd07a22 --- /dev/null +++ b/esphome/components/hte501/hte501.cpp @@ -0,0 +1,90 @@ +#include "hte501.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hte501 { + +static const char *const TAG = "hte501"; + +void HTE501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up HTE501..."); + uint8_t address[] = {0x70, 0x29}; + this->write(address, 2, false); + uint8_t identification[9]; + this->read(identification, 9); + if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +} + +void HTE501Component::dump_config() { + ESP_LOGCONFIG(TAG, "HTE501:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with HTE501 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } +void HTE501Component::update() { + uint8_t address_1[] = {0x2C, 0x1B}; + this->write(address_1, 2, true); + this->set_timeout(50, [this]() { + uint8_t i2c_response[6]; + this->read(i2c_response, 6); + if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return; + } + float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]); + if (temperature > 55536) { + temperature = (temperature - 65536) / 100; + } else { + temperature = temperature / 100; + } + float humidity = ((float) encode_uint16(i2c_response[3], i2c_response[4])) / 100.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + }); +} + +unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { + unsigned char crc_val = 0xFF; + unsigned char i = 0; + unsigned char j = 0; + for (i = from; i <= to; i++) { + int cur_val = buf[i]; + for (j = 0; j < 8; j++) { + if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal + { + crc_val = ((crc_val << 1) ^ 0x31); + } else { + crc_val = (crc_val << 1); + } + cur_val = cur_val << 1; + } + } + return crc_val; +} +} // namespace hte501 +} // namespace esphome diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h new file mode 100644 index 0000000000..0d2c952e81 --- /dev/null +++ b/esphome/components/hte501/hte501.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hte501 { + +/// This class implements support for the hte501 of temperature i2c sensors. +class HTE501Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace hte501 +} // namespace esphome diff --git a/esphome/components/hte501/sensor.py b/esphome/components/hte501/sensor.py new file mode 100644 index 0000000000..8bd6160038 --- /dev/null +++ b/esphome/components/hte501/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +hte501_ns = cg.esphome_ns.namespace("hte501") +HTE501Component = hte501_ns.class_( + "HTE501Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HTE501Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index c8c0ca5369..0c3e249512 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -195,6 +195,8 @@ async def http_request_action_to_code(config, action_id, template_arg, args): for conf in config.get(CONF_ON_RESPONSE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) cg.add(var.register_response_trigger(trigger)) - await automation.build_automation(trigger, [(int, "status_code")], conf) + await automation.build_automation( + trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf + ) return var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 4e1cfe94b3..46894a9afd 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -66,6 +66,9 @@ void HttpRequestComponent::send(const std::vector } this->client_.setTimeout(this->timeout_); +#if defined(USE_ESP32) + this->client_.setConnectTimeout(this->timeout_); +#endif if (this->useragent_ != nullptr) { this->client_.setUserAgent(this->useragent_); } @@ -73,25 +76,27 @@ void HttpRequestComponent::send(const std::vector this->client_.addHeader(header.name, header.value, false, true); } + uint32_t start_time = millis(); int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); + uint32_t duration = millis() - start_time; for (auto *trigger : response_triggers) - trigger->process(http_code); + trigger->process(http_code, duration); if (http_code < 0) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_.c_str(), - HTTPClient::errorToString(http_code).c_str()); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(), + HTTPClient::errorToString(http_code).c_str(), duration); this->status_set_warning(); return; } if (http_code < 200 || http_code >= 300) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_.c_str(), http_code); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); this->status_set_warning(); return; } this->status_clear_warning(); - ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_.c_str(), http_code); + ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); } #ifdef USE_ESP8266 diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index eab3045fdc..0958c07683 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -31,7 +31,10 @@ struct Header { const char *value; }; -class HttpRequestResponseTrigger; +class HttpRequestResponseTrigger : public Trigger { + public: + void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); } +}; class HttpRequestComponent : public Component { public: @@ -138,11 +141,6 @@ template class HttpRequestSendAction : public Action { std::vector response_triggers_; }; -class HttpRequestResponseTrigger : public Trigger { - public: - void process(int status_code) { this->trigger(status_code); } -}; - } // namespace http_request } // namespace esphome diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 730499a493..c2dbbd6737 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -6,8 +6,10 @@ from esphome.const import ( CONF_MODEL, CONF_MOISTURE, CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRECIPITATION_INTENSITY, + DEVICE_CLASS_PRECIPITATION, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, ICON_THERMOMETER, ) @@ -70,31 +72,31 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ACC): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_PRECIPITATION, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_R_INT): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( unit_of_measurement=UNIT_INTENSITY, accuracy_decimals=0, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/i2s_audio/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/i2s_audio_media_player.cpp index f1f1dc0d51..2b00a5ec26 100644 --- a/esphome/components/i2s_audio/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/i2s_audio_media_player.cpp @@ -103,9 +103,11 @@ void I2SAudioMediaPlayer::stop_() { void I2SAudioMediaPlayer::setup() { ESP_LOGCONFIG(TAG, "Setting up Audio..."); +#if SOC_I2S_SUPPORTS_DAC if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { this->audio_ = make_unique