1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-13 00:32:20 +01:00

Merge branch 'dev' into multi_device

This commit is contained in:
DanielV
2025-05-06 00:48:23 +02:00
committed by GitHub
280 changed files with 17624 additions and 3325 deletions

View File

@@ -114,4 +114,5 @@ config/
examples/ examples/
Dockerfile Dockerfile
.git/ .git/
tests/build/ tests/
.*

View File

@@ -1,15 +1,11 @@
name: Build Image name: Build Image
inputs: inputs:
platform:
description: "Platform to build for"
required: true
example: "linux/amd64"
target: target:
description: "Target to build" description: "Target to build"
required: true required: true
example: "docker" example: "docker"
baseimg: build_type:
description: "Base image type" description: "Build type"
required: true required: true
example: "docker" example: "docker"
suffix: suffix:
@@ -19,6 +15,11 @@ inputs:
description: "Version to build" description: "Version to build"
required: true required: true
example: "2023.12.0" example: "2023.12.0"
base_os:
description: "Base OS to use"
required: false
default: "debian"
example: "debian"
runs: runs:
using: "composite" using: "composite"
steps: steps:
@@ -46,52 +47,52 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.15.0 uses: docker/build-push-action@v6.16.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }} target: ${{ inputs.target }}
cache-from: type=gha cache-from: type=gha
cache-to: ${{ steps.cache-to.outputs.value }} cache-to: ${{ steps.cache-to.outputs.value }}
build-args: | build-args: |
BASEIMGTYPE=${{ inputs.baseimg }} BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }} BUILD_VERSION=${{ inputs.version }}
BUILD_OS=${{ inputs.base_os }}
outputs: | outputs: |
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
- name: Export ghcr digests - name: Export ghcr digests
shell: bash shell: bash
run: | run: |
mkdir -p /tmp/digests/${{ inputs.target }}/ghcr mkdir -p /tmp/digests/${{ inputs.build_type }}/ghcr
digest="${{ steps.build-ghcr.outputs.digest }}" digest="${{ steps.build-ghcr.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" touch "/tmp/digests/${{ inputs.build_type }}/ghcr/${digest#sha256:}"
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.15.0 uses: docker/build-push-action@v6.16.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }} target: ${{ inputs.target }}
cache-from: type=gha cache-from: type=gha
cache-to: ${{ steps.cache-to.outputs.value }} cache-to: ${{ steps.cache-to.outputs.value }}
build-args: | build-args: |
BASEIMGTYPE=${{ inputs.baseimg }} BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }} BUILD_VERSION=${{ inputs.version }}
BUILD_OS=${{ inputs.base_os }}
outputs: | outputs: |
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
- name: Export dockerhub digests - name: Export dockerhub digests
shell: bash shell: bash
run: | run: |
mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub mkdir -p /tmp/digests/${{ inputs.build_type }}/dockerhub
digest="${{ steps.build-dockerhub.outputs.digest }}" digest="${{ steps.build-dockerhub.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" touch "/tmp/digests/${{ inputs.build_type }}/dockerhub/${digest#sha256:}"

View File

@@ -17,7 +17,7 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
@@ -34,7 +34,7 @@ runs:
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .
- name: Create Python virtual environment - name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
@@ -43,5 +43,5 @@ runs:
python -m venv venv python -m venv venv
./venv/Scripts/activate ./venv/Scripts/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .

View File

@@ -17,7 +17,6 @@ updates:
docker-actions: docker-actions:
applies-to: version-updates applies-to: version-updates
patterns: patterns:
- "docker/setup-qemu-action"
- "docker/login-action" - "docker/login-action"
- "docker/setup-buildx-action" - "docker/setup-buildx-action"
- package-ecosystem: github-actions - package-ecosystem: github-actions

View File

@@ -23,7 +23,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@@ -37,12 +37,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["ubuntu-latest", "ubuntu-24.04-arm"] os: ["ubuntu-24.04", "ubuntu-24.04-arm"]
build_type: ["ha-addon", "docker", "lint"] build_type:
- "ha-addon"
- "docker"
# - "lint"
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -39,10 +39,10 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
@@ -58,7 +58,7 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .
ruff: ruff:
@@ -165,6 +165,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
script/ci-custom.py script/ci-custom.py
script/build_codeowners.py --check script/build_codeowners.py --check
script/build_language_schema.py --check
pytest: pytest:
name: Run pytest name: Run pytest

View File

@@ -53,7 +53,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment - name: Set up python environment
@@ -68,31 +68,31 @@ jobs:
uses: pypa/gh-action-pypi-publish@v1.12.4 uses: pypa/gh-action-pypi-publish@v1.12.4
deploy-docker: deploy-docker:
name: Build ESPHome ${{ matrix.platform }} name: Build ESPHome ${{ matrix.platform.arch }}
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
permissions: permissions:
contents: read contents: read
packages: write packages: write
runs-on: ubuntu-latest runs-on: ${{ matrix.platform.os }}
needs: [init] needs: [init]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: platform:
- linux/amd64 - arch: amd64
- linux/arm64 os: "ubuntu-24.04"
- arch: arm64
os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.6.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.4.0
@@ -109,45 +109,36 @@ jobs:
- name: Build docker - name: Build docker
uses: ./.github/actions/build-image uses: ./.github/actions/build-image
with: with:
platform: ${{ matrix.platform }} target: final
target: docker build_type: docker
baseimg: docker
suffix: "" suffix: ""
version: ${{ needs.init.outputs.tag }} version: ${{ needs.init.outputs.tag }}
- name: Build ha-addon - name: Build ha-addon
uses: ./.github/actions/build-image uses: ./.github/actions/build-image
with: with:
platform: ${{ matrix.platform }} target: final
target: hassio build_type: ha-addon
baseimg: hassio
suffix: "hassio" suffix: "hassio"
version: ${{ needs.init.outputs.tag }} version: ${{ needs.init.outputs.tag }}
- name: Build lint # - name: Build lint
uses: ./.github/actions/build-image # uses: ./.github/actions/build-image
with: # with:
platform: ${{ matrix.platform }} # target: lint
target: lint # build_type: lint
baseimg: docker # suffix: lint
suffix: lint # version: ${{ needs.init.outputs.tag }}
version: ${{ needs.init.outputs.tag }}
- name: Sanitize platform name
id: sanitize
run: |
echo "${{ matrix.platform }}" | sed 's|/|-|g' > /tmp/platform
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests - name: Upload digests
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
with: with:
name: digests-${{ steps.sanitize.outputs.name }} name: digests-${{ matrix.platform.arch }}
path: /tmp/digests path: /tmp/digests
retention-days: 1 retention-days: 1
deploy-manifest: deploy-manifest:
name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }} name: Publish ESPHome ${{ matrix.image.build_type }} to ${{ matrix.registry }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- init - init
@@ -160,15 +151,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
image: image:
- title: "ha-addon" - build_type: "docker"
target: "hassio"
suffix: "hassio"
- title: "docker"
target: "docker"
suffix: "" suffix: ""
- title: "lint" - build_type: "ha-addon"
target: "lint" suffix: "hassio"
suffix: "lint" # - build_type: "lint"
# suffix: "lint"
registry: registry:
- ghcr - ghcr
- dockerhub - dockerhub
@@ -176,7 +164,7 @@ jobs:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests
@@ -212,7 +200,7 @@ jobs:
done done
- name: Create manifest list and push - name: Create manifest list and push
working-directory: /tmp/digests/${{ matrix.image.target }}/${{ matrix.registry }} working-directory: /tmp/digests/${{ matrix.image.build_type }}/${{ matrix.registry }}
run: | run: |
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)

View File

@@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: 3.12 python-version: 3.12

View File

@@ -98,6 +98,7 @@ esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/const/* @esphome/core
esphome/components/coolix/* @glmnet esphome/components/coolix/* @glmnet
esphome/components/copy/* @OttoWinter esphome/components/copy/* @OttoWinter
esphome/components/cover/* @esphome/core esphome/components/cover/* @esphome/core
@@ -278,7 +279,7 @@ esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @jorre05 esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
@@ -319,6 +320,7 @@ esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931 esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pca9554/* @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon esphome/components/pcf85063/* @brogon
@@ -328,6 +330,7 @@ esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie esphome/components/pm1006/* @habbie
esphome/components/pm2005/* @andrewjswan esphome/components/pm2005/* @andrewjswan
esphome/components/pmsa003i/* @sjtrny esphome/components/pmsa003i/* @sjtrny
esphome/components/pmsx003/* @ximex
esphome/components/pmwcs3/* @SeByDocKy esphome/components/pmwcs3/* @SeByDocKy
esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz
@@ -427,6 +430,7 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984 esphome/components/tca9548a/* @andreashergert1984
@@ -466,6 +470,7 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb esphome/components/uart/button/* @ssieb
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli esphome/components/ufire_ise/* @pvizeli

View File

@@ -1,131 +1,54 @@
# Build these with the build.py script ARG BUILD_VERSION=dev
# Example: ARG BUILD_OS=alpine
# python3 docker/build.py --tag dev --arch amd64 --build-type docker build ARG BUILD_BASE_VERSION=2025.04.0
ARG BUILD_TYPE=docker
# One of "docker", "hassio" FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
ARG BASEIMGTYPE=docker FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
ARG BUILD_TYPE
FROM base-source-${BUILD_TYPE} AS base
# https://github.com/hassio-addons/addon-debian-base/releases RUN git config --system --add safe.directory "*"
FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm
FROM debian:12.2-slim AS base-docker
FROM base-${BASEIMGTYPE} AS base RUN pip install uv==0.6.14
COPY requirements.txt /
ARG TARGETARCH
ARG TARGETVARIANT
# Note that --break-system-packages is used below because
# https://peps.python.org/pep-0668/ added a safety check that prevents
# installing packages with the same name as a system package. This is
# not a problem for us because we are not concerned about overwriting
# system packages because we are running in an isolated container.
RUN \ RUN \
apt-get update \ uv pip install --no-cache-dir \
# Use pinned versions so that we get updates with build caching -r /requirements.txt
&& apt-get install -y --no-install-recommends \
python3-pip=23.0.1+dfsg-1 \
python3-setuptools=66.1.1-1+deb12u1 \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u2 \
curl=7.88.1-10+deb12u12 \
openssh-client=1:9.2p1-2+deb12u5 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
patch=2.7.6-7 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
ENV \
# Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
# Store globally installed pio libs in /piolibs
PLATFORMIO_GLOBALLIB_DIR=/piolibs
RUN \ RUN \
pip3 install \ platformio settings set enable_telemetry No \
--break-system-packages --no-cache-dir \
# Keep platformio version in sync with requirements.txt
platformio==6.1.18 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \ && platformio settings set check_platformio_interval 1000000 \
&& mkdir -p /piolibs && mkdir -p /piolibs
# First install requirements to leverage caching when requirements don't change
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
COPY requirements.txt requirements_optional.txt /
RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
# Fail on any non-zero status
set -e
# install build tools in case wheels are not available
BUILD_DEPS="
build-essential=12.9
python3-dev=3.11.2-1+b1
zlib1g-dev=1:1.2.13.dfsg-1
libjpeg-dev=1:2.1.5-2
libfreetype-dev=2.12.1+dfsg-5+deb12u4
libssl-dev=3.0.15-1~deb12u1
libffi-dev=3.4.4-1
cargo=0.66.0+ds1-1
pkg-config=1.8.1-1
"
LIB_DEPS="
libtiff6=4.5.0-6+deb12u1
libopenjp2-7=2.5.0-2+deb12u1
"
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then
apt-get update
apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS
fi
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then
apt-get remove -y --purge --auto-remove $BUILD_DEPS
rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
fi
END-OF-RUN
COPY script/platformio_install_deps.py platformio.ini / COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries RUN /platformio_install_deps.py /platformio.ini --libraries
# Avoid unsafe git error when container user and file config volume permissions don't match ARG BUILD_VERSION
RUN git config --system --add safe.directory '*'
LABEL \
org.opencontainers.image.authors="The ESPHome Authors" \
org.opencontainers.image.title="ESPHome" \
org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
org.opencontainers.image.url="https://esphome.io/" \
org.opencontainers.image.documentation="https://esphome.io/" \
org.opencontainers.image.source="https://github.com/esphome/esphome" \
org.opencontainers.image.licenses="ESPHome" \
org.opencontainers.image.version=${BUILD_VERSION}
# ======================= docker-type image ======================= # ======================= docker-type image =======================
FROM base AS docker FROM base AS base-docker
# Copy esphome and install
COPY . /esphome
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
# Settings for dashboard
ENV USERNAME="" PASSWORD=""
# Expose the dashboard to Docker # Expose the dashboard to Docker
EXPOSE 6052 EXPOSE 6052
# Run healthcheck (heartbeat) # Run healthcheck (heartbeat)
HEALTHCHECK --interval=30s --timeout=30s \ HEALTHCHECK --interval=30s --timeout=30s \
CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1 CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
COPY docker/docker_entrypoint.sh /entrypoint.sh COPY docker/docker_entrypoint.sh /entrypoint.sh
@@ -139,43 +62,13 @@ ENTRYPOINT ["/entrypoint.sh"]
CMD ["dashboard", "/config"] CMD ["dashboard", "/config"]
ARG BUILD_VERSION=dev # ======================= ha-addon-type image =======================
FROM base AS base-ha-addon
# Labels
LABEL \
org.opencontainers.image.authors="The ESPHome Authors" \
org.opencontainers.image.title="ESPHome" \
org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
org.opencontainers.image.url="https://esphome.io/" \
org.opencontainers.image.documentation="https://esphome.io/" \
org.opencontainers.image.source="https://github.com/esphome/esphome" \
org.opencontainers.image.licenses="ESPHome" \
org.opencontainers.image.version=${BUILD_VERSION}
# ======================= hassio-type image =======================
FROM base AS hassio
RUN \
apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
nginx-light=1.22.1-9+deb12u1 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
ARG BUILD_VERSION=dev
# Copy root filesystem # Copy root filesystem
COPY docker/ha-addon-rootfs/ / COPY docker/ha-addon-rootfs/ /
# Copy esphome and install ARG BUILD_VERSION
COPY . /esphome
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
# Labels
LABEL \ LABEL \
io.hass.name="ESPHome" \ io.hass.name="ESPHome" \
io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \ io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
@@ -183,35 +76,9 @@ LABEL \
io.hass.version="${BUILD_VERSION}" io.hass.version="${BUILD_VERSION}"
# io.hass.arch is inherited from addon-debian-base # io.hass.arch is inherited from addon-debian-base
ARG BUILD_TYPE
FROM base-${BUILD_TYPE} AS final
# Copy esphome and install
COPY . /esphome
# ======================= lint-type image ======================= RUN uv pip install --no-cache-dir -e /esphome
FROM base AS lint
ENV \
PLATFORMIO_CORE_DIR=/esphome/.temp/platformio
RUN \
curl -L https://apt.llvm.org/llvm-snapshot.gpg.key -o /etc/apt/trusted.gpg.d/apt.llvm.org.asc \
&& echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" > /etc/apt/sources.list.d/llvm.sources.list \
&& apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
clang-format-13=1:13.0.1-11+b2 \
patch=2.7.6-7 \
software-properties-common=0.99.30-4.1~deb12u1 \
nano=7.2-1+deb12u1 \
build-essential=12.9 \
python3-dev=3.11.2-1+b1 \
clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
COPY requirements_test.txt /
RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt
VOLUME ["/esphome"]
WORKDIR /esphome

View File

@@ -54,7 +54,7 @@ manifest_parser = subparsers.add_parser(
class DockerParams: class DockerParams:
build_to: str build_to: str
manifest_to: str manifest_to: str
baseimgtype: str build_type: str
platform: str platform: str
target: str target: str
@@ -66,24 +66,19 @@ class DockerParams:
TYPE_LINT: "esphome/esphome-lint", TYPE_LINT: "esphome/esphome-lint",
}[build_type] }[build_type]
build_to = f"{prefix}-{arch}" build_to = f"{prefix}-{arch}"
baseimgtype = {
TYPE_DOCKER: "docker",
TYPE_HA_ADDON: "hassio",
TYPE_LINT: "docker",
}[build_type]
platform = { platform = {
ARCH_AMD64: "linux/amd64", ARCH_AMD64: "linux/amd64",
ARCH_AARCH64: "linux/arm64", ARCH_AARCH64: "linux/arm64",
}[arch] }[arch]
target = { target = {
TYPE_DOCKER: "docker", TYPE_DOCKER: "final",
TYPE_HA_ADDON: "hassio", TYPE_HA_ADDON: "final",
TYPE_LINT: "lint", TYPE_LINT: "lint",
}[build_type] }[build_type]
return cls( return cls(
build_to=build_to, build_to=build_to,
manifest_to=prefix, manifest_to=prefix,
baseimgtype=baseimgtype, build_type=build_type,
platform=platform, platform=platform,
target=target, target=target,
) )
@@ -145,7 +140,7 @@ def main():
"buildx", "buildx",
"build", "build",
"--build-arg", "--build-arg",
f"BASEIMGTYPE={params.baseimgtype}", f"BUILD_TYPE={params.build_type}",
"--build-arg", "--build-arg",
f"BUILD_VERSION={args.tag}", f"BUILD_VERSION={args.tag}",
"--cache-from", "--cache-from",

View File

@@ -47,9 +47,10 @@ SAMPLING_MODES = {
adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
adc2_channel_t = cg.global_ns.enum("adc2_channel_t") adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
# pin to adc1 channel mapping # pin to adc1 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0, 36: adc1_channel_t.ADC1_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1, 37: adc1_channel_t.ADC1_CHANNEL_1,
@@ -60,6 +61,41 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
34: adc1_channel_t.ADC1_CHANNEL_6, 34: adc1_channel_t.ADC1_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7, 35: adc1_channel_t.ADC1_CHANNEL_7,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc1_channel_t.ADC1_CHANNEL_1,
@@ -72,6 +108,7 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc1_channel_t.ADC1_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc1_channel_t.ADC1_CHANNEL_1,
@@ -84,40 +121,12 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc1_channel_t.ADC1_CHANNEL_9,
}, },
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6,
},
VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
},
} }
# pin to adc2 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
# TODO: add other variants # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0, 4: adc2_channel_t.ADC2_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1, 0: adc2_channel_t.ADC2_CHANNEL_1,
@@ -130,6 +139,19 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
25: adc2_channel_t.ADC2_CHANNEL_8, 25: adc2_channel_t.ADC2_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9, 26: adc2_channel_t.ADC2_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc2_channel_t.ADC2_CHANNEL_1,
@@ -142,6 +164,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc2_channel_t.ADC2_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc2_channel_t.ADC2_CHANNEL_1,
@@ -154,12 +177,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc2_channel_t.ADC2_CHANNEL_9,
}, },
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
VARIANT_ESP32C2: {},
VARIANT_ESP32C6: {},
VARIANT_ESP32H2: {},
} }

View File

@@ -61,6 +61,7 @@ service APIConnection {
rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {} rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {}
rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
@@ -1495,6 +1496,37 @@ message BluetoothDeviceClearCacheResponse {
int32 error = 3; int32 error = 3;
} }
enum BluetoothScannerState {
BLUETOOTH_SCANNER_STATE_IDLE = 0;
BLUETOOTH_SCANNER_STATE_STARTING = 1;
BLUETOOTH_SCANNER_STATE_RUNNING = 2;
BLUETOOTH_SCANNER_STATE_FAILED = 3;
BLUETOOTH_SCANNER_STATE_STOPPING = 4;
BLUETOOTH_SCANNER_STATE_STOPPED = 5;
}
enum BluetoothScannerMode {
BLUETOOTH_SCANNER_MODE_PASSIVE = 0;
BLUETOOTH_SCANNER_MODE_ACTIVE = 1;
}
message BluetoothScannerStateResponse {
option(id) = 126;
option(source) = SOURCE_SERVER;
option(ifdef) = "USE_BLUETOOTH_PROXY";
BluetoothScannerState state = 1;
BluetoothScannerMode mode = 2;
}
message BluetoothScannerSetModeRequest {
option(id) = 127;
option(source) = SOURCE_CLIENT;
option(ifdef) = "USE_BLUETOOTH_PROXY";
BluetoothScannerMode mode = 1;
}
// ==================== PUSH TO TALK ==================== // ==================== PUSH TO TALK ====================
enum VoiceAssistantSubscribeFlag { enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;

View File

@@ -1477,6 +1477,11 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
return resp; return resp;
} }
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
}
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT

View File

@@ -221,6 +221,7 @@ class APIConnection : public APIServerConnection {
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
const SubscribeBluetoothConnectionsFreeRequest &msg) override; const SubscribeBluetoothConnectionsFreeRequest &msg) override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME

View File

@@ -422,6 +422,38 @@ const char *proto_enum_to_string<enums::BluetoothDeviceRequestType>(enums::Bluet
} }
#endif #endif
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::BluetoothScannerState>(enums::BluetoothScannerState value) {
switch (value) {
case enums::BLUETOOTH_SCANNER_STATE_IDLE:
return "BLUETOOTH_SCANNER_STATE_IDLE";
case enums::BLUETOOTH_SCANNER_STATE_STARTING:
return "BLUETOOTH_SCANNER_STATE_STARTING";
case enums::BLUETOOTH_SCANNER_STATE_RUNNING:
return "BLUETOOTH_SCANNER_STATE_RUNNING";
case enums::BLUETOOTH_SCANNER_STATE_FAILED:
return "BLUETOOTH_SCANNER_STATE_FAILED";
case enums::BLUETOOTH_SCANNER_STATE_STOPPING:
return "BLUETOOTH_SCANNER_STATE_STOPPING";
case enums::BLUETOOTH_SCANNER_STATE_STOPPED:
return "BLUETOOTH_SCANNER_STATE_STOPPED";
default:
return "UNKNOWN";
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::BluetoothScannerMode>(enums::BluetoothScannerMode value) {
switch (value) {
case enums::BLUETOOTH_SCANNER_MODE_PASSIVE:
return "BLUETOOTH_SCANNER_MODE_PASSIVE";
case enums::BLUETOOTH_SCANNER_MODE_ACTIVE:
return "BLUETOOTH_SCANNER_MODE_ACTIVE";
default:
return "UNKNOWN";
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<> template<>
const char *proto_enum_to_string<enums::VoiceAssistantSubscribeFlag>(enums::VoiceAssistantSubscribeFlag value) { const char *proto_enum_to_string<enums::VoiceAssistantSubscribeFlag>(enums::VoiceAssistantSubscribeFlag value) {
switch (value) { switch (value) {
@@ -6955,6 +6987,61 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #endif
bool BluetoothScannerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->state = value.as_enum<enums::BluetoothScannerState>();
return true;
}
case 2: {
this->mode = value.as_enum<enums::BluetoothScannerMode>();
return true;
}
default:
return false;
}
}
void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::BluetoothScannerState>(1, this->state);
buffer.encode_enum<enums::BluetoothScannerMode>(2, this->mode);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void BluetoothScannerStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothScannerStateResponse {\n");
out.append(" state: ");
out.append(proto_enum_to_string<enums::BluetoothScannerState>(this->state));
out.append("\n");
out.append(" mode: ");
out.append(proto_enum_to_string<enums::BluetoothScannerMode>(this->mode));
out.append("\n");
out.append("}");
}
#endif
bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->mode = value.as_enum<enums::BluetoothScannerMode>();
return true;
}
default:
return false;
}
}
void BluetoothScannerSetModeRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::BluetoothScannerMode>(1, this->mode);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void BluetoothScannerSetModeRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothScannerSetModeRequest {\n");
out.append(" mode: ");
out.append(proto_enum_to_string<enums::BluetoothScannerMode>(this->mode));
out.append("\n");
out.append("}");
}
#endif
bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {
case 1: { case 1: {

View File

@@ -169,6 +169,18 @@ enum BluetoothDeviceRequestType : uint32_t {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5, BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5,
BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6, BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6,
}; };
enum BluetoothScannerState : uint32_t {
BLUETOOTH_SCANNER_STATE_IDLE = 0,
BLUETOOTH_SCANNER_STATE_STARTING = 1,
BLUETOOTH_SCANNER_STATE_RUNNING = 2,
BLUETOOTH_SCANNER_STATE_FAILED = 3,
BLUETOOTH_SCANNER_STATE_STOPPING = 4,
BLUETOOTH_SCANNER_STATE_STOPPED = 5,
};
enum BluetoothScannerMode : uint32_t {
BLUETOOTH_SCANNER_MODE_PASSIVE = 0,
BLUETOOTH_SCANNER_MODE_ACTIVE = 1,
};
enum VoiceAssistantSubscribeFlag : uint32_t { enum VoiceAssistantSubscribeFlag : uint32_t {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0, VOICE_ASSISTANT_SUBSCRIBE_NONE = 0,
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1, VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1,
@@ -1770,6 +1782,29 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage {
protected: protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothScannerStateResponse : public ProtoMessage {
public:
enums::BluetoothScannerState state{};
enums::BluetoothScannerMode mode{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class BluetoothScannerSetModeRequest : public ProtoMessage {
public:
enums::BluetoothScannerMode mode{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SubscribeVoiceAssistantRequest : public ProtoMessage { class SubscribeVoiceAssistantRequest : public ProtoMessage {
public: public:
bool subscribe{false}; bool subscribe{false};

View File

@@ -472,6 +472,16 @@ bool APIServerConnectionBase::send_bluetooth_device_clear_cache_response(const B
return this->send_message_<BluetoothDeviceClearCacheResponse>(msg, 88); return this->send_message_<BluetoothDeviceClearCacheResponse>(msg, 88);
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
bool APIServerConnectionBase::send_bluetooth_scanner_state_response(const BluetoothScannerStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_bluetooth_scanner_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<BluetoothScannerStateResponse>(msg, 126);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -1212,6 +1222,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str());
#endif #endif
this->on_noise_encryption_set_key_request(msg); this->on_noise_encryption_set_key_request(msg);
#endif
break;
}
case 127: {
#ifdef USE_BLUETOOTH_PROXY
BluetoothScannerSetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_scanner_set_mode_request(msg);
#endif #endif
break; break;
} }
@@ -1705,6 +1726,19 @@ void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
this->unsubscribe_bluetooth_le_advertisements(msg); this->unsubscribe_bluetooth_le_advertisements(msg);
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->bluetooth_scanner_set_mode(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {

View File

@@ -234,6 +234,12 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
bool send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg); bool send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg);
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
bool send_bluetooth_scanner_state_response(const BluetoothScannerStateResponse &msg);
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
#endif #endif
@@ -440,6 +446,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif #endif
@@ -551,6 +560,9 @@ class APIServerConnection : public APIServerConnectionBase {
void on_unsubscribe_bluetooth_le_advertisements_request( void on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif #endif

View File

@@ -1,10 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h"
#include "esphome/components/as3935/as3935.h" #include "esphome/components/as3935/as3935.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome { namespace esphome {
namespace as3935_i2c { namespace as3935_i2c {

View File

@@ -7,7 +7,7 @@
namespace esphome { namespace esphome {
namespace as7341 { namespace as7341 {
static const uint8_t AS7341_CHIP_ID = 0X09; static const uint8_t AS7341_CHIP_ID = 0x09;
static const uint8_t AS7341_CONFIG = 0x70; static const uint8_t AS7341_CONFIG = 0x70;
static const uint8_t AS7341_LED = 0x74; static const uint8_t AS7341_LED = 0x74;

View File

@@ -48,6 +48,12 @@ def set_stream_limits(
min_sample_rate: int = _UNDEF, min_sample_rate: int = _UNDEF,
max_sample_rate: int = _UNDEF, max_sample_rate: int = _UNDEF,
): ):
"""Sets the limits for the audio stream that audio component can handle
When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive.
When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send.
"""
def set_limits_in_config(config): def set_limits_in_config(config):
if min_bits_per_sample is not _UNDEF: if min_bits_per_sample is not _UNDEF:
config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
@@ -69,43 +75,87 @@ def final_validate_audio_schema(
name: str, name: str,
*, *,
audio_device: str, audio_device: str,
bits_per_sample: int, bits_per_sample: int = _UNDEF,
channels: int, channels: int = _UNDEF,
sample_rate: int, sample_rate: int = _UNDEF,
enabled_channels: list[int] = _UNDEF,
audio_device_issue: bool = False,
): ):
"""Validates audio compatibility when passed between different components.
The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings
- If audio_device_issue is True, then the error message indicates the user should adjust the AUDIO_COMPONENT_SCHEMA derived component's configuration to match the values passed to this function
- If audio_device_issue is False, then the error message indicates the user should adjust the configuration of the component calling this function, as it falls out of the valid stream limits
Args:
name (str): Friendly name of the component calling this function with an audio component to validate
audio_device (str): The configuration parameter name that contains the ID of an AUDIO_COMPONENT_SCHEMA derived component to validate against
bits_per_sample (int, optional): The desired bits per sample
channels (int, optional): The desired number of channels
sample_rate (int, optional): The desired sample rate
enabled_channels (list[int], optional): The desired enabled channels
audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False.
"""
def validate_audio_compatiblity(audio_config): def validate_audio_compatiblity(audio_config):
audio_schema = {} audio_schema = {}
try: if bits_per_sample is not _UNDEF:
cv.int_range( try:
min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE), cv.int_range(
max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE), min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
)(bits_per_sample) max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE),
except cv.Invalid as exc: )(bits_per_sample)
raise cv.Invalid( except cv.Invalid as exc:
f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}" if audio_device_issue:
) from exc error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {bits_per_sample} bits per sample."
else:
error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
raise cv.Invalid(error_string) from exc
try: if channels is not _UNDEF:
cv.int_range( try:
min=audio_config.get(CONF_MIN_CHANNELS), cv.int_range(
max=audio_config.get(CONF_MAX_CHANNELS), min=audio_config.get(CONF_MIN_CHANNELS),
)(channels) max=audio_config.get(CONF_MAX_CHANNELS),
except cv.Invalid as exc: )(channels)
raise cv.Invalid( except cv.Invalid as exc:
f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}" if audio_device_issue:
) from exc error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {channels} channels."
else:
error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
raise cv.Invalid(error_string) from exc
try: if sample_rate is not _UNDEF:
cv.int_range( try:
min=audio_config.get(CONF_MIN_SAMPLE_RATE), cv.int_range(
max=audio_config.get(CONF_MAX_SAMPLE_RATE), min=audio_config.get(CONF_MIN_SAMPLE_RATE),
)(sample_rate) max=audio_config.get(CONF_MAX_SAMPLE_RATE),
return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config) )(sample_rate)
except cv.Invalid as exc: except cv.Invalid as exc:
raise cv.Invalid( if audio_device_issue:
f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}" error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires a {sample_rate} sample rate."
) from exc else:
error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
raise cv.Invalid(error_string) from exc
if enabled_channels is not _UNDEF:
for channel in enabled_channels:
try:
# Channels are 0-indexed
cv.int_range(
min=0,
max=audio_config.get(CONF_MAX_CHANNELS) - 1,
)(channel)
except cv.Invalid as exc:
if audio_device_issue:
error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires channel {channel}."
else:
error_string = f"Invalid configuration for the {name} component. Enabled channel {channel} {str(exc)}"
raise cv.Invalid(error_string) from exc
return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config)
return cv.Schema( return cv.Schema(
{ {

View File

@@ -135,5 +135,30 @@ const char *audio_file_type_to_string(AudioFileType file_type);
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale); size_t samples_to_scale);
/// @brief Unpacks a quantized audio sample into a Q31 fixed point number.
/// @param data Pointer to uint8_t array containing the audio sample
/// @param bytes_per_sample The number of bytes per sample
/// @return Q31 sample
inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_sample) {
int32_t sample = 0;
if (bytes_per_sample == 1) {
sample |= data[0] << 24;
} else if (bytes_per_sample == 2) {
sample |= data[0] << 16;
sample |= data[1] << 24;
} else if (bytes_per_sample == 3) {
sample |= data[0] << 8;
sample |= data[1] << 16;
sample |= data[2] << 24;
} else if (bytes_per_sample == 4) {
sample |= data[0];
sample |= data[1] << 8;
sample |= data[2] << 16;
sample |= data[3] << 24;
}
return sample;
}
} // namespace audio } // namespace audio
} // namespace esphome } // namespace esphome

View File

@@ -4,6 +4,8 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include <cstring>
namespace esphome { namespace esphome {
namespace audio { namespace audio {

View File

@@ -6,6 +6,7 @@
#include "audio_transfer_buffer.h" #include "audio_transfer_buffer.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/ring_buffer.h" #include "esphome/core/ring_buffer.h"
#ifdef USE_SPEAKER #ifdef USE_SPEAKER

View File

@@ -15,21 +15,17 @@ void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state)) if (!this->publish_dedup_.next(state))
return; return;
if (this->filter_list_ == nullptr) { if (this->filter_list_ == nullptr) {
this->send_state_internal(state, false); this->send_state_internal(state);
} else { } else {
this->filter_list_->input(state, false); this->filter_list_->input(state);
} }
} }
void BinarySensor::publish_initial_state(bool state) { void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state)) this->has_state_ = false;
return; this->publish_state(state);
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
} }
void BinarySensor::send_state_internal(bool state, bool is_initial) { void BinarySensor::send_state_internal(bool state) {
bool is_initial = !this->has_state_;
if (is_initial) { if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else { } else {

View File

@@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial); void send_state_internal(bool state);
/// Return whether this binary sensor has outputted a state. /// Return whether this binary sensor has outputted a state.
virtual bool has_state() const; virtual bool has_state() const;

View File

@@ -9,37 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) { void Filter::output(bool value) {
if (!this->dedup_.next(value)) if (!this->dedup_.next(value))
return; return;
if (this->next_ == nullptr) { if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial); this->parent_->send_state_internal(value);
} else { } else {
this->next_->input(value, is_initial); this->next_->input(value);
} }
} }
void Filter::input(bool value, bool is_initial) { void Filter::input(bool value) {
auto b = this->new_value(value, is_initial); auto b = this->new_value(value);
if (b.has_value()) { if (b.has_value()) {
this->output(*b, is_initial); this->output(*b);
} }
} }
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else { } else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
} }
return {}; return {};
} }
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {}; return {};
} else { } else {
this->cancel_timeout("ON"); this->cancel_timeout("ON");
@@ -49,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) { if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {}; return {};
} else { } else {
this->cancel_timeout("OFF"); this->cancel_timeout("OFF");
@@ -61,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; } optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {} AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) { optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) { if (value) {
// Ignore if already running // Ignore if already running
if (this->active_timing_ != 0) if (this->active_timing_ != 0)
@@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) { void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val, false); // This is at least the second one so not initial this->output(val);
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
} }
@@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {} LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value, bool is_initial) { optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) { if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true; this->steady_ = true;
this->output(value, is_initial); this->output(value);
}); });
return {}; return {};
} else { } else {
this->steady_ = false; this->steady_ = false;
this->output(value, is_initial); this->output(value);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }

View File

@@ -14,11 +14,11 @@ class BinarySensor;
class Filter { class Filter {
public: public:
virtual optional<bool> new_value(bool value, bool is_initial) = 0; virtual optional<bool> new_value(bool value) = 0;
void input(bool value, bool is_initial); void input(bool value);
void output(bool value, bool is_initial); void output(bool value);
protected: protected:
friend BinarySensor; friend BinarySensor;
@@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component { class DelayedOnOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter { class InvertFilter : public Filter {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
}; };
struct AutorepeatFilterTiming { struct AutorepeatFilterTiming {
@@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public: public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings); explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public: public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f); explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
protected: protected:
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
@@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component { class SettleFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;

View File

@@ -45,7 +45,7 @@ static const uint8_t BL0906_WRITE_COMMAND = 0xCA;
static const uint8_t BL0906_V_RMS = 0x16; static const uint8_t BL0906_V_RMS = 0x16;
// Total power // Total power
static const uint8_t BL0906_WATT_SUM = 0X2C; static const uint8_t BL0906_WATT_SUM = 0x2C;
// Current1~6 // Current1~6
static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1 static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1
@@ -56,29 +56,29 @@ static const uint8_t BL0906_I_5_RMS = 0x13;
static const uint8_t BL0906_I_6_RMS = 0x14; // current_6 static const uint8_t BL0906_I_6_RMS = 0x14; // current_6
// Power1~6 // Power1~6
static const uint8_t BL0906_WATT_1 = 0X23; // power_1 static const uint8_t BL0906_WATT_1 = 0x23; // power_1
static const uint8_t BL0906_WATT_2 = 0X24; static const uint8_t BL0906_WATT_2 = 0x24;
static const uint8_t BL0906_WATT_3 = 0X25; static const uint8_t BL0906_WATT_3 = 0x25;
static const uint8_t BL0906_WATT_4 = 0X26; static const uint8_t BL0906_WATT_4 = 0x26;
static const uint8_t BL0906_WATT_5 = 0X29; static const uint8_t BL0906_WATT_5 = 0x29;
static const uint8_t BL0906_WATT_6 = 0X2A; // power_6 static const uint8_t BL0906_WATT_6 = 0x2A; // power_6
// Active pulse count, unsigned // Active pulse count, unsigned
static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1 static const uint8_t BL0906_CF_1_CNT = 0x30; // Channel_1
static const uint8_t BL0906_CF_2_CNT = 0X31; static const uint8_t BL0906_CF_2_CNT = 0x31;
static const uint8_t BL0906_CF_3_CNT = 0X32; static const uint8_t BL0906_CF_3_CNT = 0x32;
static const uint8_t BL0906_CF_4_CNT = 0X33; static const uint8_t BL0906_CF_4_CNT = 0x33;
static const uint8_t BL0906_CF_5_CNT = 0X36; static const uint8_t BL0906_CF_5_CNT = 0x36;
static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6 static const uint8_t BL0906_CF_6_CNT = 0x37; // Channel_6
// Total active pulse count, unsigned // Total active pulse count, unsigned
static const uint8_t BL0906_CF_SUM_CNT = 0X39; static const uint8_t BL0906_CF_SUM_CNT = 0x39;
// Voltage frequency cycle // Voltage frequency cycle
static const uint8_t BL0906_FREQUENCY = 0X4E; static const uint8_t BL0906_FREQUENCY = 0x4E;
// Internal temperature // Internal temperature
static const uint8_t BL0906_TEMPERATURE = 0X5E; static const uint8_t BL0906_TEMPERATURE = 0x5E;
// Calibration register // Calibration register
// RMS gain adjustment register // RMS gain adjustment register

View File

@@ -25,6 +25,22 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
void BluetoothProxy::setup() {
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
}
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
api::BluetoothScannerStateResponse resp;
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
this->api_connection_->send_bluetooth_scanner_state_response(resp);
}
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_)
return false; return false;
@@ -453,6 +469,8 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
this->api_connection_ = api_connection; this->api_connection_ = api_connection;
this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
this->parent_->recalculate_advertisement_parser_types(); this->parent_->recalculate_advertisement_parser_types();
this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state());
} }
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
@@ -525,6 +543,17 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e
this->api_connection_->send_bluetooth_device_unpairing_response(call); this->api_connection_->send_bluetooth_device_unpairing_response(call);
} }
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
if (this->parent_->get_scan_active() == active) {
return;
}
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
this->parent_->set_scan_active(active);
this->parent_->stop_scan();
this->parent_->set_scan_continuous(
true); // Set this to true to automatically start scanning again when it has cleaned up.
}
BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace bluetooth_proxy } // namespace bluetooth_proxy

View File

@@ -41,6 +41,7 @@ enum BluetoothProxyFeature : uint32_t {
FEATURE_PAIRING = 1 << 3, FEATURE_PAIRING = 1 << 3,
FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_CACHE_CLEARING = 1 << 4,
FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_RAW_ADVERTISEMENTS = 1 << 5,
FEATURE_STATE_AND_MODE = 1 << 6,
}; };
enum BluetoothProxySubscriptionFlag : uint32_t { enum BluetoothProxySubscriptionFlag : uint32_t {
@@ -53,6 +54,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
void dump_config() override; void dump_config() override;
void setup() override;
void loop() override; void loop() override;
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
@@ -84,6 +86,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK);
void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK);
void bluetooth_scanner_set_mode(bool active);
static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) {
bd_addr[0] = (address >> 40) & 0xff; bd_addr[0] = (address >> 40) & 0xff;
bd_addr[1] = (address >> 32) & 0xff; bd_addr[1] = (address >> 32) & 0xff;
@@ -107,6 +111,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
uint32_t flags = 0; uint32_t flags = 0;
flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN; flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN;
flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS; flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS;
flags |= BluetoothProxyFeature::FEATURE_STATE_AND_MODE;
if (this->active_) { if (this->active_) {
flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS; flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS;
flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING;
@@ -124,6 +129,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
protected: protected:
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve); BluetoothConnection *get_connection_(uint64_t address, bool reserve);

View File

@@ -86,6 +86,9 @@ void Canbus::loop() {
data.push_back(can_message.data[i]); data.push_back(can_message.data[i]);
} }
this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request,
data);
// fire all triggers // fire all triggers
for (auto *trigger : this->triggers_) { for (auto *trigger : this->triggers_) {
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&

View File

@@ -81,6 +81,20 @@ class Canbus : public Component {
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void add_trigger(CanbusTrigger *trigger); void add_trigger(CanbusTrigger *trigger);
/**
* Add a callback to be called when a CAN message is received. All received messages
* are passed to the callback without filtering.
*
* The callback function receives:
* - can_id of the received data
* - extended_id True if the can_id is an extended id
* - rtr If this is a remote transmission request
* - data The message data
*/
void add_callback(
std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
this->callback_manager_.add(std::move(callback));
}
protected: protected:
template<typename... Ts> friend class CanbusSendAction; template<typename... Ts> friend class CanbusSendAction;
@@ -88,6 +102,8 @@ class Canbus : public Component {
uint32_t can_id_; uint32_t can_id_;
bool use_extended_id_; bool use_extended_id_;
CanSpeed bit_rate_; CanSpeed bit_rate_;
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
virtual bool setup_internal(); virtual bool setup_internal();
virtual Error send_message(struct CanFrame *frame); virtual Error send_message(struct CanFrame *frame);

View File

@@ -20,7 +20,7 @@ enum ClimateMode : uint8_t {
CLIMATE_MODE_FAN_ONLY = 4, CLIMATE_MODE_FAN_ONLY = 4,
/// The climate device is set to dry/humidity mode /// The climate device is set to dry/humidity mode
CLIMATE_MODE_DRY = 5, CLIMATE_MODE_DRY = 5,
/** The climate device is adjusting the temperatre dynamically. /** The climate device is adjusting the temperature dynamically.
* For example, the target temperature can be adjusted based on a schedule, or learned behavior. * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
* The target temperature can't be adjusted when in this mode. * The target temperature can't be adjusted when in this mode.
*/ */

View File

@@ -40,24 +40,24 @@ namespace climate {
*/ */
class ClimateTraits { class ClimateTraits {
public: public:
bool get_supports_current_temperature() const { return supports_current_temperature_; } bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
void set_supports_current_temperature(bool supports_current_temperature) { void set_supports_current_temperature(bool supports_current_temperature) {
supports_current_temperature_ = supports_current_temperature; this->supports_current_temperature_ = supports_current_temperature;
} }
bool get_supports_current_humidity() const { return supports_current_humidity_; } bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
void set_supports_current_humidity(bool supports_current_humidity) { void set_supports_current_humidity(bool supports_current_humidity) {
supports_current_humidity_ = supports_current_humidity; this->supports_current_humidity_ = supports_current_humidity;
} }
bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; }
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
supports_two_point_target_temperature_ = supports_two_point_target_temperature; this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
} }
bool get_supports_target_humidity() const { return supports_target_humidity_; } bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
void set_supports_target_humidity(bool supports_target_humidity) { void set_supports_target_humidity(bool supports_target_humidity) {
supports_target_humidity_ = supports_target_humidity; this->supports_target_humidity_ = supports_target_humidity;
} }
void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); } void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
@@ -72,15 +72,15 @@ class ClimateTraits {
} }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return supported_modes_; } const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
void set_supports_action(bool supports_action) { supports_action_ = supports_action; } void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
bool get_supports_action() const { return supports_action_; } bool get_supports_action() const { return this->supports_action_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); } void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
@@ -99,35 +99,37 @@ class ClimateTraits {
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); } bool get_supports_fan_modes() const {
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return supported_fan_modes_; } return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
}
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
const std::set<std::string> &get_supported_custom_fan_modes() const { return supported_custom_fan_modes_; } const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return supported_custom_fan_modes_.count(custom_fan_mode); return this->supported_custom_fan_modes_.count(custom_fan_mode);
} }
void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); } void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; } const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
supported_custom_presets_ = std::move(supported_custom_presets); this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
const std::set<std::string> &get_supported_custom_presets() const { return supported_custom_presets_; } const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return supported_custom_presets_.count(custom_preset); return this->supported_custom_presets_.count(custom_preset);
} }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { supported_swing_modes_ = std::move(modes); } void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
@@ -138,54 +140,58 @@ class ClimateTraits {
void set_supports_swing_mode_horizontal(bool supported) { void set_supports_swing_mode_horizontal(bool supported) {
set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
} }
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return supported_swing_modes_; } const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
float get_visual_min_temperature() const { return visual_min_temperature_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } void set_visual_min_temperature(float visual_min_temperature) {
float get_visual_max_temperature() const { return visual_max_temperature_; } this->visual_min_temperature_ = visual_min_temperature;
void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } }
float get_visual_target_temperature_step() const { return visual_target_temperature_step_; } float get_visual_max_temperature() const { return this->visual_max_temperature_; }
float get_visual_current_temperature_step() const { return visual_current_temperature_step_; } void set_visual_max_temperature(float visual_max_temperature) {
this->visual_max_temperature_ = visual_max_temperature;
}
float get_visual_target_temperature_step() const { return this->visual_target_temperature_step_; }
float get_visual_current_temperature_step() const { return this->visual_current_temperature_step_; }
void set_visual_target_temperature_step(float temperature_step) { void set_visual_target_temperature_step(float temperature_step) {
visual_target_temperature_step_ = temperature_step; this->visual_target_temperature_step_ = temperature_step;
} }
void set_visual_current_temperature_step(float temperature_step) { void set_visual_current_temperature_step(float temperature_step) {
visual_current_temperature_step_ = temperature_step; this->visual_current_temperature_step_ = temperature_step;
} }
void set_visual_temperature_step(float temperature_step) { void set_visual_temperature_step(float temperature_step) {
visual_target_temperature_step_ = temperature_step; this->visual_target_temperature_step_ = temperature_step;
visual_current_temperature_step_ = temperature_step; this->visual_current_temperature_step_ = temperature_step;
} }
int8_t get_target_temperature_accuracy_decimals() const; int8_t get_target_temperature_accuracy_decimals() const;
int8_t get_current_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const;
float get_visual_min_humidity() const { return visual_min_humidity_; } float get_visual_min_humidity() const { return this->visual_min_humidity_; }
void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } void set_visual_min_humidity(float visual_min_humidity) { this->visual_min_humidity_ = visual_min_humidity; }
float get_visual_max_humidity() const { return visual_max_humidity_; } float get_visual_max_humidity() const { return this->visual_max_humidity_; }
void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
supported_modes_.insert(mode); this->supported_modes_.insert(mode);
} else { } else {
supported_modes_.erase(mode); this->supported_modes_.erase(mode);
} }
} }
void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) {
if (supported) { if (supported) {
supported_fan_modes_.insert(mode); this->supported_fan_modes_.insert(mode);
} else { } else {
supported_fan_modes_.erase(mode); this->supported_fan_modes_.erase(mode);
} }
} }
void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) {
if (supported) { if (supported) {
supported_swing_modes_.insert(mode); this->supported_swing_modes_.insert(mode);
} else { } else {
supported_swing_modes_.erase(mode); this->supported_swing_modes_.erase(mode);
} }
} }

View File

@@ -32,7 +32,7 @@ const uint32_t FAN_MAX = 0x40;
// Temperature // Temperature
const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1; const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1;
const uint32_t TEMP_MASK = 0XF00; const uint32_t TEMP_MASK = 0xF00;
const uint32_t TEMP_SHIFT = 8; const uint32_t TEMP_SHIFT = 8;
const uint16_t BITS = 28; const uint16_t BITS = 28;
@@ -43,11 +43,11 @@ void LgIrClimate::transmit_state() {
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
// Set command // Set command
if (send_swing_cmd_) { if (this->send_swing_cmd_) {
send_swing_cmd_ = false; this->send_swing_cmd_ = false;
remote_state |= COMMAND_SWING; remote_state |= COMMAND_SWING;
} else { } else {
bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF); bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
switch (this->mode) { switch (this->mode) {
case climate::CLIMATE_MODE_COOL: case climate::CLIMATE_MODE_COOL:
remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL; remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
@@ -71,7 +71,7 @@ void LgIrClimate::transmit_state() {
} }
} }
mode_before_ = this->mode; this->mode_before_ = this->mode;
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
@@ -102,7 +102,7 @@ void LgIrClimate::transmit_state() {
remote_state |= ((temp - 15) << TEMP_SHIFT); remote_state |= ((temp - 15) << TEMP_SHIFT);
} }
transmit_(remote_state); this->transmit_(remote_state);
this->publish_state(); this->publish_state();
} }
@@ -187,7 +187,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
} }
void LgIrClimate::transmit_(uint32_t value) { void LgIrClimate::transmit_(uint32_t value) {
calc_checksum_(value); this->calc_checksum_(value);
ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value); ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value);
auto transmit = this->transmitter_->transmit(); auto transmit = this->transmitter_->transmit();

View File

@@ -21,7 +21,7 @@ class LgIrClimate : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device. /// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override { void control(const climate::ClimateCall &call) override {
send_swing_cmd_ = call.get_swing_mode().has_value(); this->send_swing_cmd_ = call.get_swing_mode().has_value();
// swing resets after unit powered off // swing resets after unit powered off
if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF)
this->swing_mode = climate::CLIMATE_SWING_OFF; this->swing_mode = climate::CLIMATE_SWING_OFF;

View File

@@ -0,0 +1,5 @@
"""Constants used by esphome components."""
CODEOWNERS = ["@esphome/core"]
CONF_DRAW_ROUNDING = "draw_rounding"

View File

@@ -0,0 +1,28 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from .. import cst226_ns
from ..touchscreen import CST226ButtonListener, CST226Touchscreen
CONF_CST226_ID = "cst226_id"
CST226Button = cst226_ns.class_(
"CST226Button",
binary_sensor.BinarySensor,
cg.Component,
CST226ButtonListener,
cg.Parented.template(CST226Touchscreen),
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend(
{
cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen),
}
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_CST226_ID])

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "../touchscreen/cst226_touchscreen.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace cst226 {
class CST226Button : public binary_sensor::BinarySensor,
public Component,
public CST226ButtonListener,
public Parented<CST226Touchscreen> {
public:
void setup() override;
void dump_config() override;
void update_button(bool state) override;
};
} // namespace cst226
} // namespace esphome

View File

@@ -0,0 +1,19 @@
#include "cs226_button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace cst226 {
static const char *const TAG = "CST226.binary_sensor";
void CST226Button::setup() {
this->parent_->register_button_listener(this);
this->publish_initial_state(false);
}
void CST226Button::dump_config() { LOG_BINARY_SENSOR("", "CST226 Button", this); }
void CST226Button::update_button(bool state) { this->publish_state(state); }
} // namespace cst226
} // namespace esphome

View File

@@ -3,8 +3,10 @@
namespace esphome { namespace esphome {
namespace cst226 { namespace cst226 {
static const char *const TAG = "cst226.touchscreen";
void CST226Touchscreen::setup() { void CST226Touchscreen::setup() {
esph_log_config(TAG, "Setting up CST226 Touchscreen..."); ESP_LOGCONFIG(TAG, "Setting up CST226 Touchscreen...");
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup(); this->reset_pin_->setup();
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true);
@@ -26,6 +28,11 @@ void CST226Touchscreen::update_touches() {
return; return;
} }
this->status_clear_warning(); this->status_clear_warning();
if (data[0] == 0x83 && data[1] == 0x17 && data[5] == 0x80) {
this->update_button_state_(true);
return;
}
this->update_button_state_(false);
if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) { if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
this->skip_update_ = true; this->skip_update_ = true;
return; return;
@@ -43,13 +50,21 @@ void CST226Touchscreen::update_touches() {
int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F); int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
int16_t z = data[index + 4]; int16_t z = data[index + 4];
this->add_raw_touch_position_(id, x, y, z); this->add_raw_touch_position_(id, x, y, z);
esph_log_v(TAG, "Read touch %d: %d/%d", id, x, y); ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
index += 5; index += 5;
if (i == 0) if (i == 0)
index += 2; index += 2;
} }
} }
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
this->mark_failed();
return false;
}
return true;
}
void CST226Touchscreen::continue_setup_() { void CST226Touchscreen::continue_setup_() {
uint8_t buffer[8]; uint8_t buffer[8];
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
@@ -58,7 +73,7 @@ void CST226Touchscreen::continue_setup_() {
} }
buffer[0] = 0xD1; buffer[0] = 0xD1;
if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) { if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) {
esph_log_e(TAG, "Write byte to 0xD1 failed"); ESP_LOGE(TAG, "Write byte to 0xD1 failed");
this->mark_failed(); this->mark_failed();
return; return;
} }
@@ -66,7 +81,7 @@ void CST226Touchscreen::continue_setup_() {
if (this->read16_(0xD204, buffer, 4)) { if (this->read16_(0xD204, buffer, 4)) {
uint16_t chip_id = buffer[2] + (buffer[3] << 8); uint16_t chip_id = buffer[2] + (buffer[3] << 8);
uint16_t project_id = buffer[0] + (buffer[1] << 8); uint16_t project_id = buffer[0] + (buffer[1] << 8);
esph_log_config(TAG, "Chip ID %X, project ID %x", chip_id, project_id); ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
} }
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
if (this->read16_(0xD1F8, buffer, 4)) { if (this->read16_(0xD1F8, buffer, 4)) {
@@ -80,7 +95,14 @@ void CST226Touchscreen::continue_setup_() {
} }
} }
this->setup_complete_ = true; this->setup_complete_ = true;
esph_log_config(TAG, "CST226 Touchscreen setup complete"); ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete");
}
void CST226Touchscreen::update_button_state_(bool state) {
if (this->button_touched_ == state)
return;
this->button_touched_ = state;
for (auto *listener : this->button_listeners_)
listener->update_button(state);
} }
void CST226Touchscreen::dump_config() { void CST226Touchscreen::dump_config() {

View File

@@ -9,10 +9,13 @@
namespace esphome { namespace esphome {
namespace cst226 { namespace cst226 {
static const char *const TAG = "cst226.touchscreen";
static const uint8_t CST226_REG_STATUS = 0x00; static const uint8_t CST226_REG_STATUS = 0x00;
class CST226ButtonListener {
public:
virtual void update_button(bool state) = 0;
};
class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
@@ -22,22 +25,19 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
bool can_proceed() override { return this->setup_complete_ || this->is_failed(); } bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
protected: protected:
bool read16_(uint16_t addr, uint8_t *data, size_t len) { bool read16_(uint16_t addr, uint8_t *data, size_t len);
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
esph_log_e(TAG, "Read data from 0x%04X failed", addr);
this->mark_failed();
return false;
}
return true;
}
void continue_setup_(); void continue_setup_();
void update_button_state_(bool state);
InternalGPIOPin *interrupt_pin_{}; InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{}; GPIOPin *reset_pin_{};
uint8_t chip_id_{}; uint8_t chip_id_{};
bool setup_complete_{}; bool setup_complete_{};
std::vector<CST226ButtonListener *> button_listeners_;
bool button_touched_{};
}; };
} // namespace cst226 } // namespace cst226

View File

@@ -65,7 +65,7 @@ void DaikinClimate::transmit_state() {
transmit.perform(); transmit.perform();
} }
uint8_t DaikinClimate::operation_mode_() { uint8_t DaikinClimate::operation_mode_() const {
uint8_t operating_mode = DAIKIN_MODE_ON; uint8_t operating_mode = DAIKIN_MODE_ON;
switch (this->mode) { switch (this->mode) {
case climate::CLIMATE_MODE_COOL: case climate::CLIMATE_MODE_COOL:
@@ -92,9 +92,12 @@ uint8_t DaikinClimate::operation_mode_() {
return operating_mode; return operating_mode;
} }
uint16_t DaikinClimate::fan_speed_() { uint16_t DaikinClimate::fan_speed_() const {
uint16_t fan_speed; uint16_t fan_speed;
switch (this->fan_mode.value()) { switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_QUIET:
fan_speed = DAIKIN_FAN_SILENT << 8;
break;
case climate::CLIMATE_FAN_LOW: case climate::CLIMATE_FAN_LOW:
fan_speed = DAIKIN_FAN_1 << 8; fan_speed = DAIKIN_FAN_1 << 8;
break; break;
@@ -126,12 +129,11 @@ uint16_t DaikinClimate::fan_speed_() {
return fan_speed; return fan_speed;
} }
uint8_t DaikinClimate::temperature_() { uint8_t DaikinClimate::temperature_() const {
// Force special temperatures depending on the mode // Force special temperatures depending on the mode
switch (this->mode) { switch (this->mode) {
case climate::CLIMATE_MODE_FAN_ONLY: case climate::CLIMATE_MODE_FAN_ONLY:
return 0x32; return 0x32;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_DRY: case climate::CLIMATE_MODE_DRY:
return 0xc0; return 0xc0;
default: default:
@@ -148,19 +150,25 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum)
return false; return false;
uint8_t mode = frame[5]; uint8_t mode = frame[5];
// Temperature is given in degrees celcius * 2
// only update for states that use the temperature
uint8_t temperature = frame[6];
if (mode & DAIKIN_MODE_ON) { if (mode & DAIKIN_MODE_ON) {
switch (mode & 0xF0) { switch (mode & 0xF0) {
case DAIKIN_MODE_COOL: case DAIKIN_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL; this->mode = climate::CLIMATE_MODE_COOL;
this->target_temperature = static_cast<float>(temperature * 0.5f);
break; break;
case DAIKIN_MODE_DRY: case DAIKIN_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY; this->mode = climate::CLIMATE_MODE_DRY;
break; break;
case DAIKIN_MODE_HEAT: case DAIKIN_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT; this->mode = climate::CLIMATE_MODE_HEAT;
this->target_temperature = static_cast<float>(temperature * 0.5f);
break; break;
case DAIKIN_MODE_AUTO: case DAIKIN_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->target_temperature = static_cast<float>(temperature * 0.5f);
break; break;
case DAIKIN_MODE_FAN: case DAIKIN_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY; this->mode = climate::CLIMATE_MODE_FAN_ONLY;
@@ -169,10 +177,6 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
} else { } else {
this->mode = climate::CLIMATE_MODE_OFF; this->mode = climate::CLIMATE_MODE_OFF;
} }
uint8_t temperature = frame[6];
if (!(temperature & 0xC0)) {
this->target_temperature = temperature >> 1;
}
uint8_t fan_mode = frame[8]; uint8_t fan_mode = frame[8];
uint8_t swing_mode = frame[9]; uint8_t swing_mode = frame[9];
if (fan_mode & 0xF && swing_mode & 0xF) { if (fan_mode & 0xF && swing_mode & 0xF) {
@@ -187,7 +191,6 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
switch (fan_mode & 0xF0) { switch (fan_mode & 0xF0) {
case DAIKIN_FAN_1: case DAIKIN_FAN_1:
case DAIKIN_FAN_2: case DAIKIN_FAN_2:
case DAIKIN_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_LOW; this->fan_mode = climate::CLIMATE_FAN_LOW;
break; break;
case DAIKIN_FAN_3: case DAIKIN_FAN_3:
@@ -200,6 +203,9 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
case DAIKIN_FAN_AUTO: case DAIKIN_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO; this->fan_mode = climate::CLIMATE_FAN_AUTO;
break; break;
case DAIKIN_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
} }
this->publish_state(); this->publish_state();
return true; return true;

View File

@@ -44,17 +44,17 @@ class DaikinClimate : public climate_ir::ClimateIR {
public: public:
DaikinClimate() DaikinClimate()
: climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, : climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, {climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_HIGH}, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
protected: protected:
// Transmit via IR the state of this climate controller. // Transmit via IR the state of this climate controller.
void transmit_state() override; void transmit_state() override;
uint8_t operation_mode_(); uint8_t operation_mode_() const;
uint16_t fan_speed_(); uint16_t fan_speed_() const;
uint8_t temperature_(); uint8_t temperature_() const;
// Handle received IR Buffer // Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override; bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]); bool parse_state_frame_(const uint8_t frame[]);

View File

@@ -1,6 +1,7 @@
#include "debug_component.h" #include "debug_component.h"
#include <algorithm> #include <algorithm>
#include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -25,6 +26,7 @@ void DebugComponent::dump_config() {
#ifdef USE_SENSOR #ifdef USE_SENSOR
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
@@ -86,6 +88,9 @@ void DebugComponent::update() {
this->loop_time_sensor_->publish_state(this->max_loop_time_); this->loop_time_sensor_->publish_state(this->max_loop_time_);
this->max_loop_time_ = 0; this->max_loop_time_ = 0;
} }
if (this->cpu_frequency_sensor_ != nullptr) {
this->cpu_frequency_sensor_->publish_state(arch_get_cpu_freq_hz());
}
#endif // USE_SENSOR #endif // USE_SENSOR
update_platform_(); update_platform_();

View File

@@ -34,8 +34,12 @@ class DebugComponent : public PollingComponent {
#endif #endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32 #ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; } void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32 #endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor;
}
#endif // USE_SENSOR #endif // USE_SENSOR
protected: protected:
uint32_t free_heap_{}; uint32_t free_heap_{};
@@ -53,6 +57,7 @@ class DebugComponent : public PollingComponent {
#ifdef USE_ESP32 #ifdef USE_ESP32
sensor::Sensor *psram_sensor_{nullptr}; sensor::Sensor *psram_sensor_{nullptr};
#endif // USE_ESP32 #endif // USE_ESP32
sensor::Sensor *cpu_frequency_sensor_{nullptr};
#endif // USE_SENSOR #endif // USE_SENSOR
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -75,6 +80,7 @@ class DebugComponent : public PollingComponent {
#endif // USE_TEXT_SENSOR #endif // USE_TEXT_SENSOR
std::string get_reset_reason_(); std::string get_reset_reason_();
std::string get_wakeup_cause_();
uint32_t get_free_heap_(); uint32_t get_free_heap_();
void get_device_info_(std::string &device_info); void get_device_info_(std::string &device_info);
void update_platform_(); void update_platform_();

View File

@@ -1,25 +1,18 @@
#include "debug_component.h" #include "debug_component.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <esp_sleep.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <esp_system.h> #include <esp_system.h>
#include <esp_chip_info.h> #include <esp_chip_info.h>
#include <esp_partition.h> #include <esp_partition.h>
#if defined(USE_ESP32_VARIANT_ESP32) #include <map>
#include <esp32/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32C3)
#include <esp32c3/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32C6)
#include <esp32c6/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S2)
#include <esp32s2/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S3)
#include <esp32s3/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32H2)
#include <esp32h2/rom/rtc.h>
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <Esp.h> #include <Esp.h>
#endif #endif
@@ -29,6 +22,90 @@ namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
// index by values returned by esp_reset_reason
static const char *const RESET_REASONS[] = {
"unknown source",
"power-on event",
"external pin",
"software via esp_restart",
"exception/panic",
"interrupt watchdog",
"task watchdog",
"other watchdogs",
"exiting deep sleep mode",
"brownout",
"SDIO",
"USB peripheral",
"JTAG",
"efuse error",
"power glitch detected",
"CPU lock up",
};
static const char *const REBOOT_KEY = "reboot_source";
static const size_t REBOOT_MAX_LEN = 24;
// on shutdown, store the source of the reboot request
void DebugComponent::on_shutdown() {
auto *component = App.get_current_component();
char buffer[REBOOT_MAX_LEN]{};
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
if (component != nullptr) {
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
}
ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
pref.save(&buffer);
global_preferences->sync();
}
std::string DebugComponent::get_reset_reason_() {
std::string reset_reason;
unsigned reason = esp_reset_reason();
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
reset_reason = RESET_REASONS[reason];
if (reason == ESP_RST_SW) {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
char buffer[REBOOT_MAX_LEN]{};
if (pref.load(&buffer)) {
reset_reason = "Reboot request from " + std::string(buffer);
}
}
} else {
reset_reason = "unknown source";
}
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason;
}
static const char *const WAKEUP_CAUSES[] = {
"undefined",
"undefined",
"external signal using RTC_IO",
"external signal using RTC_CNTL",
"timer",
"touchpad",
"ULP program",
"GPIO",
"UART",
"WIFI",
"COCPU int",
"COCPU crash",
"BT",
};
std::string DebugComponent::get_wakeup_cause_() {
const char *wake_reason;
unsigned reason = esp_sleep_get_wakeup_cause();
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
wake_reason = WAKEUP_CAUSES[reason];
} else {
wake_reason = "unknown source";
}
ESP_LOGD(TAG, "Wakeup Reason: %s", wake_reason);
return wake_reason;
}
void DebugComponent::log_partition_info_() { void DebugComponent::log_partition_info_() {
ESP_LOGCONFIG(TAG, "Partition table:"); ESP_LOGCONFIG(TAG, "Partition table:");
ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size"); ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size");
@@ -42,171 +119,16 @@ void DebugComponent::log_partition_info_() {
esp_partition_iterator_release(it); esp_partition_iterator_release(it);
} }
std::string DebugComponent::get_reset_reason_() {
std::string reset_reason;
switch (esp_reset_reason()) {
case ESP_RST_POWERON:
reset_reason = "Reset due to power-on event";
break;
case ESP_RST_EXT:
reset_reason = "Reset by external pin";
break;
case ESP_RST_SW:
reset_reason = "Software reset via esp_restart";
break;
case ESP_RST_PANIC:
reset_reason = "Software reset due to exception/panic";
break;
case ESP_RST_INT_WDT:
reset_reason = "Reset (software or hardware) due to interrupt watchdog";
break;
case ESP_RST_TASK_WDT:
reset_reason = "Reset due to task watchdog";
break;
case ESP_RST_WDT:
reset_reason = "Reset due to other watchdogs";
break;
case ESP_RST_DEEPSLEEP:
reset_reason = "Reset after exiting deep sleep mode";
break;
case ESP_RST_BROWNOUT:
reset_reason = "Brownout reset (software or hardware)";
break;
case ESP_RST_SDIO:
reset_reason = "Reset over SDIO";
break;
#ifdef USE_ESP32_VARIANT_ESP32
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4))
case ESP_RST_USB:
reset_reason = "Reset by USB peripheral";
break;
case ESP_RST_JTAG:
reset_reason = "Reset by JTAG";
break;
case ESP_RST_EFUSE:
reset_reason = "Reset due to efuse error";
break;
case ESP_RST_PWR_GLITCH:
reset_reason = "Reset due to power glitch detected";
break;
case ESP_RST_CPU_LOCKUP:
reset_reason = "Reset due to CPU lock up (double exception)";
break;
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4)
#endif // USE_ESP32_VARIANT_ESP32
default: // Includes ESP_RST_UNKNOWN
switch (rtc_get_reset_reason(0)) {
case POWERON_RESET:
reset_reason = "Power On Reset";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case SW_RESET:
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case RTC_SW_SYS_RESET:
#endif
reset_reason = "Software Reset Digital Core";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case OWDT_RESET:
reset_reason = "Watch Dog Reset Digital Core";
break;
#endif
case DEEPSLEEP_RESET:
reset_reason = "Deep Sleep Reset Digital Core";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case SDIO_RESET:
reset_reason = "SLC Module Reset Digital Core";
break;
#endif
case TG0WDT_SYS_RESET:
reset_reason = "Timer Group 0 Watch Dog Reset Digital Core";
break;
case TG1WDT_SYS_RESET:
reset_reason = "Timer Group 1 Watch Dog Reset Digital Core";
break;
case RTCWDT_SYS_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core";
break;
#if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
case INTRUSION_RESET:
reset_reason = "Intrusion Reset CPU";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32)
case TGWDT_CPU_RESET:
reset_reason = "Timer Group Reset CPU";
break;
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case TG0WDT_CPU_RESET:
reset_reason = "Timer Group 0 Reset CPU";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32)
case SW_CPU_RESET:
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case RTC_SW_CPU_RESET:
#endif
reset_reason = "Software Reset CPU";
break;
case RTCWDT_CPU_RESET:
reset_reason = "RTC Watch Dog Reset CPU";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case EXT_CPU_RESET:
reset_reason = "External CPU Reset";
break;
#endif
case RTCWDT_BROWN_OUT_RESET:
reset_reason = "Voltage Unstable Reset";
break;
case RTCWDT_RTC_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module";
break;
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32C6)
case TG1WDT_CPU_RESET:
reset_reason = "Timer Group 1 Reset CPU";
break;
case SUPER_WDT_RESET:
reset_reason = "Super Watchdog Reset Digital Core And RTC Module";
break;
case EFUSE_RESET:
reset_reason = "eFuse Reset Digital Core";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
case GLITCH_RTC_RESET:
reset_reason = "Glitch Reset Digital Core And RTC Module";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case USB_UART_CHIP_RESET:
reset_reason = "USB UART Reset Digital Core";
break;
case USB_JTAG_CHIP_RESET:
reset_reason = "USB JTAG Reset Digital Core";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
case POWER_GLITCH_RESET:
reset_reason = "Power Glitch Reset Digital Core And RTC Module";
break;
#endif
default:
reset_reason = "Unknown Reset Reason";
}
break;
}
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason;
}
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); } uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
static const std::map<int, const char *> CHIP_FEATURES = {
{CHIP_FEATURE_BLE, "BLE"},
{CHIP_FEATURE_BT, "BT"},
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
{CHIP_FEATURE_EMB_PSRAM, "EMB PSRAM"},
{CHIP_FEATURE_WIFI_BGN, "2.4GHz WiFi"},
};
void DebugComponent::get_device_info_(std::string &device_info) { void DebugComponent::get_device_info_(std::string &device_info) {
#if defined(USE_ARDUINO) #if defined(USE_ARDUINO)
const char *flash_mode; const char *flash_mode;
@@ -242,44 +164,16 @@ void DebugComponent::get_device_info_(std::string &device_info) {
esp_chip_info_t info; esp_chip_info_t info;
esp_chip_info(&info); esp_chip_info(&info);
const char *model; const char *model = ESPHOME_VARIANT;
#if defined(USE_ESP32_VARIANT_ESP32)
model = "ESP32";
#elif defined(USE_ESP32_VARIANT_ESP32C3)
model = "ESP32-C3";
#elif defined(USE_ESP32_VARIANT_ESP32C6)
model = "ESP32-C6";
#elif defined(USE_ESP32_VARIANT_ESP32S2)
model = "ESP32-S2";
#elif defined(USE_ESP32_VARIANT_ESP32S3)
model = "ESP32-S3";
#elif defined(USE_ESP32_VARIANT_ESP32H2)
model = "ESP32-H2";
#else
model = "UNKNOWN";
#endif
std::string features; std::string features;
if (info.features & CHIP_FEATURE_EMB_FLASH) { for (auto feature : CHIP_FEATURES) {
features += "EMB_FLASH,"; if (info.features & feature.first) {
info.features &= ~CHIP_FEATURE_EMB_FLASH; features += feature.second;
features += ", ";
info.features &= ~feature.first;
}
} }
if (info.features & CHIP_FEATURE_WIFI_BGN) { if (info.features != 0)
features += "WIFI_BGN,";
info.features &= ~CHIP_FEATURE_WIFI_BGN;
}
if (info.features & CHIP_FEATURE_BLE) {
features += "BLE,";
info.features &= ~CHIP_FEATURE_BLE;
}
if (info.features & CHIP_FEATURE_BT) {
features += "BT,";
info.features &= ~CHIP_FEATURE_BT;
}
if (info.features & CHIP_FEATURE_EMB_PSRAM) {
features += "EMB_PSRAM,";
info.features &= ~CHIP_FEATURE_EMB_PSRAM;
}
if (info.features)
features += "Other:" + format_hex(info.features); features += "Other:" + format_hex(info.features);
ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores, ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores,
info.revision); info.revision);
@@ -289,6 +183,8 @@ void DebugComponent::get_device_info_(std::string &device_info) {
device_info += features; device_info += features;
device_info += " Cores:" + to_string(info.cores); device_info += " Cores:" + to_string(info.cores);
device_info += " Revision:" + to_string(info.revision); device_info += " Revision:" + to_string(info.revision);
device_info += str_sprintf("|CPU Frequency: %" PRIu32 " MHz", arch_get_cpu_freq_hz() / 1000000);
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", arch_get_cpu_freq_hz() / 1000000);
// Framework detection // Framework detection
device_info += "|Framework: "; device_info += "|Framework: ";
@@ -315,48 +211,7 @@ void DebugComponent::get_device_info_(std::string &device_info) {
device_info += "|Reset: "; device_info += "|Reset: ";
device_info += get_reset_reason_(); device_info += get_reset_reason_();
const char *wakeup_reason; std::string wakeup_reason = this->get_wakeup_cause_();
switch (rtc_get_wakeup_cause()) {
case NO_SLEEP:
wakeup_reason = "No Sleep";
break;
case EXT_EVENT0_TRIG:
wakeup_reason = "External Event 0";
break;
case EXT_EVENT1_TRIG:
wakeup_reason = "External Event 1";
break;
case GPIO_TRIG:
wakeup_reason = "GPIO";
break;
case TIMER_EXPIRE:
wakeup_reason = "Wakeup Timer";
break;
case SDIO_TRIG:
wakeup_reason = "SDIO";
break;
case MAC_TRIG:
wakeup_reason = "MAC";
break;
case UART0_TRIG:
wakeup_reason = "UART0";
break;
case UART1_TRIG:
wakeup_reason = "UART1";
break;
case TOUCH_TRIG:
wakeup_reason = "Touch";
break;
case SAR_TRIG:
wakeup_reason = "SAR";
break;
case BT_TRIG:
wakeup_reason = "BT";
break;
default:
wakeup_reason = "Unknown";
}
ESP_LOGD(TAG, "Wakeup Reason: %s", wakeup_reason);
device_info += "|Wakeup: "; device_info += "|Wakeup: ";
device_info += wakeup_reason; device_info += wakeup_reason;
} }

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
from esphome.components.esp32 import CONF_CPU_FREQUENCY
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BLOCK, CONF_BLOCK,
@@ -10,6 +11,7 @@ from esphome.const import (
ICON_COUNTER, ICON_COUNTER,
ICON_TIMER, ICON_TIMER,
UNIT_BYTES, UNIT_BYTES,
UNIT_HERTZ,
UNIT_MILLISECOND, UNIT_MILLISECOND,
UNIT_PERCENT, UNIT_PERCENT,
) )
@@ -60,6 +62,14 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
), ),
), ),
cv.Optional(CONF_CPU_FREQUENCY): cv.All(
sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
icon="mdi:speedometer",
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
} }
@@ -85,3 +95,7 @@ async def to_code(config):
if psram_conf := config.get(CONF_PSRAM): if psram_conf := config.get(CONF_PSRAM):
sens = await sensor.new_sensor(psram_conf) sens = await sensor.new_sensor(psram_conf)
cg.add(debug_component.set_psram_sensor(sens)) cg.add(debug_component.set_psram_sensor(sens))
if cpu_freq_conf := config.get(CONF_CPU_FREQUENCY):
sens = await sensor.new_sensor(cpu_freq_conf)
cg.add(debug_component.set_cpu_frequency_sensor(sens))

View File

@@ -31,9 +31,12 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6)
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
#if !defined(USE_ESP32_VARIANT_ESP32H2)
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif #endif
#endif
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
} }
@@ -65,7 +68,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
} }
void DeepSleepComponent::deep_sleep_() { void DeepSleepComponent::deep_sleep_() {
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) { if (this->wakeup_pin_ != nullptr) {
@@ -84,6 +87,15 @@ void DeepSleepComponent::deep_sleep_() {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
} }
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32H2)
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
}
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);

View File

@@ -69,21 +69,16 @@ bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOL
return true; return true;
} }
if (absolute) { if (absolute) {
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
} else {
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
} }
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
} }
bool Rect::inside(Rect rect, bool absolute) const { bool Rect::inside(Rect rect) const {
if (!this->is_set() || !rect.is_set()) { if (!this->is_set() || !rect.is_set()) {
return true; return true;
} }
if (absolute) { return this->x2() >= rect.x && this->x <= rect.x2() && this->y2() >= rect.y && this->y <= rect.y2();
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
} else {
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
}
} }
void Rect::info(const std::string &prefix) { void Rect::info(const std::string &prefix) {

View File

@@ -26,7 +26,7 @@ class Rect {
void extend(Rect rect); void extend(Rect rect);
void shrink(Rect rect); void shrink(Rect rect);
bool inside(Rect rect, bool absolute = true) const; bool inside(Rect rect) const;
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const; bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
bool equal(Rect rect) const; bool equal(Rect rect) const;
void info(const std::string &prefix = "rect info:"); void info(const std::string &prefix = "rect info:");

View File

@@ -187,7 +187,7 @@ void ENS160Component::update() {
} }
return; return;
case INVALID_OUTPUT: case INVALID_OUTPUT:
ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output"); ESP_LOGE(TAG, "ENS160 Invalid Status - No valid output");
this->status_set_warning(); this->status_set_warning();
return; return;
} }

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
import itertools
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
@@ -37,6 +38,7 @@ from esphome.const import (
__version__, __version__,
) )
from esphome.core import CORE, HexInt, TimePeriod from esphome.core import CORE, HexInt, TimePeriod
from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
@@ -54,6 +56,12 @@ from .const import ( # noqa
KEY_SUBMODULES, KEY_SUBMODULES,
KEY_VARIANT, KEY_VARIANT,
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_FRIENDLY, VARIANT_FRIENDLY,
VARIANTS, VARIANTS,
) )
@@ -70,7 +78,43 @@ CONF_RELEASE = "release"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies]
CPU_FREQUENCIES = {
VARIANT_ESP32: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C2: get_cpu_frequencies(80, 120),
VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
}
# Make sure not missed here if a new variant added.
assert all(v in CPU_FREQUENCIES for v in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
def set_core_data(config): def set_core_data(config):
cpu_frequency = config.get(CONF_CPU_FREQUENCY, None)
variant = config[CONF_VARIANT]
# if not specified in config, set to 160MHz if supported, the fastest otherwise
if cpu_frequency is None:
choices = CPU_FREQUENCIES[variant]
if "160MHZ" in choices:
cpu_frequency = "160MHZ"
else:
cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency
elif cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"Invalid CPU frequency '{cpu_frequency}' for {config[CONF_VARIANT]}",
path=[CONF_CPU_FREQUENCY],
)
CORE.data[KEY_ESP32] = {} CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32 CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
@@ -83,6 +127,7 @@ def set_core_data(config):
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION] config[CONF_FRAMEWORK][CONF_VERSION]
) )
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT]
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@@ -553,11 +598,15 @@ FLASH_SIZES = [
] ]
CONF_FLASH_SIZE = "flash_size" CONF_FLASH_SIZE = "flash_size"
CONF_CPU_FREQUENCY = "cpu_frequency"
CONF_PARTITIONS = "partitions" CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_BOARD): cv.string_strict, cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_CPU_FREQUENCY): cv.one_of(
*FULL_CPU_FREQUENCIES, upper=True
),
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True *FLASH_SIZES, upper=True
), ),
@@ -598,6 +647,7 @@ async def to_code(config):
os.path.join(os.path.dirname(__file__), "post_build.py.script"), os.path.join(os.path.dirname(__file__), "post_build.py.script"),
) )
freq = config[CONF_CPU_FREQUENCY][:-3]
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf") cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
@@ -631,6 +681,9 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
# Set default CPU frequency
add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True)
cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config: if CONF_PARTITIONS in config:
add_extra_build_file( add_extra_build_file(
@@ -696,6 +749,7 @@ async def to_code(config):
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
cg.add(RawExpression(f"setCpuFrequencyMhz({freq})"))
APP_PARTITION_SIZES = { APP_PARTITION_SIZES = {

View File

@@ -13,11 +13,13 @@
#include <hal/cpu_hal.h> #include <hal/cpu_hal.h>
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <esp32-hal.h> #include <Esp.h>
#endif #else
#include <esp_clk_tree.h>
void setup(); void setup();
void loop(); void loop();
#endif
namespace esphome { namespace esphome {
@@ -59,9 +61,13 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
#endif #endif
uint32_t arch_get_cpu_freq_hz() { uint32_t arch_get_cpu_freq_hz() {
rtc_cpu_freq_config_t config; uint32_t freq = 0;
rtc_clk_cpu_freq_get_config(&config); #ifdef USE_ESP_IDF
return config.freq_mhz * 1000000U; esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif
return freq;
} }
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF

View File

@@ -2,10 +2,6 @@
#include "ble.h" #include "ble.h"
#ifdef USE_ESP32_VARIANT_ESP32C6
#include "const_esp32c6.h"
#endif // USE_ESP32_VARIANT_ESP32C6
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -114,6 +110,7 @@ void ESP32BLE::advertising_init_() {
this->advertising_->set_scan_response(true); this->advertising_->set_scan_response(true);
this->advertising_->set_min_preferred_interval(0x06); this->advertising_->set_min_preferred_interval(0x06);
this->advertising_->set_appearance(this->appearance_);
} }
bool ESP32BLE::ble_setup_() { bool ESP32BLE::ble_setup_() {
@@ -127,11 +124,7 @@ bool ESP32BLE::ble_setup_() {
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller // start bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
#ifdef USE_ESP32_VARIANT_ESP32C6
esp_bt_controller_config_t cfg = BT_CONTROLLER_CONFIG;
#else
esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
#endif
err = esp_bt_controller_init(&cfg); err = esp_bt_controller_init(&cfg);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err));

View File

@@ -95,6 +95,7 @@ class ESP32BLE : public Component {
void advertising_start(); void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data); void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_add_service_uuid(ESPBTUUID uuid);
void advertising_remove_service_uuid(ESPBTUUID uuid); void advertising_remove_service_uuid(ESPBTUUID uuid);
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback); void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
@@ -128,11 +129,12 @@ class ESP32BLE : public Component {
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
Queue<BLEEvent> ble_events_; Queue<BLEEvent> ble_events_;
BLEAdvertising *advertising_; BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_; uint32_t advertising_cycle_time_{};
bool enable_on_boot_; bool enable_on_boot_{};
optional<std::string> name_; optional<std::string> name_;
uint16_t appearance_{0};
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -32,6 +32,7 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data); void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data); void set_service_data(const std::vector<uint8_t> &data);
void register_raw_advertisement_callback(std::function<void(bool)> &&callback); void register_raw_advertisement_callback(std::function<void(bool)> &&callback);

View File

@@ -1,74 +0,0 @@
#pragma once
#ifdef USE_ESP32_VARIANT_ESP32C6
#include <esp_bt.h>
namespace esphome {
namespace esp32_ble {
static const esp_bt_controller_config_t BT_CONTROLLER_CONFIG = {
.config_version = CONFIG_VERSION,
.ble_ll_resolv_list_size = CONFIG_BT_LE_LL_RESOLV_LIST_SIZE,
.ble_hci_evt_hi_buf_count = DEFAULT_BT_LE_HCI_EVT_HI_BUF_COUNT,
.ble_hci_evt_lo_buf_count = DEFAULT_BT_LE_HCI_EVT_LO_BUF_COUNT,
.ble_ll_sync_list_cnt = DEFAULT_BT_LE_MAX_PERIODIC_ADVERTISER_LIST,
.ble_ll_sync_cnt = DEFAULT_BT_LE_MAX_PERIODIC_SYNCS,
.ble_ll_rsp_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT,
.ble_ll_adv_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT,
.ble_ll_tx_pwr_dbm = BLE_LL_TX_PWR_DBM_N,
.rtc_freq = RTC_FREQ_N,
.ble_ll_sca = CONFIG_BT_LE_LL_SCA,
.ble_ll_scan_phy_number = BLE_LL_SCAN_PHY_NUMBER_N,
.ble_ll_conn_def_auth_pyld_tmo = BLE_LL_CONN_DEF_AUTH_PYLD_TMO_N,
.ble_ll_jitter_usecs = BLE_LL_JITTER_USECS_N,
.ble_ll_sched_max_adv_pdu_usecs = BLE_LL_SCHED_MAX_ADV_PDU_USECS_N,
.ble_ll_sched_direct_adv_max_usecs = BLE_LL_SCHED_DIRECT_ADV_MAX_USECS_N,
.ble_ll_sched_adv_max_usecs = BLE_LL_SCHED_ADV_MAX_USECS_N,
.ble_scan_rsp_data_max_len = DEFAULT_BT_LE_SCAN_RSP_DATA_MAX_LEN_N,
.ble_ll_cfg_num_hci_cmd_pkts = BLE_LL_CFG_NUM_HCI_CMD_PKTS_N,
.ble_ll_ctrl_proc_timeout_ms = BLE_LL_CTRL_PROC_TIMEOUT_MS_N,
.nimble_max_connections = DEFAULT_BT_LE_MAX_CONNECTIONS,
.ble_whitelist_size = DEFAULT_BT_NIMBLE_WHITELIST_SIZE, // NOLINT
.ble_acl_buf_size = DEFAULT_BT_LE_ACL_BUF_SIZE,
.ble_acl_buf_count = DEFAULT_BT_LE_ACL_BUF_COUNT,
.ble_hci_evt_buf_size = DEFAULT_BT_LE_HCI_EVT_BUF_SIZE,
.ble_multi_adv_instances = DEFAULT_BT_LE_MAX_EXT_ADV_INSTANCES,
.ble_ext_adv_max_size = DEFAULT_BT_LE_EXT_ADV_MAX_SIZE,
.controller_task_stack_size = NIMBLE_LL_STACK_SIZE,
.controller_task_prio = ESP_TASK_BT_CONTROLLER_PRIO,
.controller_run_cpu = 0,
.enable_qa_test = RUN_QA_TEST,
.enable_bqb_test = RUN_BQB_TEST,
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 1)
// The following fields have been removed since ESP IDF version 5.3.1, see commit:
// https://github.com/espressif/esp-idf/commit/e761c1de8f9c0777829d597b4d5a33bb070a30a8
.enable_uart_hci = HCI_UART_EN,
.ble_hci_uart_port = DEFAULT_BT_LE_HCI_UART_PORT,
.ble_hci_uart_baud = DEFAULT_BT_LE_HCI_UART_BAUD,
.ble_hci_uart_data_bits = DEFAULT_BT_LE_HCI_UART_DATA_BITS,
.ble_hci_uart_stop_bits = DEFAULT_BT_LE_HCI_UART_STOP_BITS,
.ble_hci_uart_flow_ctrl = DEFAULT_BT_LE_HCI_UART_FLOW_CTRL,
.ble_hci_uart_uart_parity = DEFAULT_BT_LE_HCI_UART_PARITY,
#endif
.enable_tx_cca = DEFAULT_BT_LE_TX_CCA_ENABLED,
.cca_rssi_thresh = 256 - DEFAULT_BT_LE_CCA_RSSI_THRESH,
.sleep_en = NIMBLE_SLEEP_ENABLE,
.coex_phy_coded_tx_rx_time_limit = DEFAULT_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_EFF,
.dis_scan_backoff = NIMBLE_DISABLE_SCAN_BACKOFF,
.ble_scan_classify_filter_enable = 1,
.main_xtal_freq = CONFIG_XTAL_FREQ,
.version_num = (uint8_t) efuse_hal_chip_revision(),
.cpu_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
.ignore_wl_for_direct_adv = 0,
.enable_pcl = DEFAULT_BT_LE_POWER_CONTROL_ENABLED,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3)
.csa2_select = DEFAULT_BT_LE_50_FEATURE_SUPPORT,
#endif
.config_magic = CONFIG_MAGIC,
};
} // namespace esp32_ble
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32C6

View File

@@ -32,6 +32,7 @@ DEPENDENCIES = ["esp32"]
DOMAIN = "esp32_ble_server" DOMAIN = "esp32_ble_server"
CONF_ADVERTISE = "advertise" CONF_ADVERTISE = "advertise"
CONF_APPEARANCE = "appearance"
CONF_BROADCAST = "broadcast" CONF_BROADCAST = "broadcast"
CONF_CHARACTERISTICS = "characteristics" CONF_CHARACTERISTICS = "characteristics"
CONF_DESCRIPTION = "description" CONF_DESCRIPTION = "description"
@@ -421,6 +422,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(BLEServer), cv.GenerateID(): cv.declare_id(BLEServer),
cv.GenerateID(esp32_ble.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): value_schema("string", templatable=False), cv.Optional(CONF_MANUFACTURER): value_schema("string", templatable=False),
cv.Optional(CONF_APPEARANCE, default=0): cv.uint16_t,
cv.Optional(CONF_MODEL): value_schema("string", templatable=False), cv.Optional(CONF_MODEL): value_schema("string", templatable=False),
cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False), cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False),
cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]), cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]),
@@ -531,6 +533,7 @@ async def to_code(config):
cg.add(parent.register_gatts_event_handler(var)) cg.add(parent.register_gatts_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var))
cg.add(var.set_parent(parent)) cg.add(var.set_parent(parent))
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
if CONF_MANUFACTURER_DATA in config: if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
for service_config in config[CONF_SERVICES]: for service_config in config[CONF_SERVICES]:

View File

@@ -17,6 +17,7 @@ from esphome.components.esp32_ble import (
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTIVE, CONF_ACTIVE,
CONF_CONTINUOUS,
CONF_DURATION, CONF_DURATION,
CONF_ID, CONF_ID,
CONF_INTERVAL, CONF_INTERVAL,
@@ -42,7 +43,6 @@ CONF_MAX_CONNECTIONS = "max_connections"
CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_ESP32_BLE_ID = "esp32_ble_id"
CONF_SCAN_PARAMETERS = "scan_parameters" CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_WINDOW = "window" CONF_WINDOW = "window"
CONF_CONTINUOUS = "continuous"
CONF_ON_SCAN_END = "on_scan_end" CONF_ON_SCAN_END = "on_scan_end"
DEFAULT_MAX_CONNECTIONS = 3 DEFAULT_MAX_CONNECTIONS = 3

View File

@@ -57,7 +57,6 @@ void ESP32BLETracker::setup() {
global_esp32_ble_tracker = this; global_esp32_ble_tracker = this;
this->scan_result_lock_ = xSemaphoreCreateMutex(); this->scan_result_lock_ = xSemaphoreCreateMutex();
this->scan_end_lock_ = xSemaphoreCreateMutex();
#ifdef USE_OTA #ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback( ota::get_global_ota_callback()->add_on_state_callback(
@@ -117,119 +116,104 @@ void ESP32BLETracker::loop() {
} }
bool promote_to_connecting = discovered && !searching && !connecting; bool promote_to_connecting = discovered && !searching && !connecting;
if (!this->scanner_idle_) { if (this->scanner_state_ == ScannerState::RUNNING &&
if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock this->scan_result_index_ && // if it looks like we have a scan result we will take the lock
xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) {
uint32_t index = this->scan_result_index_; uint32_t index = this->scan_result_index_;
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
} }
if (this->raw_advertisements_) { if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
for (auto *client : this->clients_) {
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
}
if (this->parse_advertisements_) {
for (size_t i = 0; i < index; i++) {
ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]);
bool found = false;
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); if (listener->parse_device(device))
found = true;
} }
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); if (client->parse_device(device)) {
} found = true;
} if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true;
if (this->parse_advertisements_) {
for (size_t i = 0; i < index; i++) {
ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]);
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true;
}
} }
} }
}
if (!found && !this->scan_continuous_) { if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device); this->print_bt_device_info(device);
}
} }
} }
this->scan_result_index_ = 0;
xSemaphoreGive(this->scan_result_lock_);
} }
this->scan_result_index_ = 0;
/* xSemaphoreGive(this->scan_result_lock_);
}
Avoid starting the scanner if: if (this->scanner_state_ == ScannerState::STOPPED) {
- we are already scanning this->end_of_scan_(); // Change state to IDLE
- we are connecting to a device }
- we are disconnecting from a device if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
Otherwise the scanner could fail to ever start again this->stop_scan_();
and our only way to recover is to reboot. if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after %d attempts, rebooting to restore BLE stack...",
https://github.com/espressif/esp-idf/issues/6688 std::numeric_limits<uint8_t>::max());
App.reboot();
*/
if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) {
if (this->scan_continuous_) {
if (!disconnecting && !promote_to_connecting && !this->scan_start_failed_ && !this->scan_set_param_failed_) {
this->start_scan_(false);
} else {
// We didn't start the scan, so we need to release the lock
xSemaphoreGive(this->scan_end_lock_);
}
} else if (!this->scanner_idle_) {
this->end_of_scan_();
return;
}
} }
if (this->scan_start_failed_) {
if (this->scan_start_failed_ || this->scan_set_param_failed_) { ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) { this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after %d attempts, rebooting to restore BLE stack...", }
std::numeric_limits<uint8_t>::max()); if (this->scan_set_param_failed_) {
App.reboot(); ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
} this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
xSemaphoreGive(this->scan_end_lock_);
} else {
ESP_LOGD(TAG, "Stopping scan after failure...");
this->stop_scan_();
}
if (this->scan_start_failed_) {
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
}
if (this->scan_set_param_failed_) {
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
}
} }
} }
/*
Avoid starting the scanner if:
- we are already scanning
- we are connecting to a device
- we are disconnecting from a device
Otherwise the scanner could fail to ever start again
and our only way to recover is to reboot.
https://github.com/espressif/esp-idf/issues/6688
*/
if (this->scanner_state_ == ScannerState::IDLE && this->scan_continuous_ && !connecting && !disconnecting &&
!promote_to_connecting) {
this->start_scan_(false); // first = false
}
// If there is a discovered client and no connecting // If there is a discovered client and no connecting
// clients and no clients using the scanner to search for // clients and no clients using the scanner to search for
// devices, then stop scanning and promote the discovered // devices, then stop scanning and promote the discovered
// client to ready to connect. // client to ready to connect.
if (promote_to_connecting) { if (promote_to_connecting &&
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->state() == ClientState::DISCOVERED) { if (client->state() == ClientState::DISCOVERED) {
if (xSemaphoreTake(this->scan_end_lock_, 0L)) { if (this->scanner_state_ == ScannerState::RUNNING) {
// Scanner is not running since we got the ESP_LOGD(TAG, "Stopping scan to make connection...");
// lock, so we can promote the client. this->stop_scan_();
xSemaphoreGive(this->scan_end_lock_); } else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGD(TAG, "Promoting client to connect...");
// We only want to promote one client at a time. // We only want to promote one client at a time.
// once the scanner is fully stopped. // once the scanner is fully stopped.
client->set_state(ClientState::READY_TO_CONNECT); client->set_state(ClientState::READY_TO_CONNECT);
} else {
ESP_LOGD(TAG, "Pausing scan to make connection...");
this->stop_scan_();
} }
break; break;
} }
@@ -237,13 +221,7 @@ void ESP32BLETracker::loop() {
} }
} }
void ESP32BLETracker::start_scan() { void ESP32BLETracker::start_scan() { this->start_scan_(true); }
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
this->start_scan_(true);
} else {
ESP_LOGW(TAG, "Scan requested when a scan is already in progress. Ignoring.");
}
}
void ESP32BLETracker::stop_scan() { void ESP32BLETracker::stop_scan() {
ESP_LOGD(TAG, "Stopping scan."); ESP_LOGD(TAG, "Stopping scan.");
@@ -251,16 +229,23 @@ void ESP32BLETracker::stop_scan() {
this->stop_scan_(); this->stop_scan_();
} }
void ESP32BLETracker::ble_before_disabled_event_handler() { void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
this->stop_scan_();
xSemaphoreGive(this->scan_end_lock_);
}
void ESP32BLETracker::stop_scan_() { void ESP32BLETracker::stop_scan_() {
this->cancel_timeout("scan"); if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
if (this->scanner_idle_) { if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan is starting while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
}
return; return;
} }
this->cancel_timeout("scan");
this->set_scanner_state_(ScannerState::STOPPING);
esp_err_t err = esp_ble_gap_stop_scanning(); esp_err_t err = esp_ble_gap_stop_scanning();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err); ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err);
@@ -273,13 +258,22 @@ void ESP32BLETracker::start_scan_(bool first) {
ESP_LOGW(TAG, "Cannot start scan while ESP32BLE is disabled."); ESP_LOGW(TAG, "Cannot start scan while ESP32BLE is disabled.");
return; return;
} }
// The lock must be held when calling this function. if (this->scanner_state_ != ScannerState::IDLE) {
if (xSemaphoreTake(this->scan_end_lock_, 0L)) { if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "start_scan called without holding scan_end_lock_"); ESP_LOGE(TAG, "Cannot start scan while already starting.");
} else if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Cannot start scan while already running.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Cannot start scan while already stopping.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot start scan while already failed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Cannot start scan while already stopped.");
}
return; return;
} }
this->set_scanner_state_(ScannerState::STARTING);
ESP_LOGD(TAG, "Starting scan..."); ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
if (!first) { if (!first) {
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
@@ -307,24 +301,21 @@ void ESP32BLETracker::start_scan_(bool first) {
ESP_LOGE(TAG, "esp_ble_gap_start_scanning failed: %d", err); ESP_LOGE(TAG, "esp_ble_gap_start_scanning failed: %d", err);
return; return;
} }
this->scanner_idle_ = false;
} }
void ESP32BLETracker::end_of_scan_() { void ESP32BLETracker::end_of_scan_() {
// The lock must be held when calling this function. // The lock must be held when calling this function.
if (xSemaphoreTake(this->scan_end_lock_, 0L)) { if (this->scanner_state_ != ScannerState::STOPPED) {
ESP_LOGE(TAG, "end_of_scan_ called without holding the scan_end_lock_"); ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped.");
return; return;
} }
ESP_LOGD(TAG, "End of scan, set scanner state to IDLE.");
ESP_LOGD(TAG, "End of scan.");
this->scanner_idle_ = true;
this->already_discovered_.clear(); this->already_discovered_.clear();
xSemaphoreGive(this->scan_end_lock_);
this->cancel_timeout("scan"); this->cancel_timeout("scan");
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
this->set_scanner_state_(ScannerState::IDLE);
} }
void ESP32BLETracker::register_client(ESPBTClient *client) { void ESP32BLETracker::register_client(ESPBTClient *client) {
@@ -392,19 +383,46 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) { void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status); ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
this->scan_start_failed_ = param.status; this->scan_start_failed_ = param.status;
if (this->scanner_state_ != ScannerState::STARTING) {
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Scan was already running when start complete.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was stopping when start complete.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when start complete.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when start complete.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when start complete.");
}
}
if (param.status == ESP_BT_STATUS_SUCCESS) { if (param.status == ESP_BT_STATUS_SUCCESS) {
this->scan_start_fail_count_ = 0; this->scan_start_fail_count_ = 0;
this->set_scanner_state_(ScannerState::RUNNING);
} else { } else {
this->set_scanner_state_(ScannerState::FAILED);
if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) { if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) {
this->scan_start_fail_count_++; this->scan_start_fail_count_++;
} }
xSemaphoreGive(this->scan_end_lock_);
} }
} }
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) { void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status); ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
xSemaphoreGive(this->scan_end_lock_); if (this->scanner_state_ != ScannerState::STOPPING) {
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Scan was not running when stop complete.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when stop complete.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when stop complete.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when stop complete.");
}
}
this->set_scanner_state_(ScannerState::STOPPED);
} }
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) { void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
@@ -417,7 +435,21 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re
xSemaphoreGive(this->scan_result_lock_); xSemaphoreGive(this->scan_result_lock_);
} }
} else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { } else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
xSemaphoreGive(this->scan_end_lock_); // Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was not running when scan completed.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when scan completed.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when scan completed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
}
}
this->set_scanner_state_(ScannerState::STOPPED);
} }
} }
@@ -428,6 +460,11 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
} }
} }
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state;
this->scanner_state_callbacks_.call(state);
}
ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); }
optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) {
if (!data.uuid.contains(0x4C, 0x00)) if (!data.uuid.contains(0x4C, 0x00))
@@ -680,8 +717,26 @@ void ESP32BLETracker::dump_config() {
ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f);
ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE");
ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", YESNO(this->scan_continuous_)); ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG, " Scanner Idle: %s", YESNO(this->scanner_idle_)); switch (this->scanner_state_) {
ESP_LOGCONFIG(TAG, " Scan End: %s", YESNO(xSemaphoreGetMutexHolder(this->scan_end_lock_) == nullptr)); case ScannerState::IDLE:
ESP_LOGCONFIG(TAG, " Scanner State: IDLE");
break;
case ScannerState::STARTING:
ESP_LOGCONFIG(TAG, " Scanner State: STARTING");
break;
case ScannerState::RUNNING:
ESP_LOGCONFIG(TAG, " Scanner State: RUNNING");
break;
case ScannerState::STOPPING:
ESP_LOGCONFIG(TAG, " Scanner State: STOPPING");
break;
case ScannerState::STOPPED:
ESP_LOGCONFIG(TAG, " Scanner State: STOPPED");
break;
case ScannerState::FAILED:
ESP_LOGCONFIG(TAG, " Scanner State: FAILED");
break;
}
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_); searching_, disconnecting_);
if (this->scan_start_fail_count_) { if (this->scan_start_fail_count_) {

View File

@@ -154,6 +154,21 @@ enum class ClientState {
ESTABLISHED, ESTABLISHED,
}; };
enum class ScannerState {
// Scanner is idle, init state, set from the main loop when processing STOPPED
IDLE,
// Scanner is starting, set from the main loop only
STARTING,
// Scanner is running, set from the ESP callback only
RUNNING,
// Scanner failed to start, set from the ESP callback only
FAILED,
// Scanner is stopping, set from the main loop only
STOPPING,
// Scanner is stopped, set from the ESP callback only
STOPPED,
};
enum class ConnectionType { enum class ConnectionType {
// The default connection type, we hold all the services in ram // The default connection type, we hold all the services in ram
// for the duration of the connection. // for the duration of the connection.
@@ -203,6 +218,7 @@ class ESP32BLETracker : public Component,
void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; }
void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; }
void set_scan_active(bool scan_active) { scan_active_ = scan_active; } void set_scan_active(bool scan_active) { scan_active_ = scan_active; }
bool get_scan_active() const { return scan_active_; }
void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; } void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; }
/// Setup the FreeRTOS task and the Bluetooth stack. /// Setup the FreeRTOS task and the Bluetooth stack.
@@ -226,6 +242,11 @@ class ESP32BLETracker : public Component,
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) {
this->scanner_state_callbacks_.add(std::move(callback));
}
ScannerState get_scanner_state() const { return this->scanner_state_; }
protected: protected:
void stop_scan_(); void stop_scan_();
/// Start a single scan by setting up the parameters and doing some esp-idf calls. /// Start a single scan by setting up the parameters and doing some esp-idf calls.
@@ -240,6 +261,8 @@ class ESP32BLETracker : public Component,
void gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param); void gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param);
/// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received.
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param); void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param);
/// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
void set_scanner_state_(ScannerState state);
int app_id_{0}; int app_id_{0};
@@ -257,12 +280,12 @@ class ESP32BLETracker : public Component,
uint8_t scan_start_fail_count_{0}; uint8_t scan_start_fail_count_{0};
bool scan_continuous_; bool scan_continuous_;
bool scan_active_; bool scan_active_;
bool scanner_idle_{true}; ScannerState scanner_state_{ScannerState::IDLE};
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
bool ble_was_disabled_{true}; bool ble_was_disabled_{true};
bool raw_advertisements_{false}; bool raw_advertisements_{false};
bool parse_advertisements_{false}; bool parse_advertisements_{false};
SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_result_lock_;
SemaphoreHandle_t scan_end_lock_;
size_t scan_result_index_{0}; size_t scan_result_index_{0};
#ifdef USE_PSRAM #ifdef USE_PSRAM
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32;

View File

@@ -40,9 +40,6 @@ async def new_fastled_light(config):
if CONF_MAX_REFRESH_RATE in config: if CONF_MAX_REFRESH_RATE in config:
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
cg.add_library("fastled/FastLED", "3.9.16")
await light.register_light(var, config) await light.register_light(var, config)
# https://github.com/FastLED/FastLED/blob/master/library.json
# 3.3.3 has an issue on ESP32 with RMT and fastled_clockless:
# https://github.com/esphome/issues/issues/1375
cg.add_library("fastled/FastLED", "3.3.2")
return var return var

View File

@@ -34,7 +34,7 @@ void FastLEDLightOutput::write_state(light::LightState *state) {
this->mark_shown_(); this->mark_shown_();
ESP_LOGVV(TAG, "Writing RGB values to bus..."); ESP_LOGVV(TAG, "Writing RGB values to bus...");
this->controller_->showLeds(); this->controller_->showLeds(this->state_parent_->current_values.get_brightness() * 255);
} }
} // namespace fastled_base } // namespace fastled_base

View File

@@ -8,30 +8,45 @@ namespace esphome {
namespace gpio_expander { namespace gpio_expander {
/// @brief A class to cache the read state of a GPIO expander. /// @brief A class to cache the read state of a GPIO expander.
/// This class caches reads between GPIO Pins which are on the same bank.
/// This means that for reading whole Port (ex. 8 pins) component needs only one
/// I2C/SPI read per main loop call. It assumes, that one bit in byte identifies one GPIO pin
/// Template parameters:
/// T - Type which represents internal register. Could be uint8_t or uint16_t. Adjust to
/// match size of your internal GPIO bank register.
/// N - Number of pins
template<typename T, T N> class CachedGpioExpander { template<typename T, T N> class CachedGpioExpander {
public: public:
bool digital_read(T pin) { bool digital_read(T pin) {
if (!this->read_cache_invalidated_[pin]) { uint8_t bank = pin / (sizeof(T) * BITS_PER_BYTE);
this->read_cache_invalidated_[pin] = true; if (this->read_cache_invalidated_[bank]) {
return this->digital_read_cache(pin); this->read_cache_invalidated_[bank] = false;
if (!this->digital_read_hw(pin))
return false;
} }
return this->digital_read_hw(pin); return this->digital_read_cache(pin);
} }
void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); }
protected: protected:
/// @brief Call component low level function to read GPIO state from device
virtual bool digital_read_hw(T pin) = 0; virtual bool digital_read_hw(T pin) = 0;
/// @brief Call component read function from internal cache.
virtual bool digital_read_cache(T pin) = 0; virtual bool digital_read_cache(T pin) = 0;
/// @brief Call component low level function to write GPIO state to device
virtual void digital_write_hw(T pin, bool value) = 0; virtual void digital_write_hw(T pin, bool value) = 0;
const uint8_t cache_byte_size_ = N / (sizeof(T) * BITS_PER_BYTE);
/// @brief Invalidate cache. This function should be called in component loop().
void reset_pin_cache_() { void reset_pin_cache_() {
for (T i = 0; i < N; i++) { for (T i = 0; i < this->cache_byte_size_; i++) {
this->read_cache_invalidated_[i] = false; this->read_cache_invalidated_[i] = true;
} }
} }
std::array<bool, N> read_cache_invalidated_{}; static const uint8_t BITS_PER_BYTE = 8;
std::array<bool, N / (sizeof(T) * BITS_PER_BYTE)> read_cache_invalidated_{};
}; };
} // namespace gpio_expander } // namespace gpio_expander

View File

@@ -5,6 +5,7 @@ import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BORDER, CONF_BORDER,
CONF_COLOR, CONF_COLOR,
CONF_CONTINUOUS,
CONF_DIRECTION, CONF_DIRECTION,
CONF_DURATION, CONF_DURATION,
CONF_HEIGHT, CONF_HEIGHT,
@@ -61,8 +62,6 @@ VALUE_POSITION_TYPE = {
"BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW, "BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW,
} }
CONF_CONTINUOUS = "continuous"
GRAPH_TRACE_SCHEMA = cv.Schema( GRAPH_TRACE_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(GraphTrace), cv.GenerateID(): cv.declare_id(GraphTrace),

View File

@@ -18,6 +18,7 @@ MODELS = {
"yac": Model.GREE_YAC, "yac": Model.GREE_YAC,
"yac1fb9": Model.GREE_YAC1FB9, "yac1fb9": Model.GREE_YAC1FB9,
"yx1ff": Model.GREE_YX1FF, "yx1ff": Model.GREE_YX1FF,
"yag": Model.GREE_YAG,
} }
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(

View File

@@ -22,13 +22,21 @@ void GreeClimate::transmit_state() {
remote_state[0] = this->fan_speed_() | this->operation_mode_(); remote_state[0] = this->fan_speed_() | this->operation_mode_();
remote_state[1] = this->temperature_(); remote_state[1] = this->temperature_();
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF || this->model_ == GREE_YAG) {
remote_state[2] = 0x60; remote_state[2] = 0x60;
remote_state[3] = 0x50; remote_state[3] = 0x50;
remote_state[4] = this->vertical_swing_(); remote_state[4] = this->vertical_swing_();
} }
if (this->model_ == GREE_YAC) { if (this->model_ == GREE_YAG) {
remote_state[5] = 0x40;
if (this->vertical_swing_() == GREE_VDIR_SWING || this->horizontal_swing_() == GREE_HDIR_SWING) {
remote_state[0] |= (1 << 6);
}
}
if (this->model_ == GREE_YAC || this->model_ == GREE_YAG) {
remote_state[4] |= (this->horizontal_swing_() << 4); remote_state[4] |= (this->horizontal_swing_() << 4);
} }
@@ -57,6 +65,12 @@ void GreeClimate::transmit_state() {
// Calculate the checksum // Calculate the checksum
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else if (this->model_ == GREE_YAG) {
remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4);
} else { } else {
remote_state[7] = remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +

View File

@@ -58,7 +58,7 @@ const uint8_t GREE_VDIR_MIDDLE = 0x04;
const uint8_t GREE_VDIR_MDOWN = 0x05; const uint8_t GREE_VDIR_MDOWN = 0x05;
const uint8_t GREE_VDIR_DOWN = 0x06; const uint8_t GREE_VDIR_DOWN = 0x06;
// Only available on YAC // Only available on YAC/YAG
// Horizontal air directions. Note that these cannot be set on all heat pumps // Horizontal air directions. Note that these cannot be set on all heat pumps
const uint8_t GREE_HDIR_AUTO = 0x00; const uint8_t GREE_HDIR_AUTO = 0x00;
const uint8_t GREE_HDIR_MANUAL = 0x00; const uint8_t GREE_HDIR_MANUAL = 0x00;
@@ -78,7 +78,7 @@ const uint8_t GREE_PRESET_SLEEP = 0x01;
const uint8_t GREE_PRESET_SLEEP_BIT = 0x80; const uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Model codes // Model codes
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF }; enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
class GreeClimate : public climate_ir::ClimateIR { class GreeClimate : public climate_ir::ClimateIR {
public: public:

View File

@@ -69,7 +69,7 @@ void HLW8012Component::update() {
float power = cf_hz * this->power_multiplier_; float power = cf_hz * this->power_multiplier_;
if (this->change_mode_at_ != 0) { if (this->change_mode_at_ != 0 || this->change_mode_every_ == 0) {
// Only read cf1 after one cycle. Apparently it's quite unstable after being changed. // Only read cf1 after one cycle. Apparently it's quite unstable after being changed.
if (this->current_mode_) { if (this->current_mode_) {
float current = cf1_hz * this->current_multiplier_; float current = cf1_hz * this->current_multiplier_;

View File

@@ -8,7 +8,7 @@
namespace esphome { namespace esphome {
namespace hm3301 { namespace hm3301 {
static const uint8_t SELECT_COMM_CMD = 0X88; static const uint8_t SELECT_COMM_CMD = 0x88;
class HM3301Component : public PollingComponent, public i2c::I2CDevice { class HM3301Component : public PollingComponent, public i2c::I2CDevice {
public: public:

View File

@@ -10,9 +10,11 @@ from esphome.const import (
CONF_TIMEOUT, CONF_TIMEOUT,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_URL, CONF_URL,
PLATFORM_HOST,
__version__, __version__,
) )
from esphome.core import CORE, Lambda from esphome.core import CORE, Lambda
from esphome.helpers import IS_MACOS
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["json", "watchdog"] AUTO_LOAD = ["json", "watchdog"]
@@ -21,6 +23,7 @@ http_request_ns = cg.esphome_ns.namespace("http_request")
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component) HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent) HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent)
HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent) HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
HttpRequestHost = http_request_ns.class_("HttpRequestHost", HttpRequestComponent)
HttpContainer = http_request_ns.class_("HttpContainer") HttpContainer = http_request_ns.class_("HttpContainer")
@@ -43,10 +46,13 @@ CONF_REDIRECT_LIMIT = "redirect_limit"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
CONF_BUFFER_SIZE_RX = "buffer_size_rx" CONF_BUFFER_SIZE_RX = "buffer_size_rx"
CONF_BUFFER_SIZE_TX = "buffer_size_tx" CONF_BUFFER_SIZE_TX = "buffer_size_tx"
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
CONF_ON_RESPONSE = "on_response" CONF_ON_RESPONSE = "on_response"
CONF_HEADERS = "headers" CONF_HEADERS = "headers"
CONF_REQUEST_HEADERS = "request_headers"
CONF_COLLECT_HEADERS = "collect_headers"
CONF_BODY = "body" CONF_BODY = "body"
CONF_JSON = "json" CONF_JSON = "json"
CONF_CAPTURE_RESPONSE = "capture_response" CONF_CAPTURE_RESPONSE = "capture_response"
@@ -85,6 +91,8 @@ def validate_ssl_verification(config):
def _declare_request_class(value): def _declare_request_class(value):
if CORE.is_host:
return cv.declare_id(HttpRequestHost)(value)
if CORE.using_esp_idf: if CORE.using_esp_idf:
return cv.declare_id(HttpRequestIDF)(value) return cv.declare_id(HttpRequestIDF)(value)
if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040: if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
@@ -119,6 +127,10 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32_idf=512): cv.All( cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32_idf=512): cv.All(
cv.uint16_t, cv.only_with_esp_idf cv.uint16_t, cv.only_with_esp_idf
), ),
cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All(
cv.file_,
cv.only_on(PLATFORM_HOST),
),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version( cv.require_framework_version(
@@ -126,6 +138,7 @@ CONFIG_SCHEMA = cv.All(
esp32_arduino=cv.Version(0, 0, 0), esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(0, 0, 0), esp_idf=cv.Version(0, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
), ),
validate_ssl_verification, validate_ssl_verification,
) )
@@ -168,6 +181,21 @@ async def to_code(config):
cg.add_library("ESP8266HTTPClient", None) cg.add_library("ESP8266HTTPClient", None)
if CORE.is_rp2040 and CORE.using_arduino: if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("HTTPClient", None) cg.add_library("HTTPClient", None)
if CORE.is_host:
if IS_MACOS:
cg.add_build_flag("-I/opt/homebrew/opt/openssl/include")
cg.add_build_flag("-L/opt/homebrew/opt/openssl/lib")
cg.add_build_flag("-lssl")
cg.add_build_flag("-lcrypto")
cg.add_build_flag("-Wl,-framework,CoreFoundation")
cg.add_build_flag("-Wl,-framework,Security")
cg.add_define("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
cg.add_define("CPPHTTPLIB_OPENSSL_SUPPORT")
elif path := config.get(CONF_CA_CERTIFICATE_PATH):
cg.add_define("CPPHTTPLIB_OPENSSL_SUPPORT")
cg.add(var.set_ca_path(path))
cg.add_build_flag("-lssl")
cg.add_build_flag("-lcrypto")
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -176,9 +204,13 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.use_id(HttpRequestComponent), cv.GenerateID(): cv.use_id(HttpRequestComponent),
cv.Required(CONF_URL): cv.templatable(validate_url), cv.Required(CONF_URL): cv.templatable(validate_url),
cv.Optional(CONF_HEADERS): cv.All( cv.Optional(CONF_HEADERS): cv.invalid(
"The 'headers' options has been renamed to 'request_headers'"
),
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)}) cv.Schema({cv.string: cv.templatable(cv.string)})
), ),
cv.Optional(CONF_COLLECT_HEADERS): cv.ensure_list(cv.string),
cv.Optional(CONF_VERIFY_SSL): cv.invalid( cv.Optional(CONF_VERIFY_SSL): cv.invalid(
f"{CONF_VERIFY_SSL} has moved to the base component configuration." f"{CONF_VERIFY_SSL} has moved to the base component configuration."
), ),
@@ -263,11 +295,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
for key in json_: for key in json_:
template_ = await cg.templatable(json_[key], args, cg.std_string) template_ = await cg.templatable(json_[key], args, cg.std_string)
cg.add(var.add_json(key, template_)) cg.add(var.add_json(key, template_))
for key in config.get(CONF_HEADERS, []): for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
template_ = await cg.templatable( template_ = await cg.templatable(value, args, cg.const_char_ptr)
config[CONF_HEADERS][key], args, cg.const_char_ptr cg.add(var.add_request_header(key, template_))
)
cg.add(var.add_header(key, template_)) for value in config.get(CONF_COLLECT_HEADERS, []):
cg.add(var.add_collect_header(value))
for conf in config.get(CONF_ON_RESPONSE, []): for conf in config.get(CONF_ON_RESPONSE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])

View File

@@ -20,5 +20,25 @@ void HttpRequestComponent::dump_config() {
} }
} }
std::string HttpContainer::get_response_header(const std::string &header_name) {
auto response_headers = this->get_response_headers();
auto header_name_lower_case = str_lower_case(header_name);
if (response_headers.count(header_name_lower_case) == 0) {
ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str());
return "";
} else {
auto values = response_headers[header_name_lower_case];
if (values.empty()) {
ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen",
header_name_lower_case.c_str());
return "";
} else {
auto header_value = values.front();
ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str());
return header_value;
}
}
}
} // namespace http_request } // namespace http_request
} // namespace esphome } // namespace esphome

View File

@@ -3,6 +3,7 @@
#include <list> #include <list>
#include <map> #include <map>
#include <memory> #include <memory>
#include <set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -95,9 +96,19 @@ class HttpContainer : public Parented<HttpRequestComponent> {
size_t get_bytes_read() const { return this->bytes_read_; } size_t get_bytes_read() const { return this->bytes_read_; }
/**
* @brief Get response headers.
*
* @return The key is the lower case response header name, the value is the header value.
*/
std::map<std::string, std::list<std::string>> get_response_headers() { return this->response_headers_; }
std::string get_response_header(const std::string &header_name);
protected: protected:
size_t bytes_read_{0}; size_t bytes_read_{0};
bool secure_{false}; bool secure_{false};
std::map<std::string, std::list<std::string>> response_headers_{};
}; };
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> { class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
@@ -119,21 +130,46 @@ class HttpRequestComponent : public Component {
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
std::shared_ptr<HttpContainer> get(std::string url) { return this->start(std::move(url), "GET", "", {}); } std::shared_ptr<HttpContainer> get(const std::string &url) { return this->start(url, "GET", "", {}); }
std::shared_ptr<HttpContainer> get(std::string url, std::list<Header> headers) { std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers) {
return this->start(std::move(url), "GET", "", std::move(headers)); return this->start(url, "GET", "", request_headers);
} }
std::shared_ptr<HttpContainer> post(std::string url, std::string body) { std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers,
return this->start(std::move(url), "POST", std::move(body), {}); const std::set<std::string> &collect_headers) {
return this->start(url, "GET", "", request_headers, collect_headers);
} }
std::shared_ptr<HttpContainer> post(std::string url, std::string body, std::list<Header> headers) { std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
return this->start(std::move(url), "POST", std::move(body), std::move(headers)); return this->start(url, "POST", body, {});
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::list<Header> &request_headers) {
return this->start(url, "POST", body, request_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
return this->start(url, "POST", body, request_headers, collect_headers);
} }
virtual std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body, std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
std::list<Header> headers) = 0; const std::list<Header> &request_headers) {
return this->start(url, method, body, request_headers, {});
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
std::set<std::string> lower_case_collect_headers;
for (const std::string &collect_header : collect_headers) {
lower_case_collect_headers.insert(str_lower_case(collect_header));
}
return this->perform(url, method, body, request_headers, lower_case_collect_headers);
}
protected: protected:
virtual std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::set<std::string> collect_headers) = 0;
const char *useragent_{nullptr}; const char *useragent_{nullptr};
bool follow_redirects_{}; bool follow_redirects_{};
uint16_t redirect_limit_{}; uint16_t redirect_limit_{};
@@ -149,7 +185,11 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, body) TEMPLATABLE_VALUE(std::string, body)
TEMPLATABLE_VALUE(bool, capture_response) TEMPLATABLE_VALUE(bool, capture_response)
void add_header(const char *key, TemplatableValue<const char *, Ts...> value) { this->headers_.insert({key, value}); } void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) {
this->request_headers_.insert({key, value});
}
void add_collect_header(const char *value) { this->collect_headers_.insert(value); }
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.insert({key, value}); } void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.insert({key, value}); }
@@ -176,16 +216,17 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1); auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
body = json::build_json(f); body = json::build_json(f);
} }
std::list<Header> headers; std::list<Header> request_headers;
for (const auto &item : this->headers_) { for (const auto &item : this->request_headers_) {
auto val = item.second; auto val = item.second;
Header header; Header header;
header.name = item.first; header.name = item.first;
header.value = val.value(x...); header.value = val.value(x...);
headers.push_back(header); request_headers.push_back(header);
} }
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers); auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
this->collect_headers_);
if (container == nullptr) { if (container == nullptr) {
for (auto *trigger : this->error_triggers_) for (auto *trigger : this->error_triggers_)
@@ -238,7 +279,8 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
} }
void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); }
HttpRequestComponent *parent_; HttpRequestComponent *parent_;
std::map<const char *, TemplatableValue<const char *, Ts...>> headers_{}; std::map<const char *, TemplatableValue<const char *, Ts...>> request_headers_{};
std::set<std::string> collect_headers_{"content-type", "content-length"};
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{}; std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr}; std::function<void(Ts..., JsonObject)> json_func_{nullptr};
std::vector<HttpRequestResponseTrigger *> response_triggers_{}; std::vector<HttpRequestResponseTrigger *> response_triggers_{};

View File

@@ -14,8 +14,9 @@ namespace http_request {
static const char *const TAG = "http_request.arduino"; static const char *const TAG = "http_request.arduino";
std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::string method, std::string body, std::shared_ptr<HttpContainer> HttpRequestArduino::perform(std::string url, std::string method, std::string body,
std::list<Header> headers) { std::list<Header> request_headers,
std::set<std::string> collect_headers) {
if (!network::is_connected()) { if (!network::is_connected()) {
this->status_momentary_error("failed", 1000); this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
@@ -95,14 +96,17 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
if (this->useragent_ != nullptr) { if (this->useragent_ != nullptr) {
container->client_.setUserAgent(this->useragent_); container->client_.setUserAgent(this->useragent_);
} }
for (const auto &header : headers) { for (const auto &header : request_headers) {
container->client_.addHeader(header.name.c_str(), header.value.c_str(), false, true); container->client_.addHeader(header.name.c_str(), header.value.c_str(), false, true);
} }
// returned needed headers must be collected before the requests // returned needed headers must be collected before the requests
static const char *header_keys[] = {"Content-Length", "Content-Type"}; const char *header_keys[collect_headers.size()];
static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]); int index = 0;
container->client_.collectHeaders(header_keys, HEADER_COUNT); for (auto const &header_name : collect_headers) {
header_keys[index++] = header_name.c_str();
}
container->client_.collectHeaders(header_keys, index);
App.feed_wdt(); App.feed_wdt();
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
@@ -121,6 +125,18 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
// Still return the container, so it can be used to get the status code and error message // Still return the container, so it can be used to get the status code and error message
} }
container->response_headers_ = {};
auto header_count = container->client_.headers();
for (int i = 0; i < header_count; i++) {
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
if (collect_headers.count(header_name) > 0) {
std::string header_value = container->client_.header(i).c_str();
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
container->response_headers_[header_name].push_back(header_value);
break;
}
}
int content_length = container->client_.getSize(); int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length); ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length; container->content_length = (size_t) content_length;

View File

@@ -29,9 +29,10 @@ class HttpContainerArduino : public HttpContainer {
}; };
class HttpRequestArduino : public HttpRequestComponent { class HttpRequestArduino : public HttpRequestComponent {
public: protected:
std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body, std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> headers) override; std::list<Header> request_headers,
std::set<std::string> collect_headers) override;
}; };
} // namespace http_request } // namespace http_request

View File

@@ -0,0 +1,141 @@
#include "http_request_host.h"
#ifdef USE_HOST
#include <regex>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
namespace http_request {
static const char *const TAG = "http_request.host";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::set<std::string> response_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
return nullptr;
}
std::regex url_regex(R"(^(([^:\/?#]+):)?(//([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)", std::regex::extended);
std::smatch url_match_result;
if (!std::regex_match(url, url_match_result, url_regex) || url_match_result.length() < 7) {
ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url.c_str());
return nullptr;
}
auto host = url_match_result[4].str();
auto scheme_host = url_match_result[1].str() + url_match_result[3].str();
auto path = url_match_result[5].str() + url_match_result[6].str();
if (path.empty())
path = "/";
std::shared_ptr<HttpContainerHost> container = std::make_shared<HttpContainerHost>();
container->set_parent(this);
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
httplib::Headers h_headers;
h_headers.emplace("Host", host.c_str());
h_headers.emplace("User-Agent", this->useragent_);
for (const auto &[name, value] : request_headers) {
h_headers.emplace(name, value);
}
httplib::Client client(scheme_host.c_str());
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url.c_str());
return nullptr;
}
client.set_follow_location(this->follow_redirects_);
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (this->ca_path_ != nullptr)
client.set_ca_cert_path(this->ca_path_);
#endif
httplib::Result result;
if (method == "GET") {
result = client.Get(path, h_headers, [&](const char *data, size_t data_length) {
ESP_LOGV(TAG, "Got data length: %zu", data_length);
container->response_body_.insert(container->response_body_.end(), (const uint8_t *) data,
(const uint8_t *) data + data_length);
return true;
});
} else if (method == "HEAD") {
result = client.Head(path, h_headers);
} else if (method == "PUT") {
result = client.Put(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else if (method == "PATCH") {
result = client.Patch(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else if (method == "POST") {
result = client.Post(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else {
ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method.c_str(), url.c_str());
container->end();
return nullptr;
}
App.feed_wdt();
if (!result) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url.c_str(), (unsigned) result.error());
container->end();
this->status_momentary_error("failed", 1000);
return nullptr;
}
App.feed_wdt();
auto response = *result;
container->status_code = response.status;
if (!is_success(response.status)) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), response.status);
this->status_momentary_error("failed", 1000);
// Still return the container, so it can be used to get the status code and error message
}
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first);
if (response_headers.find(lower_name) != response_headers.end()) {
container->response_headers_[lower_name].emplace_back(header.second);
}
}
container->duration_ms = millis() - start;
return container;
}
int HttpContainerHost::read(uint8_t *buf, size_t max_len) {
auto bytes_remaining = this->response_body_.size() - this->bytes_read_;
auto read_len = std::min(max_len, bytes_remaining);
memcpy(buf, this->response_body_.data() + this->bytes_read_, read_len);
this->bytes_read_ += read_len;
return read_len;
}
void HttpContainerHost::end() {
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->response_body_ = std::vector<uint8_t>();
this->bytes_read_ = 0;
}
} // namespace http_request
} // namespace esphome
#endif // USE_HOST

View File

@@ -0,0 +1,37 @@
#pragma once
#include "http_request.h"
#ifdef USE_HOST
#define CPPHTTPLIB_NO_EXCEPTIONS
#include "httplib.h"
namespace esphome {
namespace http_request {
class HttpRequestHost;
class HttpContainerHost : public HttpContainer {
public:
int read(uint8_t *buf, size_t max_len) override;
void end() override;
protected:
friend class HttpRequestHost;
std::vector<uint8_t> response_body_{};
};
class HttpRequestHost : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::set<std::string> response_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
protected:
const char *ca_path_{};
};
} // namespace http_request
} // namespace esphome
#endif // USE_HOST

View File

@@ -19,14 +19,41 @@ namespace http_request {
static const char *const TAG = "http_request.idf"; static const char *const TAG = "http_request.idf";
struct UserData {
const std::set<std::string> &collect_headers;
std::map<std::string, std::list<std::string>> response_headers;
};
void HttpRequestIDF::dump_config() { void HttpRequestIDF::dump_config() {
HttpRequestComponent::dump_config(); HttpRequestComponent::dump_config();
ESP_LOGCONFIG(TAG, " Buffer Size RX: %u", this->buffer_size_rx_); ESP_LOGCONFIG(TAG, " Buffer Size RX: %u", this->buffer_size_rx_);
ESP_LOGCONFIG(TAG, " Buffer Size TX: %u", this->buffer_size_tx_); ESP_LOGCONFIG(TAG, " Buffer Size TX: %u", this->buffer_size_tx_);
} }
std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::string method, std::string body, esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
std::list<Header> headers) { UserData *user_data = (UserData *) evt->user_data;
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER: {
const std::string header_name = str_lower_case(evt->header_key);
if (user_data->collect_headers.count(header_name)) {
const std::string header_value = evt->header_value;
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
user_data->response_headers[header_name].push_back(header_value);
break;
}
break;
}
default: {
break;
}
}
return ESP_OK;
}
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::set<std::string> collect_headers) {
if (!network::is_connected()) { if (!network::is_connected()) {
this->status_momentary_error("failed", 1000); this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
@@ -76,6 +103,10 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
config.event_handler = http_event_handler;
auto user_data = UserData{collect_headers, {}};
config.user_data = static_cast<void *>(&user_data);
esp_http_client_handle_t client = esp_http_client_init(&config); esp_http_client_handle_t client = esp_http_client_init(&config);
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client); std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
@@ -83,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
container->set_secure(secure); container->set_secure(secure);
for (const auto &header : headers) { for (const auto &header : request_headers) {
esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
} }
@@ -124,6 +155,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
container->feed_wdt(); container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client); container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt(); container->feed_wdt();
container->set_response_headers(user_data.response_headers);
if (is_success(container->status_code)) { if (is_success(container->status_code)) {
container->duration_ms = millis() - start; container->duration_ms = millis() - start;
return container; return container;

View File

@@ -21,6 +21,10 @@ class HttpContainerIDF : public HttpContainer {
/// @brief Feeds the watchdog timer if the executing task has one attached /// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt(); void feed_wdt();
void set_response_headers(std::map<std::string, std::list<std::string>> &response_headers) {
this->response_headers_ = std::move(response_headers);
}
protected: protected:
esp_http_client_handle_t client_; esp_http_client_handle_t client_;
}; };
@@ -29,16 +33,19 @@ class HttpRequestIDF : public HttpRequestComponent {
public: public:
void dump_config() override; void dump_config() override;
std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
std::list<Header> headers) override;
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; } void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; } void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
protected: protected:
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::set<std::string> collect_headers) override;
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{}; uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{}; uint16_t buffer_size_tx_{};
/// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
}; };
} // namespace http_request } // namespace http_request

File diff suppressed because it is too large Load Diff

View File

@@ -139,6 +139,10 @@ class I2CDevice {
/// @param address of the device /// @param address of the device
void set_i2c_address(uint8_t address) { address_ = address; } void set_i2c_address(uint8_t address) { address_ = address; }
/// @brief Returns the I2C address of the object.
/// @return the I2C address
uint8_t get_i2c_address() const { return this->address_; }
/// @brief we store the pointer to the I2CBus to use /// @brief we store the pointer to the I2CBus to use
/// @param bus pointer to the I2CBus object /// @param bus pointer to the I2CBus object
void set_i2c_bus(I2CBus *bus) { bus_ = bus; } void set_i2c_bus(I2CBus *bus) { bus_ = bus; }

View File

@@ -67,7 +67,7 @@ void IDFI2CBus::setup() {
ESP_LOGV(TAG, "i2c_timeout set to %" PRIu32 " ticks (%" PRIu32 " us)", timeout_ * 80, timeout_); ESP_LOGV(TAG, "i2c_timeout set to %" PRIu32 " ticks (%" PRIu32 " us)", timeout_ * 80, timeout_);
} }
} }
err = i2c_driver_install(port_, I2C_MODE_MASTER, 0, 0, ESP_INTR_FLAG_IRAM); err = i2c_driver_install(port_, I2C_MODE_MASTER, 0, 0, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err));
this->mark_failed(); this->mark_failed();

View File

@@ -8,7 +8,15 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_CHANNEL,
CONF_ID,
CONF_SAMPLE_RATE,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv import esphome.final_validate as fv
@@ -31,10 +39,14 @@ CONF_SECONDARY = "secondary"
CONF_USE_APLL = "use_apll" CONF_USE_APLL = "use_apll"
CONF_BITS_PER_CHANNEL = "bits_per_channel" CONF_BITS_PER_CHANNEL = "bits_per_channel"
CONF_MCLK_MULTIPLE = "mclk_multiple"
CONF_MONO = "mono" CONF_MONO = "mono"
CONF_LEFT = "left" CONF_LEFT = "left"
CONF_RIGHT = "right" CONF_RIGHT = "right"
CONF_STEREO = "stereo" CONF_STEREO = "stereo"
CONF_BOTH = "both"
CONF_USE_LEGACY = "use_legacy"
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component)
@@ -50,6 +62,12 @@ I2S_MODE_OPTIONS = {
CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT
} }
i2s_role_t = cg.global_ns.enum("i2s_role_t")
I2S_ROLE_OPTIONS = {
CONF_PRIMARY: i2s_role_t.I2S_ROLE_MASTER, # NOLINT
CONF_SECONDARY: i2s_role_t.I2S_ROLE_SLAVE, # NOLINT
}
# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h # https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h
I2S_PORTS = { I2S_PORTS = {
VARIANT_ESP32: 2, VARIANT_ESP32: 2,
@@ -60,10 +78,23 @@ I2S_PORTS = {
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
I2S_CHANNELS = { I2S_CHANNELS = {
CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, # left data to both channels
CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, # mono data
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, # mono data
CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, # stereo data to both channels
}
i2s_slot_mode_t = cg.global_ns.enum("i2s_slot_mode_t")
I2S_SLOT_MODE = {
CONF_MONO: i2s_slot_mode_t.I2S_SLOT_MODE_MONO,
CONF_STEREO: i2s_slot_mode_t.I2S_SLOT_MODE_STEREO,
}
i2s_std_slot_mask_t = cg.global_ns.enum("i2s_std_slot_mask_t")
I2S_STD_SLOT_MASK = {
CONF_LEFT: i2s_std_slot_mask_t.I2S_STD_SLOT_LEFT,
CONF_RIGHT: i2s_std_slot_mask_t.I2S_STD_SLOT_RIGHT,
CONF_BOTH: i2s_std_slot_mask_t.I2S_STD_SLOT_BOTH,
} }
i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t")
@@ -83,9 +114,37 @@ I2S_BITS_PER_CHANNEL = {
32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT, 32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT,
} }
i2s_slot_bit_width_t = cg.global_ns.enum("i2s_slot_bit_width_t")
I2S_SLOT_BIT_WIDTH = {
"default": i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_AUTO,
8: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_8BIT,
16: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_16BIT,
24: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_24BIT,
32: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_32BIT,
}
i2s_mclk_multiple_t = cg.global_ns.enum("i2s_mclk_multiple_t")
I2S_MCLK_MULTIPLE = {
128: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_128,
256: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_256,
384: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_384,
512: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_512,
}
_validate_bits = cv.float_with_unit("bits", "bit") _validate_bits = cv.float_with_unit("bits", "bit")
def validate_mclk_divisible_by_3(config):
if config[CONF_BITS_PER_SAMPLE] == 24 and config[CONF_MCLK_MULTIPLE] % 3 != 0:
raise cv.Invalid(
f"{CONF_MCLK_MULTIPLE} must be divisible by 3 when bits per sample is 24"
)
return config
_use_legacy_driver = None
def i2s_audio_component_schema( def i2s_audio_component_schema(
class_: MockObjClass, class_: MockObjClass,
*, *,
@@ -97,43 +156,83 @@ def i2s_audio_component_schema(
{ {
cv.GenerateID(): cv.declare_id(class_), cv.GenerateID(): cv.declare_id(class_),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Optional(CONF_CHANNEL, default=default_channel): cv.enum(I2S_CHANNELS), cv.Optional(CONF_CHANNEL, default=default_channel): cv.one_of(
*I2S_CHANNELS
),
cv.Optional(CONF_SAMPLE_RATE, default=default_sample_rate): cv.int_range( cv.Optional(CONF_SAMPLE_RATE, default=default_sample_rate): cv.int_range(
min=1 min=1
), ),
cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All( cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All(
_validate_bits, cv.enum(I2S_BITS_PER_SAMPLE) _validate_bits, cv.one_of(*I2S_BITS_PER_SAMPLE)
), ),
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum( cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.one_of(
I2S_MODE_OPTIONS, lower=True *I2S_MODE_OPTIONS, lower=True
), ),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean, cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All( cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All(
cv.Any(cv.float_with_unit("bits", "bit"), "default"), cv.Any(cv.float_with_unit("bits", "bit"), "default"),
cv.enum(I2S_BITS_PER_CHANNEL), cv.one_of(*I2S_BITS_PER_CHANNEL),
), ),
cv.Optional(CONF_MCLK_MULTIPLE, default=256): cv.one_of(*I2S_MCLK_MULTIPLE),
} }
) )
async def register_i2s_audio_component(var, config): async def register_i2s_audio_component(var, config):
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
if use_legacy():
cg.add(var.set_i2s_mode(config[CONF_I2S_MODE])) cg.add(var.set_i2s_mode(I2S_MODE_OPTIONS[config[CONF_I2S_MODE]]))
cg.add(var.set_channel(config[CONF_CHANNEL])) cg.add(var.set_channel(I2S_CHANNELS[config[CONF_CHANNEL]]))
cg.add(
var.set_bits_per_sample(I2S_BITS_PER_SAMPLE[config[CONF_BITS_PER_SAMPLE]])
)
cg.add(
var.set_bits_per_channel(
I2S_BITS_PER_CHANNEL[config[CONF_BITS_PER_CHANNEL]]
)
)
else:
cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]]))
slot_mode = config[CONF_CHANNEL]
if slot_mode != CONF_STEREO:
slot_mode = CONF_MONO
slot_mask = config[CONF_CHANNEL]
if slot_mask not in [CONF_LEFT, CONF_RIGHT]:
slot_mask = CONF_BOTH
cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode]))
cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask]))
cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_bits_per_channel(config[CONF_BITS_PER_CHANNEL]))
cg.add(var.set_use_apll(config[CONF_USE_APLL])) cg.add(var.set_use_apll(config[CONF_USE_APLL]))
cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]]))
CONFIG_SCHEMA = cv.Schema( def validate_use_legacy(value):
{ global _use_legacy_driver # noqa: PLW0603
cv.GenerateID(): cv.declare_id(I2SAudioComponent), if CONF_USE_LEGACY in value:
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, if (_use_legacy_driver is not None) and (
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, _use_legacy_driver != value[CONF_USE_LEGACY]
cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, ):
} raise cv.Invalid(
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
)
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
raise cv.Invalid("Arduino supports only the legacy i2s driver.")
_use_legacy_driver = value[CONF_USE_LEGACY]
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioComponent),
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_USE_LEGACY): cv.boolean,
},
),
validate_use_legacy,
) )
@@ -148,12 +247,22 @@ def _final_validate(_):
) )
def use_legacy():
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0):
if not _use_legacy_driver:
return False
return True
FINAL_VALIDATE_SCHEMA = _final_validate FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
if use_legacy():
cg.add_define("USE_I2S_LEGACY")
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config: if CONF_I2S_BCLK_PIN in config:

View File

@@ -2,9 +2,14 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <driver/i2s.h>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#endif
namespace esphome { namespace esphome {
namespace i2s_audio { namespace i2s_audio {
@@ -13,20 +18,36 @@ class I2SAudioComponent;
class I2SAudioBase : public Parented<I2SAudioComponent> { class I2SAudioBase : public Parented<I2SAudioComponent> {
public: public:
#ifdef USE_I2S_LEGACY
void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; } void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; }
#else
void set_i2s_role(i2s_role_t role) { this->i2s_role_ = role; }
void set_slot_mode(i2s_slot_mode_t slot_mode) { this->slot_mode_ = slot_mode; }
void set_std_slot_mask(i2s_std_slot_mask_t std_slot_mask) { this->std_slot_mask_ = std_slot_mask; }
void set_slot_bit_width(i2s_slot_bit_width_t slot_bit_width) { this->slot_bit_width_ = slot_bit_width; }
#endif
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
void set_mclk_multiple(i2s_mclk_multiple_t mclk_multiple) { this->mclk_multiple_ = mclk_multiple; }
protected: protected:
#ifdef USE_I2S_LEGACY
i2s_mode_t i2s_mode_{}; i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_; i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_; i2s_bits_per_sample_t bits_per_sample_;
i2s_bits_per_chan_t bits_per_channel_; i2s_bits_per_chan_t bits_per_channel_;
#else
i2s_role_t i2s_role_{};
i2s_slot_mode_t slot_mode_;
i2s_std_slot_mask_t std_slot_mask_;
i2s_slot_bit_width_t slot_bit_width_;
#endif
uint32_t sample_rate_;
bool use_apll_; bool use_apll_;
i2s_mclk_multiple_t mclk_multiple_;
}; };
class I2SAudioIn : public I2SAudioBase {}; class I2SAudioIn : public I2SAudioBase {};
@@ -37,6 +58,7 @@ class I2SAudioComponent : public Component {
public: public:
void setup() override; void setup() override;
#ifdef USE_I2S_LEGACY
i2s_pin_config_t get_pin_config() const { i2s_pin_config_t get_pin_config() const {
return { return {
.mck_io_num = this->mclk_pin_, .mck_io_num = this->mclk_pin_,
@@ -46,6 +68,20 @@ class I2SAudioComponent : public Component {
.data_in_num = I2S_PIN_NO_CHANGE, .data_in_num = I2S_PIN_NO_CHANGE,
}; };
} }
#else
i2s_std_gpio_config_t get_pin_config() const {
return {.mclk = (gpio_num_t) this->mclk_pin_,
.bclk = (gpio_num_t) this->bclk_pin_,
.ws = (gpio_num_t) this->lrclk_pin_,
.dout = I2S_GPIO_UNUSED, // add local ports
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
}};
}
#endif
void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_mclk_pin(int pin) { this->mclk_pin_ = pin; }
void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; }
@@ -62,9 +98,13 @@ class I2SAudioComponent : public Component {
I2SAudioIn *audio_in_{nullptr}; I2SAudioIn *audio_in_{nullptr};
I2SAudioOut *audio_out_{nullptr}; I2SAudioOut *audio_out_{nullptr};
#ifdef USE_I2S_LEGACY
int mclk_pin_{I2S_PIN_NO_CHANGE}; int mclk_pin_{I2S_PIN_NO_CHANGE};
int bclk_pin_{I2S_PIN_NO_CHANGE}; int bclk_pin_{I2S_PIN_NO_CHANGE};
#else
int mclk_pin_{I2S_GPIO_UNUSED};
int bclk_pin_{I2S_GPIO_UNUSED};
#endif
int lrclk_pin_; int lrclk_pin_;
i2s_port_t port_{}; i2s_port_t port_{};
}; };

View File

@@ -14,6 +14,7 @@ from .. import (
I2SAudioComponent, I2SAudioComponent,
I2SAudioOut, I2SAudioOut,
i2s_audio_ns, i2s_audio_ns,
use_legacy,
) )
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
@@ -87,6 +88,14 @@ CONFIG_SCHEMA = cv.All(
) )
def _final_validate(_):
if not use_legacy():
raise cv.Invalid("I2S media player is only compatible with legacy i2s driver.")
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -1,17 +1,28 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, microphone from esphome.components import audio, esp32, microphone
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_CHANNEL,
CONF_ID,
CONF_NUM_CHANNELS,
CONF_NUMBER,
CONF_SAMPLE_RATE,
)
from .. import ( from .. import (
CONF_I2S_DIN_PIN, CONF_I2S_DIN_PIN,
CONF_LEFT,
CONF_MONO,
CONF_RIGHT, CONF_RIGHT,
I2SAudioIn, I2SAudioIn,
i2s_audio_component_schema, i2s_audio_component_schema,
i2s_audio_ns, i2s_audio_ns,
register_i2s_audio_component, register_i2s_audio_component,
use_legacy,
validate_mclk_divisible_by_3,
) )
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
@@ -29,7 +40,7 @@ INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
def validate_esp32_variant(config): def _validate_esp32_variant(config):
variant = esp32.get_esp32_variant() variant = esp32.get_esp32_variant()
if config[CONF_ADC_TYPE] == "external": if config[CONF_ADC_TYPE] == "external":
if config[CONF_PDM]: if config[CONF_PDM]:
@@ -43,6 +54,34 @@ def validate_esp32_variant(config):
raise NotImplementedError raise NotImplementedError
def _validate_channel(config):
if config[CONF_CHANNEL] == CONF_MONO:
raise cv.Invalid(f"I2S microphone does not support {CONF_MONO}.")
return config
def _set_num_channels_from_config(config):
if config[CONF_CHANNEL] in (CONF_LEFT, CONF_RIGHT):
config[CONF_NUM_CHANNELS] = 1
else:
config[CONF_NUM_CHANNELS] = 2
return config
def _set_stream_limits(config):
audio.set_stream_limits(
min_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE),
max_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE),
min_channels=config.get(CONF_NUM_CHANNELS),
max_channels=config.get(CONF_NUM_CHANNELS),
min_sample_rate=config.get(CONF_SAMPLE_RATE),
max_sample_rate=config.get(CONF_SAMPLE_RATE),
)(config)
return config
BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
i2s_audio_component_schema( i2s_audio_component_schema(
I2SAudioMicrophone, I2SAudioMicrophone,
@@ -70,10 +109,23 @@ CONFIG_SCHEMA = cv.All(
}, },
key=CONF_ADC_TYPE, key=CONF_ADC_TYPE,
), ),
validate_esp32_variant, _validate_esp32_variant,
_validate_channel,
_set_num_channels_from_config,
_set_stream_limits,
validate_mclk_divisible_by_3,
) )
def _final_validate(config):
if not use_legacy():
if config[CONF_ADC_TYPE] == "internal":
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.")
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -2,7 +2,12 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h> #include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#include <driver/i2s_pdm.h>
#endif
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -10,12 +15,28 @@
namespace esphome { namespace esphome {
namespace i2s_audio { namespace i2s_audio {
static const size_t BUFFER_SIZE = 512; static const UBaseType_t MAX_LISTENERS = 16;
static const uint32_t READ_DURATION_MS = 16;
static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 23;
static const char *const TAG = "i2s_audio.microphone"; static const char *const TAG = "i2s_audio.microphone";
enum MicrophoneEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // stops the microphone task
TASK_STARTING = (1 << 10),
TASK_RUNNING = (1 << 11),
TASK_STOPPING = (1 << 12),
TASK_STOPPED = (1 << 13),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
void I2SAudioMicrophone::setup() { void I2SAudioMicrophone::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone...");
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC #if SOC_I2S_SUPPORTS_ADC
if (this->adc_) { if (this->adc_) {
if (this->parent_->get_port() != I2S_NUM_0) { if (this->parent_->get_port() != I2S_NUM_0) {
@@ -24,6 +45,7 @@ void I2SAudioMicrophone::setup() {
return; return;
} }
} else } else
#endif
#endif #endif
{ {
if (this->pdm_) { if (this->pdm_) {
@@ -34,19 +56,65 @@ void I2SAudioMicrophone::setup() {
} }
} }
} }
this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS);
if (this->active_listeners_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Failed to create semaphore");
this->mark_failed();
return;
}
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
return;
}
} }
void I2SAudioMicrophone::start() { void I2SAudioMicrophone::start() {
if (this->is_failed()) if (this->is_failed())
return; return;
if (this->state_ == microphone::STATE_RUNNING)
return; // Already running xSemaphoreTake(this->active_listeners_semaphore_, 0);
this->state_ = microphone::STATE_STARTING;
} }
void I2SAudioMicrophone::start_() {
bool I2SAudioMicrophone::start_driver_() {
if (!this->parent_->try_lock()) { if (!this->parent_->try_lock()) {
return; // Waiting for another i2s to return lock return false; // Waiting for another i2s to return lock
} }
esp_err_t err;
uint8_t channel_count = 1;
#ifdef USE_I2S_LEGACY
uint8_t bits_per_sample = this->bits_per_sample_;
if (this->channel_ == I2S_CHANNEL_FMT_RIGHT_LEFT) {
channel_count = 2;
}
#else
if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_AUTO) {
this->slot_bit_width_ = I2S_SLOT_BIT_WIDTH_16BIT;
}
uint8_t bits_per_sample = this->slot_bit_width_;
if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) {
channel_count = 2;
}
#endif
#ifdef USE_ESP32_VARIANT_ESP32
// ESP32 reads audio aligned to a multiple of 2 bytes. For example, if configured for 24 bits per sample, then it will
// produce 32 bits per sample, where the actual data is in the most significant bits. Other ESP32 variants produce 24
// bits per sample in this situation.
if (bits_per_sample < 16) {
bits_per_sample = 16;
} else if ((bits_per_sample > 16) && (bits_per_sample <= 32)) {
bits_per_sample = 32;
}
#endif
#ifdef USE_I2S_LEGACY
i2s_driver_config_t config = { i2s_driver_config_t config = {
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX),
.sample_rate = this->sample_rate_, .sample_rate = this->sample_rate_,
@@ -55,16 +123,14 @@ void I2SAudioMicrophone::start_() {
.communication_format = I2S_COMM_FORMAT_STAND_I2S, .communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4, .dma_buf_count = 4,
.dma_buf_len = 256, .dma_buf_len = 240, // Must be divisible by 3 to support 24 bits per sample on old driver and newer variants
.use_apll = this->use_apll_, .use_apll = this->use_apll_,
.tx_desc_auto_clear = false, .tx_desc_auto_clear = false,
.fixed_mclk = 0, .fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = this->mclk_multiple_,
.bits_per_chan = this->bits_per_channel_, .bits_per_chan = this->bits_per_channel_,
}; };
esp_err_t err;
#if SOC_I2S_SUPPORTS_ADC #if SOC_I2S_SUPPORTS_ADC
if (this->adc_) { if (this->adc_) {
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN);
@@ -72,20 +138,20 @@ void I2SAudioMicrophone::start_() {
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
this->status_set_error(); this->status_set_error();
return; return false;
} }
err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Error setting ADC mode: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error setting ADC mode: %s", esp_err_to_name(err));
this->status_set_error(); this->status_set_error();
return; return false;
} }
err = i2s_adc_enable(this->parent_->get_port()); err = i2s_adc_enable(this->parent_->get_port());
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Error enabling ADC: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error enabling ADC: %s", esp_err_to_name(err));
this->status_set_error(); this->status_set_error();
return; return false;
} }
} else } else
@@ -98,7 +164,7 @@ void I2SAudioMicrophone::start_() {
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
this->status_set_error(); this->status_set_error();
return; return false;
} }
i2s_pin_config_t pin_config = this->parent_->get_pin_config(); i2s_pin_config_t pin_config = this->parent_->get_pin_config();
@@ -108,26 +174,126 @@ void I2SAudioMicrophone::start_() {
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error setting I2S pin: %s", esp_err_to_name(err));
this->status_set_error(); this->status_set_error();
return; return false;
} }
} }
this->state_ = microphone::STATE_RUNNING; #else
this->high_freq_.start(); i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
.dma_desc_num = 4,
.dma_frame_num = 256,
.auto_clear = false,
};
/* Allocate a new RX channel and get the handle of this channel */
err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err));
this->status_set_error();
return false;
}
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
#ifdef I2S_CLK_SRC_APLL
if (this->use_apll_) {
clk_src = I2S_CLK_SRC_APLL;
}
#endif
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
#if SOC_I2S_SUPPORTS_PDM_RX
if (this->pdm_) {
bits_per_sample = 16; // PDM mics are always 16 bits per sample with the IDF 5 driver
i2s_pdm_rx_clk_config_t clk_cfg = {
.sample_rate_hz = this->sample_rate_,
.clk_src = clk_src,
.mclk_multiple = this->mclk_multiple_,
.dn_sample_mode = I2S_PDM_DSR_8S,
};
i2s_pdm_rx_slot_config_t slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, this->slot_mode_);
switch (this->std_slot_mask_) {
case I2S_STD_SLOT_LEFT:
slot_cfg.slot_mask = I2S_PDM_SLOT_LEFT;
break;
case I2S_STD_SLOT_RIGHT:
slot_cfg.slot_mask = I2S_PDM_SLOT_RIGHT;
break;
case I2S_STD_SLOT_BOTH:
slot_cfg.slot_mask = I2S_PDM_SLOT_BOTH;
break;
}
/* Init the channel into PDM RX mode */
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = slot_cfg,
.gpio_cfg =
{
.clk = pin_config.ws,
.din = this->din_pin_,
.invert_flags =
{
.clk_inv = pin_config.invert_flags.ws_inv,
},
},
};
err = i2s_channel_init_pdm_rx_mode(this->rx_handle_, &pdm_rx_cfg);
} else
#endif
{
i2s_std_clk_config_t clk_cfg = {
.sample_rate_hz = this->sample_rate_,
.clk_src = clk_src,
.mclk_multiple = this->mclk_multiple_,
};
i2s_std_slot_config_t std_slot_cfg =
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_);
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
std_slot_cfg.slot_mask = this->std_slot_mask_;
pin_config.din = this->din_pin_;
i2s_std_config_t std_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = std_slot_cfg,
.gpio_cfg = pin_config,
};
/* Initialize the channel */
err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg);
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err));
this->status_set_error();
return false;
}
/* Before reading data, start the RX channel first */
i2s_channel_enable(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err));
this->status_set_error();
return false;
}
#endif
this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_);
this->status_clear_error(); this->status_clear_error();
return true;
} }
void I2SAudioMicrophone::stop() { void I2SAudioMicrophone::stop() {
if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) if (this->state_ == microphone::STATE_STOPPED || this->is_failed())
return; return;
if (this->state_ == microphone::STATE_STARTING) {
this->state_ = microphone::STATE_STOPPED; xSemaphoreGive(this->active_listeners_semaphore_);
return;
}
this->state_ = microphone::STATE_STOPPING;
} }
void I2SAudioMicrophone::stop_() { void I2SAudioMicrophone::stop_driver_() {
esp_err_t err; esp_err_t err;
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC #if SOC_I2S_SUPPORTS_ADC
if (this->adc_) { if (this->adc_) {
err = i2s_adc_disable(this->parent_->get_port()); err = i2s_adc_disable(this->parent_->get_port());
@@ -150,68 +316,155 @@ void I2SAudioMicrophone::stop_() {
this->status_set_error(); this->status_set_error();
return; return;
} }
#else
/* Have to stop the channel before deleting it */
err = i2s_channel_disable(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err));
this->status_set_error();
return;
}
/* If the handle is not needed any more, delete it to release the channel resources */
err = i2s_del_channel(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error deleting I2S channel: %s", esp_err_to_name(err));
this->status_set_error();
return;
}
#endif
this->parent_->unlock(); this->parent_->unlock();
this->state_ = microphone::STATE_STOPPED;
this->high_freq_.stop();
this->status_clear_error(); this->status_clear_error();
} }
size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { void I2SAudioMicrophone::mic_task(void *params) {
I2SAudioMicrophone *this_microphone = (I2SAudioMicrophone *) params;
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
uint8_t start_counter = 0;
bool started = this_microphone->start_driver_();
while (!started && start_counter < 10) {
// Attempt to load the driver again in 100 ms. Doesn't slow down main loop since its in a task.
vTaskDelay(pdMS_TO_TICKS(100));
++start_counter;
started = this_microphone->start_driver_();
}
if (started) {
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS);
std::vector<uint8_t> samples;
samples.reserve(bytes_to_read);
while (!(xEventGroupGetBits(this_microphone->event_group_) & COMMAND_STOP)) {
if (this_microphone->data_callbacks_.size() > 0) {
samples.resize(bytes_to_read);
size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS));
samples.resize(bytes_read);
this_microphone->data_callbacks_.call(samples);
} else {
delay(READ_DURATION_MS);
}
}
}
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPING);
this_microphone->stop_driver_();
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method delete the task
delay(10);
}
}
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
size_t bytes_read = 0; size_t bytes_read = 0;
esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, (100 / portTICK_PERIOD_MS)); #ifdef USE_I2S_LEGACY
if (err != ESP_OK) { esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, ticks_to_wait);
#else
// i2s_channel_read expects the timeout value in ms, not ticks
esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, pdTICKS_TO_MS(ticks_to_wait));
#endif
if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) {
// Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call
ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err));
this->status_set_warning(); this->status_set_warning();
return 0; return 0;
} }
if (bytes_read == 0) { if ((bytes_read == 0) && (ticks_to_wait > 0)) {
this->status_set_warning(); this->status_set_warning();
return 0; return 0;
} }
this->status_clear_warning(); this->status_clear_warning();
// ESP-IDF I2S implementation right-extends 8-bit data to 16 bits, #if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY)
// and 24-bit data to 32 bits. // For ESP32 8/16 bit standard mono mode samples need to be switched.
switch (this->bits_per_sample_) { if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) {
case I2S_BITS_PER_SAMPLE_8BIT: size_t samples_read = bytes_read / sizeof(int16_t);
case I2S_BITS_PER_SAMPLE_16BIT: for (int i = 0; i < samples_read; i += 2) {
return bytes_read; int16_t tmp = buf[i];
case I2S_BITS_PER_SAMPLE_24BIT: buf[i] = buf[i + 1];
case I2S_BITS_PER_SAMPLE_32BIT: { buf[i + 1] = tmp;
size_t samples_read = bytes_read / sizeof(int32_t);
for (size_t i = 0; i < samples_read; i++) {
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
buf[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
}
return samples_read * sizeof(int16_t);
} }
default:
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
return 0;
} }
} #endif
return bytes_read;
void I2SAudioMicrophone::read_() {
std::vector<int16_t> samples;
samples.resize(BUFFER_SIZE);
size_t bytes_read = this->read(samples.data(), BUFFER_SIZE / sizeof(int16_t));
samples.resize(bytes_read / sizeof(int16_t));
this->data_callbacks_.call(samples);
} }
void I2SAudioMicrophone::loop() { void I2SAudioMicrophone::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) {
ESP_LOGD(TAG, "Task has started, attempting to setup I2S audio driver");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
}
if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) {
ESP_LOGD(TAG, "Task is running and reading data");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
this->state_ = microphone::STATE_RUNNING;
}
if (event_group_bits & MicrophoneEventGroupBits::TASK_STOPPING) {
ESP_LOGD(TAG, "Task is stopping, attempting to unload the I2S audio driver");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STOPPING);
}
if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) {
ESP_LOGD(TAG, "Task is finished, freeing resources");
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr;
xEventGroupClearBits(this->event_group_, ALL_BITS);
this->state_ = microphone::STATE_STOPPED;
}
if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) < MAX_LISTENERS) &&
(this->state_ == microphone::STATE_STOPPED)) {
this->state_ = microphone::STATE_STARTING;
}
if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) == MAX_LISTENERS) &&
(this->state_ == microphone::STATE_RUNNING)) {
this->state_ = microphone::STATE_STOPPING;
}
switch (this->state_) { switch (this->state_) {
case microphone::STATE_STOPPED:
break;
case microphone::STATE_STARTING: case microphone::STATE_STARTING:
this->start_(); if ((this->task_handle_ == nullptr) && !this->status_has_error()) {
break; xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
case microphone::STATE_RUNNING: &this->task_handle_);
if (this->data_callbacks_.size() > 0) {
this->read_(); if (this->task_handle_ == nullptr) {
this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000);
}
} }
break; break;
case microphone::STATE_RUNNING:
break;
case microphone::STATE_STOPPING: case microphone::STATE_STOPPING:
this->stop_(); xEventGroupSetBits(this->event_group_, MicrophoneEventGroupBits::COMMAND_STOP);
break;
case microphone::STATE_STOPPED:
break; break;
} }
} }

View File

@@ -7,6 +7,9 @@
#include "esphome/components/microphone/microphone.h" #include "esphome/components/microphone/microphone.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <freertos/event_groups.h>
#include <freertos/semphr.h>
namespace esphome { namespace esphome {
namespace i2s_audio { namespace i2s_audio {
@@ -17,32 +20,47 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
void stop() override; void stop() override;
void loop() override; void loop() override;
#ifdef USE_I2S_LEGACY
void set_din_pin(int8_t pin) { this->din_pin_ = pin; } void set_din_pin(int8_t pin) { this->din_pin_ = pin; }
#else
void set_din_pin(int8_t pin) { this->din_pin_ = (gpio_num_t) pin; }
#endif
void set_pdm(bool pdm) { this->pdm_ = pdm; } void set_pdm(bool pdm) { this->pdm_ = pdm; }
size_t read(int16_t *buf, size_t len) override; #ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC #if SOC_I2S_SUPPORTS_ADC
void set_adc_channel(adc1_channel_t channel) { void set_adc_channel(adc1_channel_t channel) {
this->adc_channel_ = channel; this->adc_channel_ = channel;
this->adc_ = true; this->adc_ = true;
} }
#endif
#endif #endif
protected: protected:
void start_(); bool start_driver_();
void stop_(); void stop_driver_();
void read_();
size_t read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait);
static void mic_task(void *params);
SemaphoreHandle_t active_listeners_semaphore_{nullptr};
EventGroupHandle_t event_group_{nullptr};
TaskHandle_t task_handle_{nullptr};
#ifdef USE_I2S_LEGACY
int8_t din_pin_{I2S_PIN_NO_CHANGE}; int8_t din_pin_{I2S_PIN_NO_CHANGE};
#if SOC_I2S_SUPPORTS_ADC #if SOC_I2S_SUPPORTS_ADC
adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX}; adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX};
bool adc_{false}; bool adc_{false};
#endif
#else
gpio_num_t din_pin_{I2S_GPIO_UNUSED};
i2s_chan_handle_t rx_handle_;
#endif #endif
bool pdm_{false}; bool pdm_{false};
HighFrequencyLoopRequester high_freq_;
}; };
} // namespace i2s_audio } // namespace i2s_audio

View File

@@ -26,6 +26,8 @@ from .. import (
i2s_audio_component_schema, i2s_audio_component_schema,
i2s_audio_ns, i2s_audio_ns,
register_i2s_audio_component, register_i2s_audio_component,
use_legacy,
validate_mclk_divisible_by_3,
) )
AUTO_LOAD = ["audio"] AUTO_LOAD = ["audio"]
@@ -60,7 +62,7 @@ I2C_COMM_FMT_OPTIONS = {
"pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG, "pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG,
} }
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32]
def _set_num_channels_from_config(config): def _set_num_channels_from_config(config):
@@ -101,7 +103,7 @@ def _validate_esp32_variant(config):
if config[CONF_DAC_TYPE] != "internal": if config[CONF_DAC_TYPE] != "internal":
return config return config
variant = esp32.get_esp32_variant() variant = esp32.get_esp32_variant()
if variant in NO_INTERNAL_DAC_VARIANTS: if variant not in INTERNAL_DAC_VARIANTS:
raise cv.Invalid(f"{variant} does not have an internal DAC") raise cv.Invalid(f"{variant} does not have an internal DAC")
return config return config
@@ -143,8 +145,8 @@ CONFIG_SCHEMA = cv.All(
cv.Required( cv.Required(
CONF_I2S_DOUT_PIN CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number, ): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum( cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of(
I2C_COMM_FMT_OPTIONS, lower=True *I2C_COMM_FMT_OPTIONS, lower=True
), ),
} }
), ),
@@ -154,9 +156,23 @@ CONFIG_SCHEMA = cv.All(
_validate_esp32_variant, _validate_esp32_variant,
_set_num_channels_from_config, _set_num_channels_from_config,
_set_stream_limits, _set_stream_limits,
validate_mclk_divisible_by_3,
) )
def _final_validate(config):
if not use_legacy():
if config[CONF_DAC_TYPE] == "internal":
raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver.")
if config[CONF_I2S_COMM_FMT] == "stand_max":
raise cv.Invalid(
"I2S standard max format only implemented with legacy i2s driver."
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -167,7 +183,17 @@ async def to_code(config):
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
else: else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT])) if use_legacy():
cg.add(
var.set_i2s_comm_fmt(I2C_COMM_FMT_OPTIONS[config[CONF_I2S_COMM_FMT]])
)
else:
fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = "msb"
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = "pcm"
cg.add(var.set_i2s_comm_fmt(fmt))
if config[CONF_TIMEOUT] != CONF_NEVER: if config[CONF_TIMEOUT] != CONF_NEVER:
cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_timeout(config[CONF_TIMEOUT]))
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))

View File

@@ -2,7 +2,11 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h> #include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#endif
#include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio.h"
@@ -294,13 +298,21 @@ void I2SAudioSpeaker::speaker_task(void *params) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings. // Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break; break;
} }
#ifdef USE_I2S_LEGACY
i2s_event_t i2s_event; i2s_event_t i2s_event;
while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) { while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) {
if (i2s_event.type == I2S_EVENT_TX_Q_OVF) { if (i2s_event.type == I2S_EVENT_TX_Q_OVF) {
tx_dma_underflow = true; tx_dma_underflow = true;
} }
} }
#else
bool overflow;
while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) {
if (overflow) {
tx_dma_underflow = true;
}
}
#endif
if (this_speaker->pause_state_) { if (this_speaker->pause_state_) {
// Pause state is accessed atomically, so thread safe // Pause state is accessed atomically, so thread safe
@@ -319,6 +331,18 @@ void I2SAudioSpeaker::speaker_task(void *params) {
bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_); bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_);
} }
#ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 8/16 bit mono mode samples need to be switched.
if (audio_stream_info.get_channels() == 1 && audio_stream_info.get_bits_per_sample() <= 16) {
size_t len = bytes_read / sizeof(int16_t);
int16_t *tmp_buf = (int16_t *) this_speaker->data_buffer_;
for (int i = 0; i < len; i += 2) {
int16_t tmp = tmp_buf[i];
tmp_buf[i] = tmp_buf[i + 1];
tmp_buf[i + 1] = tmp;
}
}
#endif
// Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played // Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played
// callback. // callback.
const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size; const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size;
@@ -327,6 +351,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
size_t bytes_written = 0; size_t bytes_written = 0;
size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read); size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read);
#ifdef USE_I2S_LEGACY
if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size, i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
@@ -336,6 +361,10 @@ void I2SAudioSpeaker::speaker_task(void *params) {
audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written, audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written,
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
} }
#else
i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
#endif
uint32_t write_timestamp = micros(); uint32_t write_timestamp = micros();
@@ -369,8 +398,12 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this_speaker->parent_->get_port()); i2s_driver_uninstall(this_speaker->parent_->get_port());
#else
i2s_channel_disable(this_speaker->tx_handle_);
i2s_del_channel(this_speaker->tx_handle_);
#endif
this_speaker->parent_->unlock(); this_speaker->parent_->unlock();
} }
@@ -462,12 +495,21 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin
} }
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
#ifdef USE_I2S_LEGACY
if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#else
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#endif
// Can't reconfigure I2S bus, so the sample rate must match the configured value // Can't reconfigure I2S bus, so the sample rate must match the configured value
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
#ifdef USE_I2S_LEGACY
if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) { if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) {
#else
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO &&
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
#endif
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value // Currently can't handle the case when the incoming audio has more bits per sample than the configured value
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
@@ -476,6 +518,9 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
#ifdef USE_I2S_LEGACY
i2s_channel_fmt_t channel = this->channel_; i2s_channel_fmt_t channel = this->channel_;
if (audio_stream_info.get_channels() == 1) { if (audio_stream_info.get_channels() == 1) {
@@ -488,8 +533,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
channel = I2S_CHANNEL_FMT_RIGHT_LEFT; channel = I2S_CHANNEL_FMT_RIGHT_LEFT;
} }
int dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
i2s_driver_config_t config = { i2s_driver_config_t config = {
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX), .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
.sample_rate = audio_stream_info.get_sample_rate(), .sample_rate = audio_stream_info.get_sample_rate(),
@@ -498,11 +541,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
.communication_format = this->i2s_comm_fmt_, .communication_format = this->i2s_comm_fmt_,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = DMA_BUFFERS_COUNT, .dma_buf_count = DMA_BUFFERS_COUNT,
.dma_buf_len = dma_buffer_length, .dma_buf_len = (int) dma_buffer_length,
.use_apll = this->use_apll_, .use_apll = this->use_apll_,
.tx_desc_auto_clear = true, .tx_desc_auto_clear = true,
.fixed_mclk = I2S_PIN_NO_CHANGE, .fixed_mclk = I2S_PIN_NO_CHANGE,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = this->mclk_multiple_,
.bits_per_chan = this->bits_per_channel_, .bits_per_chan = this->bits_per_channel_,
#if SOC_I2S_SUPPORTS_TDM #if SOC_I2S_SUPPORTS_TDM
.chan_mask = (i2s_channel_t) (I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1), .chan_mask = (i2s_channel_t) (I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1),
@@ -545,6 +588,89 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
i2s_driver_uninstall(this->parent_->get_port()); i2s_driver_uninstall(this->parent_->get_port());
this->parent_->unlock(); this->parent_->unlock();
} }
#else
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
.dma_desc_num = DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length,
.auto_clear = true,
};
/* Allocate a new TX channel and get the handle of this channel */
esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
if (err != ESP_OK) {
this->parent_->unlock();
return err;
}
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
#ifdef I2S_CLK_SRC_APLL
if (this->use_apll_) {
clk_src = I2S_CLK_SRC_APLL;
}
#endif
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
i2s_std_clk_config_t clk_cfg = {
.sample_rate_hz = audio_stream_info.get_sample_rate(),
.clk_src = clk_src,
.mclk_multiple = this->mclk_multiple_,
};
i2s_slot_mode_t slot_mode = this->slot_mode_;
i2s_std_slot_mask_t slot_mask = this->std_slot_mask_;
if (audio_stream_info.get_channels() == 1) {
slot_mode = I2S_SLOT_MODE_MONO;
} else if (audio_stream_info.get_channels() == 2) {
slot_mode = I2S_SLOT_MODE_STEREO;
slot_mask = I2S_STD_SLOT_BOTH;
}
i2s_std_slot_config_t std_slot_cfg;
if (this->i2s_comm_fmt_ == "std") {
std_slot_cfg =
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
} else if (this->i2s_comm_fmt_ == "pcm") {
std_slot_cfg =
I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
} else {
std_slot_cfg =
I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
}
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
std_slot_cfg.slot_mask = slot_mask;
pin_config.dout = this->dout_pin_;
i2s_std_config_t std_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = std_slot_cfg,
.gpio_cfg = pin_config,
};
/* Initialize the channel */
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) {
i2s_del_channel(this->tx_handle_);
this->parent_->unlock();
return err;
}
if (this->i2s_event_queue_ == nullptr) {
this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool));
}
const i2s_event_callbacks_t callbacks = {
.on_send_q_ovf = i2s_overflow_cb,
};
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
/* Before reading data, start the TX channel first */
i2s_channel_enable(this->tx_handle_);
if (err != ESP_OK) {
i2s_del_channel(this->tx_handle_);
this->parent_->unlock();
}
#endif
return err; return err;
} }
@@ -564,6 +690,15 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
vTaskDelete(nullptr); vTaskDelete(nullptr);
} }
#ifndef USE_I2S_LEGACY
bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
bool overflow = true;
xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow);
return false;
}
#endif
} // namespace i2s_audio } // namespace i2s_audio
} // namespace esphome } // namespace esphome

View File

@@ -4,8 +4,6 @@
#include "../i2s_audio.h" #include "../i2s_audio.h"
#include <driver/i2s.h>
#include <freertos/event_groups.h> #include <freertos/event_groups.h>
#include <freertos/queue.h> #include <freertos/queue.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
@@ -30,11 +28,16 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
void set_timeout(uint32_t ms) { this->timeout_ = ms; } void set_timeout(uint32_t ms) { this->timeout_ = ms; }
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } #ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif #endif
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; } void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; }
#else
void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; }
void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); }
#endif
void start() override; void start() override;
void stop() override; void stop() override;
@@ -86,6 +89,10 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
/// @return True if an ERR_ESP bit is set and false if err == ESP_OK /// @return True if an ERR_ESP bit is set and false if err == ESP_OK
bool send_esp_err_to_event_group_(esp_err_t err); bool send_esp_err_to_event_group_(esp_err_t err);
#ifndef USE_I2S_LEGACY
static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
#endif
/// @brief Allocates the data buffer and ring buffer /// @brief Allocates the data buffer and ring buffer
/// @param data_buffer_size Number of bytes to allocate for the data buffer. /// @param data_buffer_size Number of bytes to allocate for the data buffer.
/// @param ring_buffer_size Number of bytes to allocate for the ring buffer. /// @param ring_buffer_size Number of bytes to allocate for the ring buffer.
@@ -121,7 +128,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
uint32_t buffer_duration_ms_; uint32_t buffer_duration_ms_;
optional<uint32_t> timeout_; optional<uint32_t> timeout_;
uint8_t dout_pin_;
bool task_created_{false}; bool task_created_{false};
bool pause_state_{false}; bool pause_state_{false};
@@ -130,10 +136,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
size_t bytes_written_{0}; size_t bytes_written_{0};
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif #endif
uint8_t dout_pin_;
i2s_comm_format_t i2s_comm_fmt_; i2s_comm_format_t i2s_comm_fmt_;
#else
gpio_num_t dout_pin_;
std::string i2s_comm_fmt_;
i2s_chan_handle_t tx_handle_;
#endif
uint32_t accumulated_frames_written_{0}; uint32_t accumulated_frames_written_{0};
}; };

View File

@@ -388,7 +388,7 @@ static const uint8_t PROGMEM INITCMD_GC9D01N[] = {
0x8D, 1, 0xFF, 0x8D, 1, 0xFF,
0x8E, 1, 0xFF, 0x8E, 1, 0xFF,
0x8F, 1, 0xFF, 0x8F, 1, 0xFF,
0X3A, 1, 0x05, // COLMOD: Pixel Format Set (3Ah) MCU interface, 16 bits / pixel 0x3A, 1, 0x05, // COLMOD: Pixel Format Set (3Ah) MCU interface, 16 bits / pixel
0xEC, 1, 0x01, // Inversion (ECh) DINV=1+2H1V column for Dual Gate (BFh=0) 0xEC, 1, 0x01, // Inversion (ECh) DINV=1+2H1V column for Dual Gate (BFh=0)
// According to datasheet Inversion (ECh) value 0x01 isn't valid, but Lilygo uses it everywhere // According to datasheet Inversion (ECh) value 0x01 isn't valid, but Lilygo uses it everywhere
0x74, 7, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 7, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00,

View File

@@ -286,9 +286,18 @@ CONF_TRANSPARENCY = "transparency"
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local" SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi"
SOURCE_WEB = "web" SOURCE_WEB = "web"
SOURCE_MDI = "mdi"
SOURCE_MDIL = "mdil"
SOURCE_MEMORY = "memory"
MDI_SOURCES = {
SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/",
SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/",
SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/",
}
Image_ = image_ns.class_("Image") Image_ = image_ns.class_("Image")
INSTANCE_TYPE = Image_ INSTANCE_TYPE = Image_
@@ -313,12 +322,12 @@ def download_file(url, path):
return str(path) return str(path)
def download_mdi(value): def download_gh_svg(value, source):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" base_dir = external_files.compute_local_file_dir(DOMAIN) / source
path = base_dir / f"{mdi_id}.svg" path = base_dir / f"{mdi_id}.svg"
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" url = MDI_SOURCES[source] + mdi_id + ".svg"
return download_file(url, path) return download_file(url, path)
@@ -353,12 +362,12 @@ def validate_cairosvg_installed():
def validate_file_shorthand(value): def validate_file_shorthand(value):
value = cv.string_strict(value) value = cv.string_strict(value)
if value.startswith("mdi:"): parts = value.strip().split(":")
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"[a-zA-Z0-9\-]+", parts[1])
if match is None: if match is None:
raise cv.Invalid("Could not parse mdi icon name.") raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
icon = match.group(1) return download_gh_svg(parts[1], parts[0])
return download_mdi(icon)
if value.startswith("http://") or value.startswith("https://"): if value.startswith("http://") or value.startswith("https://"):
return download_image(value) return download_image(value)
@@ -374,12 +383,20 @@ LOCAL_SCHEMA = cv.All(
local_path, local_path,
) )
MDI_SCHEMA = cv.All(
{ def mdi_schema(source):
cv.Required(CONF_ICON): cv.string, def validate_mdi(value):
}, return download_gh_svg(value, source)
download_mdi,
) return cv.All(
cv.Schema(
{
cv.Required(CONF_ICON): cv.string,
}
),
validate_mdi,
)
WEB_SCHEMA = cv.All( WEB_SCHEMA = cv.All(
{ {
@@ -388,12 +405,13 @@ WEB_SCHEMA = cv.All(
download_image, download_image,
) )
TYPED_FILE_SCHEMA = cv.typed_schema( TYPED_FILE_SCHEMA = cv.typed_schema(
{ {
SOURCE_LOCAL: LOCAL_SCHEMA, SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_MDI: MDI_SCHEMA,
SOURCE_WEB: WEB_SCHEMA, SOURCE_WEB: WEB_SCHEMA,
}, }
| {source: mdi_schema(source) for source in MDI_SOURCES},
key=CONF_SOURCE, key=CONF_SOURCE,
) )

View File

@@ -6,10 +6,27 @@ namespace esphome {
namespace image { namespace image {
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
int img_x0 = 0;
int img_y0 = 0;
int w = width_;
int h = height_;
auto clipping = display->get_clipping();
if (clipping.is_set()) {
if (clipping.x > x)
img_x0 += clipping.x - x;
if (clipping.y > y)
img_y0 += clipping.y - y;
if (w > clipping.x2() - x)
w = clipping.x2() - x;
if (h > clipping.y2() - y)
h = clipping.y2() - y;
}
switch (type_) { switch (type_) {
case IMAGE_TYPE_BINARY: { case IMAGE_TYPE_BINARY: {
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = img_y0; img_y < h; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) { if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on); display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparency_) { } else if (!this->transparency_) {
@@ -20,8 +37,8 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
break; break;
} }
case IMAGE_TYPE_GRAYSCALE: case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = img_y0; img_y < h; img_y++) {
const uint32_t pos = (img_x + img_y * this->width_); const uint32_t pos = (img_x + img_y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
Color color = Color(gray, gray, gray, 0xFF); Color color = Color(gray, gray, gray, 0xFF);
@@ -47,8 +64,8 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
} }
break; break;
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb565_pixel_(img_x, img_y); auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) { if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color); display->draw_pixel_at(x + img_x, y + img_y, color);
@@ -57,8 +74,8 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
} }
break; break;
case IMAGE_TYPE_RGB: case IMAGE_TYPE_RGB:
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb_pixel_(img_x, img_y); auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) { if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color); display->draw_pixel_at(x + img_x, y + img_y, color);

View File

@@ -129,7 +129,7 @@ enum PeriodicDataStructure : uint8_t {
LIGHT_SENSOR = 37, LIGHT_SENSOR = 37,
OUT_PIN_SENSOR = 38, OUT_PIN_SENSOR = 38,
}; };
enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 };
enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 };

View File

@@ -105,7 +105,7 @@ enum PeriodicDataStructure : uint8_t {
TARGET_RESOLUTION = 10, TARGET_RESOLUTION = 10,
}; };
enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 };
enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 };

Some files were not shown because too many files have changed in this diff Show More