mirror of
https://github.com/esphome/esphome.git
synced 2025-11-03 16:41:50 +00:00
Compare commits
78 Commits
2025.4.0
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
273343b182 | ||
|
|
38dae8489e | ||
|
|
22c0e1079e | ||
|
|
2d3f141140 | ||
|
|
e49252ca3d | ||
|
|
c9d1476ae0 | ||
|
|
ee646d7324 | ||
|
|
e557bca420 | ||
|
|
adcd6517db | ||
|
|
4c8f5275f9 | ||
|
|
526db0102c | ||
|
|
8a3fe9ce4c | ||
|
|
fb97ef33a8 | ||
|
|
805a6d85a5 | ||
|
|
8f9fbb15b8 | ||
|
|
3d24dea455 | ||
|
|
666d5374ea | ||
|
|
6792ff6d58 | ||
|
|
f29ccb9e75 | ||
|
|
911bd54765 | ||
|
|
89b1b12993 | ||
|
|
33d79e03d9 | ||
|
|
991f3d3a10 | ||
|
|
97823ddd16 | ||
|
|
6ff180152a | ||
|
|
dbb7cbed3e | ||
|
|
fbf00f0af4 | ||
|
|
82c6a40371 | ||
|
|
0242ac56df | ||
|
|
b82666002d | ||
|
|
e11883e431 | ||
|
|
ff5b9df607 | ||
|
|
e5b7e3039a | ||
|
|
31ed1eb6f0 | ||
|
|
0c3daab649 | ||
|
|
816371e3e9 | ||
|
|
3c7bb65a23 | ||
|
|
4a65fd76b3 | ||
|
|
2704db5eef | ||
|
|
f10bc73d31 | ||
|
|
55e099450c | ||
|
|
248dbd32a5 | ||
|
|
a7b676231a | ||
|
|
2fd5f9ac58 | ||
|
|
ca4838a5f4 | ||
|
|
1b72550236 | ||
|
|
e5d718d1b1 | ||
|
|
af9b568778 | ||
|
|
3677ef71d1 | ||
|
|
7e133171e0 | ||
|
|
bc56d319b5 | ||
|
|
c423a6fb61 | ||
|
|
4034bf4f04 | ||
|
|
477abc05ae | ||
|
|
ff2b93a3e4 | ||
|
|
a52d6388a9 | ||
|
|
6259ca9ded | ||
|
|
f6ef50505b | ||
|
|
3c242b7296 | ||
|
|
00dd5b8339 | ||
|
|
a007a8237a | ||
|
|
9b86cc37f0 | ||
|
|
2dfcf950fa | ||
|
|
5908b93e82 | ||
|
|
995db1f961 | ||
|
|
abcc656a6f | ||
|
|
4a9f323d92 | ||
|
|
34a4e70cc5 | ||
|
|
fb5d697c22 | ||
|
|
df4642208e | ||
|
|
264e234efc | ||
|
|
ca78dd44b5 | ||
|
|
7edf458898 | ||
|
|
d9873e24a7 | ||
|
|
645bd490ba | ||
|
|
27f6d00e7a | ||
|
|
f9d668eeca | ||
|
|
6b930595e2 |
@@ -114,4 +114,5 @@ config/
|
||||
examples/
|
||||
Dockerfile
|
||||
.git/
|
||||
tests/build/
|
||||
tests/
|
||||
.*
|
||||
|
||||
33
.github/actions/build-image/action.yaml
vendored
33
.github/actions/build-image/action.yaml
vendored
@@ -1,15 +1,11 @@
|
||||
name: Build Image
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build for"
|
||||
required: true
|
||||
example: "linux/amd64"
|
||||
target:
|
||||
description: "Target to build"
|
||||
required: true
|
||||
example: "docker"
|
||||
baseimg:
|
||||
description: "Base image type"
|
||||
build_type:
|
||||
description: "Build type"
|
||||
required: true
|
||||
example: "docker"
|
||||
suffix:
|
||||
@@ -19,6 +15,11 @@ inputs:
|
||||
description: "Version to build"
|
||||
required: true
|
||||
example: "2023.12.0"
|
||||
base_os:
|
||||
description: "Base OS to use"
|
||||
required: false
|
||||
default: "debian"
|
||||
example: "debian"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -46,52 +47,52 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
uses: docker/build-push-action@v6.16.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: ${{ inputs.platform }}
|
||||
target: ${{ inputs.target }}
|
||||
cache-from: type=gha
|
||||
cache-to: ${{ steps.cache-to.outputs.value }}
|
||||
build-args: |
|
||||
BASEIMGTYPE=${{ inputs.baseimg }}
|
||||
BUILD_TYPE=${{ inputs.build_type }}
|
||||
BUILD_VERSION=${{ inputs.version }}
|
||||
BUILD_OS=${{ inputs.base_os }}
|
||||
outputs: |
|
||||
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export ghcr digests
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p /tmp/digests/${{ inputs.target }}/ghcr
|
||||
mkdir -p /tmp/digests/${{ inputs.build_type }}/ghcr
|
||||
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
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
uses: docker/build-push-action@v6.16.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: ${{ inputs.platform }}
|
||||
target: ${{ inputs.target }}
|
||||
cache-from: type=gha
|
||||
cache-to: ${{ steps.cache-to.outputs.value }}
|
||||
build-args: |
|
||||
BASEIMGTYPE=${{ inputs.baseimg }}
|
||||
BUILD_TYPE=${{ inputs.build_type }}
|
||||
BUILD_VERSION=${{ inputs.version }}
|
||||
BUILD_OS=${{ inputs.base_os }}
|
||||
outputs: |
|
||||
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export dockerhub digests
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub
|
||||
mkdir -p /tmp/digests/${{ inputs.build_type }}/dockerhub
|
||||
digest="${{ steps.build-dockerhub.outputs.digest }}"
|
||||
touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}"
|
||||
touch "/tmp/digests/${{ inputs.build_type }}/dockerhub/${digest#sha256:}"
|
||||
|
||||
6
.github/actions/restore-python/action.yml
vendored
6
.github/actions/restore-python/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -34,7 +34,7 @@ runs:
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
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 .
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
||||
@@ -43,5 +43,5 @@ runs:
|
||||
python -m venv venv
|
||||
./venv/Scripts/activate
|
||||
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 .
|
||||
|
||||
1
.github/dependabot.yml
vendored
1
.github/dependabot.yml
vendored
@@ -17,7 +17,6 @@ updates:
|
||||
docker-actions:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "docker/setup-qemu-action"
|
||||
- "docker/login-action"
|
||||
- "docker/setup-buildx-action"
|
||||
- package-ecosystem: github-actions
|
||||
|
||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
9
.github/workflows/ci-docker.yml
vendored
9
.github/workflows/ci-docker.yml
vendored
@@ -37,12 +37,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "ubuntu-24.04-arm"]
|
||||
build_type: ["ha-addon", "docker", "lint"]
|
||||
os: ["ubuntu-24.04", "ubuntu-24.04-arm"]
|
||||
build_type:
|
||||
- "ha-addon"
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -39,10 +39,10 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Generate 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
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 .
|
||||
|
||||
ruff:
|
||||
@@ -165,6 +165,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
script/ci-custom.py
|
||||
script/build_codeowners.py --check
|
||||
script/build_language_schema.py --check
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
@@ -220,7 +221,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native tests
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
70
.github/workflows/release.yml
vendored
70
.github/workflows/release.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Set up python environment
|
||||
@@ -68,31 +68,31 @@ jobs:
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||
|
||||
deploy-docker:
|
||||
name: Build ESPHome ${{ matrix.platform }}
|
||||
name: Build ESPHome ${{ matrix.platform.arch }}
|
||||
if: github.repository == 'esphome/esphome'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
needs: [init]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- arch: amd64
|
||||
os: "ubuntu-24.04"
|
||||
- arch: arm64
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
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
|
||||
uses: docker/login-action@v3.4.0
|
||||
@@ -109,45 +109,36 @@ jobs:
|
||||
- name: Build docker
|
||||
uses: ./.github/actions/build-image
|
||||
with:
|
||||
platform: ${{ matrix.platform }}
|
||||
target: docker
|
||||
baseimg: docker
|
||||
target: final
|
||||
build_type: docker
|
||||
suffix: ""
|
||||
version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Build ha-addon
|
||||
uses: ./.github/actions/build-image
|
||||
with:
|
||||
platform: ${{ matrix.platform }}
|
||||
target: hassio
|
||||
baseimg: hassio
|
||||
target: final
|
||||
build_type: ha-addon
|
||||
suffix: "hassio"
|
||||
version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Build lint
|
||||
uses: ./.github/actions/build-image
|
||||
with:
|
||||
platform: ${{ matrix.platform }}
|
||||
target: lint
|
||||
baseimg: docker
|
||||
suffix: lint
|
||||
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: Build lint
|
||||
# uses: ./.github/actions/build-image
|
||||
# with:
|
||||
# target: lint
|
||||
# build_type: lint
|
||||
# suffix: lint
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: digests-${{ steps.sanitize.outputs.name }}
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
retention-days: 1
|
||||
|
||||
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
|
||||
needs:
|
||||
- init
|
||||
@@ -160,15 +151,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image:
|
||||
- title: "ha-addon"
|
||||
target: "hassio"
|
||||
suffix: "hassio"
|
||||
- title: "docker"
|
||||
target: "docker"
|
||||
- build_type: "docker"
|
||||
suffix: ""
|
||||
- title: "lint"
|
||||
target: "lint"
|
||||
suffix: "lint"
|
||||
- build_type: "ha-addon"
|
||||
suffix: "hassio"
|
||||
# - build_type: "lint"
|
||||
# suffix: "lint"
|
||||
registry:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
@@ -176,7 +164,7 @@ jobs:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
@@ -212,7 +200,7 @@ jobs:
|
||||
done
|
||||
|
||||
- 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: |
|
||||
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
|
||||
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
|
||||
|
||||
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ esphome/components/climate/* @esphome/core
|
||||
esphome/components/climate_ir/* @glmnet
|
||||
esphome/components/color_temperature/* @jesserockz
|
||||
esphome/components/combination/* @Cat-Ion @kahrendt
|
||||
esphome/components/const/* @esphome/core
|
||||
esphome/components/coolix/* @glmnet
|
||||
esphome/components/copy/* @OttoWinter
|
||||
esphome/components/cover/* @esphome/core
|
||||
@@ -250,6 +251,7 @@ esphome/components/ltr501/* @latonita
|
||||
esphome/components/ltr_als_ps/* @latonita
|
||||
esphome/components/lvgl/* @clydebarrow
|
||||
esphome/components/m5stack_8angle/* @rnauber
|
||||
esphome/components/mapping/* @clydebarrow
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
esphome/components/max17043/* @blacknell
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
@@ -324,6 +326,7 @@ esphome/components/pcf8563/* @KoenBreeman
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
esphome/components/pm1006/* @habbie
|
||||
esphome/components/pm2005/* @andrewjswan
|
||||
esphome/components/pmsa003i/* @sjtrny
|
||||
esphome/components/pmwcs3/* @SeByDocKy
|
||||
esphome/components/pn532/* @OttoWinter @jesserockz
|
||||
|
||||
@@ -1,131 +1,54 @@
|
||||
# Build these with the build.py script
|
||||
# Example:
|
||||
# python3 docker/build.py --tag dev --arch amd64 --build-type docker build
|
||||
ARG BUILD_VERSION=dev
|
||||
ARG BUILD_OS=alpine
|
||||
ARG BUILD_BASE_VERSION=2025.04.0
|
||||
ARG BUILD_TYPE=docker
|
||||
|
||||
# One of "docker", "hassio"
|
||||
ARG BASEIMGTYPE=docker
|
||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-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
|
||||
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
|
||||
RUN git config --system --add safe.directory "*"
|
||||
|
||||
FROM base-${BASEIMGTYPE} AS base
|
||||
RUN pip install uv==0.6.14
|
||||
|
||||
|
||||
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.
|
||||
COPY requirements.txt /
|
||||
|
||||
RUN \
|
||||
apt-get update \
|
||||
# Use pinned versions so that we get updates with build caching
|
||||
&& 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
|
||||
uv pip install --no-cache-dir \
|
||||
-r /requirements.txt
|
||||
|
||||
RUN \
|
||||
pip3 install \
|
||||
--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 enable_telemetry No \
|
||||
&& platformio settings set check_platformio_interval 1000000 \
|
||||
&& 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 /
|
||||
RUN /platformio_install_deps.py /platformio.ini --libraries
|
||||
|
||||
# Avoid unsafe git error when container user and file config volume permissions don't match
|
||||
RUN git config --system --add safe.directory '*'
|
||||
ARG BUILD_VERSION
|
||||
|
||||
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 =======================
|
||||
FROM base AS docker
|
||||
|
||||
# Copy esphome and install
|
||||
COPY . /esphome
|
||||
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
|
||||
|
||||
# Settings for dashboard
|
||||
ENV USERNAME="" PASSWORD=""
|
||||
FROM base AS base-docker
|
||||
|
||||
# Expose the dashboard to Docker
|
||||
EXPOSE 6052
|
||||
|
||||
# Run healthcheck (heartbeat)
|
||||
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
|
||||
|
||||
@@ -139,43 +62,13 @@ ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dashboard", "/config"]
|
||||
|
||||
|
||||
ARG BUILD_VERSION=dev
|
||||
|
||||
# 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
|
||||
# ======================= ha-addon-type image =======================
|
||||
FROM base AS base-ha-addon
|
||||
|
||||
# Copy root filesystem
|
||||
COPY docker/ha-addon-rootfs/ /
|
||||
|
||||
# Copy esphome and install
|
||||
COPY . /esphome
|
||||
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
|
||||
|
||||
# Labels
|
||||
ARG BUILD_VERSION
|
||||
LABEL \
|
||||
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" \
|
||||
@@ -183,35 +76,9 @@ LABEL \
|
||||
io.hass.version="${BUILD_VERSION}"
|
||||
# io.hass.arch is inherited from addon-debian-base
|
||||
|
||||
ARG BUILD_TYPE
|
||||
FROM base-${BUILD_TYPE} AS final
|
||||
|
||||
|
||||
|
||||
# ======================= lint-type image =======================
|
||||
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
|
||||
# Copy esphome and install
|
||||
COPY . /esphome
|
||||
RUN uv pip install --no-cache-dir -e /esphome
|
||||
|
||||
@@ -54,7 +54,7 @@ manifest_parser = subparsers.add_parser(
|
||||
class DockerParams:
|
||||
build_to: str
|
||||
manifest_to: str
|
||||
baseimgtype: str
|
||||
build_type: str
|
||||
platform: str
|
||||
target: str
|
||||
|
||||
@@ -66,24 +66,19 @@ class DockerParams:
|
||||
TYPE_LINT: "esphome/esphome-lint",
|
||||
}[build_type]
|
||||
build_to = f"{prefix}-{arch}"
|
||||
baseimgtype = {
|
||||
TYPE_DOCKER: "docker",
|
||||
TYPE_HA_ADDON: "hassio",
|
||||
TYPE_LINT: "docker",
|
||||
}[build_type]
|
||||
platform = {
|
||||
ARCH_AMD64: "linux/amd64",
|
||||
ARCH_AARCH64: "linux/arm64",
|
||||
}[arch]
|
||||
target = {
|
||||
TYPE_DOCKER: "docker",
|
||||
TYPE_HA_ADDON: "hassio",
|
||||
TYPE_DOCKER: "final",
|
||||
TYPE_HA_ADDON: "final",
|
||||
TYPE_LINT: "lint",
|
||||
}[build_type]
|
||||
return cls(
|
||||
build_to=build_to,
|
||||
manifest_to=prefix,
|
||||
baseimgtype=baseimgtype,
|
||||
build_type=build_type,
|
||||
platform=platform,
|
||||
target=target,
|
||||
)
|
||||
@@ -145,7 +140,7 @@ def main():
|
||||
"buildx",
|
||||
"build",
|
||||
"--build-arg",
|
||||
f"BASEIMGTYPE={params.baseimgtype}",
|
||||
f"BUILD_TYPE={params.build_type}",
|
||||
"--build-arg",
|
||||
f"BUILD_VERSION={args.tag}",
|
||||
"--cache-from",
|
||||
|
||||
@@ -114,13 +114,14 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
|
||||
// fully off, disable output immediately
|
||||
this->gate_pin.digital_write(false);
|
||||
} else {
|
||||
auto min_us = this->cycle_time_us * this->min_power / 1000;
|
||||
if (this->method == DIM_METHOD_TRAILING) {
|
||||
this->enable_time_us = 1; // cannot be 0
|
||||
this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535);
|
||||
// calculate time until disable in µs with integer arithmetic and take into account min_power
|
||||
this->disable_time_us = std::max((uint32_t) 10, this->value * (this->cycle_time_us - min_us) / 65535 + min_us);
|
||||
} else {
|
||||
// calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
|
||||
// also take into account min_power
|
||||
auto min_us = this->cycle_time_us * this->min_power / 1000;
|
||||
this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
|
||||
|
||||
if (this->method == DIM_METHOD_LEADING_PULSE) {
|
||||
|
||||
@@ -14,7 +14,8 @@ void AnalogThresholdBinarySensor::setup() {
|
||||
if (std::isnan(sensor_value)) {
|
||||
this->publish_initial_state(false);
|
||||
} else {
|
||||
this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f);
|
||||
this->publish_initial_state(sensor_value >=
|
||||
(this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
|
||||
this->sensor_->add_on_state_callback([this](float sensor_value) {
|
||||
// if there is an invalid sensor reading, ignore the change and keep the current state
|
||||
if (!std::isnan(sensor_value)) {
|
||||
this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_));
|
||||
this->publish_state(sensor_value >=
|
||||
(this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -32,8 +34,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
|
||||
void AnalogThresholdBinarySensor::dump_config() {
|
||||
LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this);
|
||||
LOG_SENSOR(" ", "Sensor", this->sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_);
|
||||
ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_);
|
||||
ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_.value());
|
||||
ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_.value());
|
||||
}
|
||||
|
||||
} // namespace analog_threshold
|
||||
|
||||
@@ -15,14 +15,13 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void set_sensor(sensor::Sensor *analog_sensor);
|
||||
void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; }
|
||||
void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; }
|
||||
template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
|
||||
template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
|
||||
float upper_threshold_;
|
||||
float lower_threshold_;
|
||||
TemplatableValue<float> upper_threshold_{};
|
||||
TemplatableValue<float> lower_threshold_{};
|
||||
};
|
||||
|
||||
} // namespace analog_threshold
|
||||
|
||||
@@ -18,11 +18,11 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_THRESHOLD): cv.Any(
|
||||
cv.float_,
|
||||
cv.templatable(cv.float_),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_UPPER): cv.float_,
|
||||
cv.Required(CONF_LOWER): cv.float_,
|
||||
cv.Required(CONF_UPPER): cv.templatable(cv.float_),
|
||||
cv.Required(CONF_LOWER): cv.templatable(cv.float_),
|
||||
}
|
||||
),
|
||||
),
|
||||
@@ -39,9 +39,11 @@ async def to_code(config):
|
||||
sens = await cg.get_variable(config[CONF_SENSOR_ID])
|
||||
cg.add(var.set_sensor(sens))
|
||||
|
||||
if isinstance(config[CONF_THRESHOLD], float):
|
||||
cg.add(var.set_upper_threshold(config[CONF_THRESHOLD]))
|
||||
cg.add(var.set_lower_threshold(config[CONF_THRESHOLD]))
|
||||
if isinstance(config[CONF_THRESHOLD], dict):
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
|
||||
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
|
||||
else:
|
||||
cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER]))
|
||||
cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER]))
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
|
||||
upper = lower
|
||||
cg.add(var.set_upper_threshold(upper))
|
||||
cg.add(var.set_lower_threshold(lower))
|
||||
|
||||
@@ -82,6 +82,19 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
),
|
||||
)
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): validate_encryption_key,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _encryption_schema(config):
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -95,11 +108,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_KEY): validate_encryption_key,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
|
||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
@@ -151,9 +160,17 @@ async def to_code(config):
|
||||
config[CONF_ON_CLIENT_DISCONNECTED],
|
||||
)
|
||||
|
||||
if encryption_config := config.get(CONF_ENCRYPTION):
|
||||
decoded = base64.b64decode(encryption_config[CONF_KEY])
|
||||
cg.add(var.set_noise_psk(list(decoded)))
|
||||
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
decoded = base64.b64decode(key)
|
||||
cg.add(var.set_noise_psk(list(decoded)))
|
||||
else:
|
||||
# No key provided, but encryption desired
|
||||
# This will allow a plaintext client to provide a noise key,
|
||||
# send it to the device, and then switch to noise.
|
||||
# The key will be saved in flash and used for future connections
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.6")
|
||||
else:
|
||||
|
||||
@@ -31,6 +31,7 @@ service APIConnection {
|
||||
option (needs_authentication) = false;
|
||||
}
|
||||
rpc execute_service (ExecuteServiceRequest) returns (void) {}
|
||||
rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
|
||||
|
||||
rpc cover_command (CoverCommandRequest) returns (void) {}
|
||||
rpc fan_command (FanCommandRequest) returns (void) {}
|
||||
@@ -230,6 +231,9 @@ message DeviceInfoResponse {
|
||||
|
||||
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
|
||||
string bluetooth_mac_address = 18;
|
||||
|
||||
// Supports receiving and saving api encryption key
|
||||
bool api_encryption_supported = 19;
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -654,6 +658,23 @@ message SubscribeLogsResponse {
|
||||
bool send_failed = 4;
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
message NoiseEncryptionSetKeyRequest {
|
||||
option (id) = 124;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_API_NOISE";
|
||||
|
||||
bytes key = 1;
|
||||
}
|
||||
|
||||
message NoiseEncryptionSetKeyResponse {
|
||||
option (id) = 125;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_NOISE";
|
||||
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
// ==================== HOMEASSISTANT.SERVICE ====================
|
||||
message SubscribeHomeassistantServicesRequest {
|
||||
option (id) = 34;
|
||||
|
||||
@@ -62,7 +62,14 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
|
||||
: parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) {
|
||||
this->proto_write_buffer_.reserve(64);
|
||||
|
||||
#if defined(USE_API_PLAINTEXT)
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
|
||||
auto noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx->has_psk()) {
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
|
||||
} else {
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
|
||||
}
|
||||
#elif defined(USE_API_PLAINTEXT)
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
|
||||
#elif defined(USE_API_NOISE)
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
|
||||
@@ -1848,6 +1855,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();
|
||||
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
return resp;
|
||||
}
|
||||
@@ -1869,6 +1879,26 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
ESP_LOGV(TAG, "Could not find matching service!");
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
psk_t psk{};
|
||||
NoiseEncryptionSetKeyResponse resp;
|
||||
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
resp.success = false;
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
resp.success = false;
|
||||
return resp;
|
||||
}
|
||||
|
||||
resp.success = true;
|
||||
return resp;
|
||||
}
|
||||
#endif
|
||||
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
state_subs_at_ = 0;
|
||||
}
|
||||
|
||||
@@ -300,6 +300,9 @@ class APIConnection : public APIServerConnection {
|
||||
return {};
|
||||
}
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
|
||||
bool is_connection_setup() override {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -11,11 +11,20 @@ using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class APINoiseContext {
|
||||
public:
|
||||
void set_psk(psk_t psk) { psk_ = psk; }
|
||||
const psk_t &get_psk() const { return psk_; }
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
bool has_psk = false;
|
||||
for (auto i : psk) {
|
||||
has_psk |= i;
|
||||
}
|
||||
this->has_psk_ = has_psk;
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
protected:
|
||||
psk_t psk_;
|
||||
psk_t psk_{};
|
||||
bool has_psk_{false};
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
|
||||
@@ -792,6 +792,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
this->voice_assistant_feature_flags = value.as_uint32();
|
||||
return true;
|
||||
}
|
||||
case 19: {
|
||||
this->api_encryption_supported = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -865,6 +869,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(17, this->voice_assistant_feature_flags);
|
||||
buffer.encode_string(16, this->suggested_area);
|
||||
buffer.encode_string(18, this->bluetooth_mac_address);
|
||||
buffer.encode_bool(19, this->api_encryption_supported);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void DeviceInfoResponse::dump_to(std::string &out) const {
|
||||
@@ -946,6 +951,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
|
||||
out.append(" bluetooth_mac_address: ");
|
||||
out.append("'").append(this->bluetooth_mac_address).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" api_encryption_supported: ");
|
||||
out.append(YESNO(this->api_encryption_supported));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
@@ -3009,6 +3018,48 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("NoiseEncryptionSetKeyRequest {\n");
|
||||
out.append(" key: ");
|
||||
out.append("'").append(this->key).append("'");
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool NoiseEncryptionSetKeyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->success = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("NoiseEncryptionSetKeyResponse {\n");
|
||||
out.append(" success: ");
|
||||
out.append(YESNO(this->success));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
|
||||
|
||||
@@ -355,6 +355,7 @@ class DeviceInfoResponse : public ProtoMessage {
|
||||
uint32_t voice_assistant_feature_flags{0};
|
||||
std::string suggested_area{};
|
||||
std::string bluetooth_mac_address{};
|
||||
bool api_encryption_supported{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
@@ -791,6 +792,28 @@ class SubscribeLogsResponse : public ProtoMessage {
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class NoiseEncryptionSetKeyRequest : public ProtoMessage {
|
||||
public:
|
||||
std::string key{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
class NoiseEncryptionSetKeyResponse : public ProtoMessage {
|
||||
public:
|
||||
bool success{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class SubscribeHomeassistantServicesRequest : public ProtoMessage {
|
||||
public:
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
|
||||
@@ -179,6 +179,16 @@ bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorSt
|
||||
bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) {
|
||||
return this->send_message_<SubscribeLogsResponse>(msg, 29);
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServerConnectionBase::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_noise_encryption_set_key_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<NoiseEncryptionSetKeyResponse>(msg, 125);
|
||||
}
|
||||
#endif
|
||||
bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str());
|
||||
@@ -1191,6 +1201,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_voice_assistant_set_configuration(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case 124: {
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_noise_encryption_set_key_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -1311,6 +1332,22 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest
|
||||
}
|
||||
this->execute_service(msg);
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
|
||||
if (!this->send_noise_encryption_set_key_response(ret)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
|
||||
@@ -83,6 +83,12 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
|
||||
bool send_subscribe_logs_response(const SubscribeLogsResponse &msg);
|
||||
#ifdef USE_API_NOISE
|
||||
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg);
|
||||
#endif
|
||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||
bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg);
|
||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||
@@ -349,6 +355,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#ifdef USE_API_NOISE
|
||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
virtual void cover_command(const CoverCommandRequest &msg) = 0;
|
||||
#endif
|
||||
@@ -457,6 +466,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
void on_get_time_request(const GetTimeRequest &msg) override;
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
@@ -22,22 +22,40 @@ namespace api {
|
||||
static const char *const TAG = "api";
|
||||
|
||||
// APIServer
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
|
||||
void APIServer::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
|
||||
this->setup_controller();
|
||||
socket_ = socket::socket_ip(SOCK_STREAM, 0);
|
||||
if (socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket.");
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
uint32_t hash = 88491486UL;
|
||||
|
||||
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
|
||||
|
||||
SavedNoisePsk noise_pref_saved{};
|
||||
if (this->noise_pref_.load(&noise_pref_saved)) {
|
||||
ESP_LOGD(TAG, "Loaded saved Noise PSK");
|
||||
|
||||
this->set_noise_psk(noise_pref_saved.psk);
|
||||
}
|
||||
#endif
|
||||
|
||||
this->socket_ = socket::socket_ip(SOCK_STREAM, 0);
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
int enable = 1;
|
||||
int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
||||
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
|
||||
// we can still continue
|
||||
}
|
||||
err = socket_->setblocking(false);
|
||||
err = this->socket_->setblocking(false);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
|
||||
this->mark_failed();
|
||||
@@ -53,14 +71,14 @@ void APIServer::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
err = socket_->bind((struct sockaddr *) &server, sl);
|
||||
err = this->socket_->bind((struct sockaddr *) &server, sl);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
err = socket_->listen(4);
|
||||
err = this->socket_->listen(4);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
|
||||
this->mark_failed();
|
||||
@@ -92,18 +110,19 @@ void APIServer::setup() {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::loop() {
|
||||
// Accept new clients
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
|
||||
auto sock = this->socket_->accept((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
clients_.emplace_back(conn);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
}
|
||||
|
||||
@@ -136,16 +155,22 @@ void APIServer::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "API Server:");
|
||||
ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Using noise encryption: YES");
|
||||
ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
if (!this->noise_ctx_->has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Supports noise encryption: YES");
|
||||
}
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Using noise encryption: NO");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIServer::uses_password() const { return !this->password_.empty(); }
|
||||
|
||||
bool APIServer::check_password(const std::string &password) const {
|
||||
// depend only on input password length
|
||||
const char *a = this->password_.c_str();
|
||||
@@ -174,7 +199,9 @@ bool APIServer::check_password(const std::string &password) const {
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
|
||||
if (obj->is_internal())
|
||||
@@ -342,57 +369,6 @@ void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
}
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
|
||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
}
|
||||
}
|
||||
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = false,
|
||||
});
|
||||
}
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = true,
|
||||
});
|
||||
};
|
||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
||||
return this->state_subs_;
|
||||
}
|
||||
uint16_t APIServer::get_port() const { return this->port_; }
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void APIServer::request_time() {
|
||||
for (auto &client : this->clients_) {
|
||||
if (!client->remove_ && client->is_authenticated())
|
||||
client->send_time_request();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
|
||||
void APIServer::on_shutdown() {
|
||||
for (auto &c : this->clients_) {
|
||||
c->send_disconnect_request(DisconnectRequest());
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
|
||||
if (obj->is_internal())
|
||||
@@ -402,6 +378,96 @@ void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
|
||||
}
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
|
||||
|
||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = false,
|
||||
});
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = true,
|
||||
});
|
||||
};
|
||||
|
||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
||||
return this->state_subs_;
|
||||
}
|
||||
|
||||
uint16_t APIServer::get_port() const { return this->port_; }
|
||||
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
auto &old_psk = this->noise_ctx_->get_psk();
|
||||
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
|
||||
ESP_LOGW(TAG, "New PSK matches old");
|
||||
return true;
|
||||
}
|
||||
|
||||
SavedNoisePsk new_saved_psk{psk};
|
||||
if (!this->noise_pref_.save(&new_saved_psk)) {
|
||||
ESP_LOGW(TAG, "Failed to save Noise PSK");
|
||||
return false;
|
||||
}
|
||||
// ensure it's written immediately
|
||||
if (!global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "Failed to sync preferences");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Noise PSK saved");
|
||||
if (make_active) {
|
||||
this->set_timeout(100, [this, psk]() {
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset connections");
|
||||
this->set_noise_psk(psk);
|
||||
for (auto &c : this->clients_) {
|
||||
c->send_disconnect_request(DisconnectRequest());
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void APIServer::request_time() {
|
||||
for (auto &client : this->clients_) {
|
||||
if (!client->remove_ && client->is_authenticated())
|
||||
client->send_time_request();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
|
||||
|
||||
void APIServer::on_shutdown() {
|
||||
for (auto &c : this->clients_) {
|
||||
c->send_disconnect_request(DisconnectRequest());
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
#endif
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
psk_t psk;
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
class APIServer : public Component, public Controller {
|
||||
public:
|
||||
APIServer();
|
||||
@@ -35,6 +41,7 @@ class APIServer : public Component, public Controller {
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
|
||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
@@ -142,6 +149,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
port: int = int(conf[CONF_PORT])
|
||||
password: str = conf[CONF_PASSWORD]
|
||||
noise_psk: str | None = None
|
||||
if CONF_ENCRYPTION in conf:
|
||||
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
|
||||
if encryption_config := conf.get(CONF_ENCRYPTION):
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
noise_psk = key
|
||||
_LOGGER.info("Starting log output from %s using esphome API", address)
|
||||
cli = APIClient(
|
||||
address,
|
||||
|
||||
@@ -265,6 +265,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::CONNECTING) {
|
||||
if (connection->disconnect_pending()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
connection->cancel_pending_disconnect();
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,8 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE
|
||||
|
||||
ColorStruct = cg.esphome_ns.struct("Color")
|
||||
|
||||
INSTANCE_TYPE = ColorStruct
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_RED_INT = "red_int"
|
||||
|
||||
5
esphome/components/const/__init__.py
Normal file
5
esphome/components/const/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants used by esphome components."""
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
@@ -251,7 +251,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
|
||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 5)
|
||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 6)
|
||||
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||
# - https://github.com/platformio/platform-espressif32/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
||||
@@ -274,12 +274,15 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
||||
# pioarduino versions that don't require a release number
|
||||
# List based on https://github.com/pioarduino/esp-idf/releases
|
||||
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
|
||||
cv.Version(5, 5, 0),
|
||||
cv.Version(5, 4, 1),
|
||||
cv.Version(5, 4, 0),
|
||||
cv.Version(5, 3, 3),
|
||||
cv.Version(5, 3, 2),
|
||||
cv.Version(5, 3, 1),
|
||||
cv.Version(5, 3, 0),
|
||||
cv.Version(5, 1, 5),
|
||||
cv.Version(5, 1, 6),
|
||||
]
|
||||
|
||||
|
||||
@@ -321,8 +324,8 @@ def _arduino_check_versions(value):
|
||||
def _esp_idf_check_versions(value):
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(5, 1, 5), "https://github.com/espressif/esp-idf.git"),
|
||||
"latest": (cv.Version(5, 1, 5), None),
|
||||
"dev": (cv.Version(5, 1, 6), "https://github.com/espressif/esp-idf.git"),
|
||||
"latest": (cv.Version(5, 1, 6), None),
|
||||
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ void ESP32BLETracker::setup() {
|
||||
|
||||
global_esp32_ble_tracker = this;
|
||||
this->scan_result_lock_ = xSemaphoreCreateMutex();
|
||||
this->scan_end_lock_ = xSemaphoreCreateMutex();
|
||||
|
||||
#ifdef USE_OTA
|
||||
ota::get_global_ota_callback()->add_on_state_callback(
|
||||
@@ -117,119 +116,104 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
bool promote_to_connecting = discovered && !searching && !connecting;
|
||||
|
||||
if (!this->scanner_idle_) {
|
||||
if (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)) {
|
||||
uint32_t index = this->scan_result_index_;
|
||||
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
|
||||
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
|
||||
}
|
||||
if (this->scanner_state_ == ScannerState::RUNNING &&
|
||||
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)) {
|
||||
uint32_t index = this->scan_result_index_;
|
||||
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
|
||||
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_) {
|
||||
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
}
|
||||
|
||||
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_) {
|
||||
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 (client->parse_device(device)) {
|
||||
found = true;
|
||||
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
||||
promote_to_connecting = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
}
|
||||
this->scan_result_index_ = 0;
|
||||
xSemaphoreGive(this->scan_result_lock_);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
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 (!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;
|
||||
}
|
||||
this->scan_result_index_ = 0;
|
||||
xSemaphoreGive(this->scan_result_lock_);
|
||||
}
|
||||
if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
this->end_of_scan_(); // Change state to IDLE
|
||||
}
|
||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||
this->stop_scan_();
|
||||
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...",
|
||||
std::numeric_limits<uint8_t>::max());
|
||||
App.reboot();
|
||||
}
|
||||
|
||||
if (this->scan_start_failed_ || this->scan_set_param_failed_) {
|
||||
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...",
|
||||
std::numeric_limits<uint8_t>::max());
|
||||
App.reboot();
|
||||
}
|
||||
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;
|
||||
}
|
||||
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
|
||||
// clients and no clients using the scanner to search for
|
||||
// devices, then stop scanning and promote the discovered
|
||||
// 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_) {
|
||||
if (client->state() == ClientState::DISCOVERED) {
|
||||
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
|
||||
// Scanner is not running since we got the
|
||||
// lock, so we can promote the client.
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGD(TAG, "Stopping scan to make connection...");
|
||||
this->stop_scan_();
|
||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGD(TAG, "Promoting client to connect...");
|
||||
// We only want to promote one client at a time.
|
||||
// once the scanner is fully stopped.
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Pausing scan to make connection...");
|
||||
this->stop_scan_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -237,13 +221,7 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::start_scan() {
|
||||
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::start_scan() { this->start_scan_(true); }
|
||||
|
||||
void ESP32BLETracker::stop_scan() {
|
||||
ESP_LOGD(TAG, "Stopping scan.");
|
||||
@@ -251,16 +229,23 @@ void ESP32BLETracker::stop_scan() {
|
||||
this->stop_scan_();
|
||||
}
|
||||
|
||||
void ESP32BLETracker::ble_before_disabled_event_handler() {
|
||||
this->stop_scan_();
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
}
|
||||
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
|
||||
|
||||
void ESP32BLETracker::stop_scan_() {
|
||||
this->cancel_timeout("scan");
|
||||
if (this->scanner_idle_) {
|
||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||
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;
|
||||
}
|
||||
this->cancel_timeout("scan");
|
||||
this->scanner_state_ = ScannerState::STOPPING;
|
||||
esp_err_t err = esp_ble_gap_stop_scanning();
|
||||
if (err != ESP_OK) {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
// The lock must be held when calling this function.
|
||||
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
|
||||
ESP_LOGE(TAG, "start_scan called without holding scan_end_lock_");
|
||||
if (this->scanner_state_ != ScannerState::IDLE) {
|
||||
if (this->scanner_state_ == ScannerState::STARTING) {
|
||||
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;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Starting scan...");
|
||||
this->scanner_state_ = ScannerState::STARTING;
|
||||
ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
|
||||
if (!first) {
|
||||
for (auto *listener : this->listeners_)
|
||||
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);
|
||||
return;
|
||||
}
|
||||
this->scanner_idle_ = false;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::end_of_scan_() {
|
||||
// The lock must be held when calling this function.
|
||||
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
|
||||
ESP_LOGE(TAG, "end_of_scan_ called without holding the scan_end_lock_");
|
||||
if (this->scanner_state_ != ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped.");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "End of scan.");
|
||||
this->scanner_idle_ = true;
|
||||
ESP_LOGD(TAG, "End of scan, set scanner state to IDLE.");
|
||||
this->already_discovered_.clear();
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
this->cancel_timeout("scan");
|
||||
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
this->scanner_state_ = ScannerState::IDLE;
|
||||
}
|
||||
|
||||
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 ¶m) {
|
||||
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", 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) {
|
||||
this->scan_start_fail_count_ = 0;
|
||||
this->scanner_state_ = ScannerState::RUNNING;
|
||||
} else {
|
||||
this->scanner_state_ = ScannerState::FAILED;
|
||||
if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) {
|
||||
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 ¶m) {
|
||||
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->scanner_state_ = ScannerState::STOPPED;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
||||
@@ -417,7 +435,21 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re
|
||||
xSemaphoreGive(this->scan_result_lock_);
|
||||
}
|
||||
} 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->scanner_state_ = ScannerState::STOPPED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,8 +712,26 @@ void ESP32BLETracker::dump_config() {
|
||||
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, " Continuous Scanning: %s", YESNO(this->scan_continuous_));
|
||||
ESP_LOGCONFIG(TAG, " Scanner Idle: %s", YESNO(this->scanner_idle_));
|
||||
ESP_LOGCONFIG(TAG, " Scan End: %s", YESNO(xSemaphoreGetMutexHolder(this->scan_end_lock_) == nullptr));
|
||||
switch (this->scanner_state_) {
|
||||
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_,
|
||||
searching_, disconnecting_);
|
||||
if (this->scan_start_fail_count_) {
|
||||
|
||||
@@ -154,6 +154,21 @@ enum class ClientState {
|
||||
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 {
|
||||
// The default connection type, we hold all the services in ram
|
||||
// for the duration of the connection.
|
||||
@@ -173,6 +188,8 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
|
||||
virtual void connect() = 0;
|
||||
virtual void disconnect() = 0;
|
||||
bool disconnect_pending() const { return this->want_disconnect_; }
|
||||
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
||||
virtual void set_state(ClientState st) {
|
||||
this->state_ = st;
|
||||
if (st == ClientState::IDLE) {
|
||||
@@ -255,12 +272,11 @@ class ESP32BLETracker : public Component,
|
||||
uint8_t scan_start_fail_count_{0};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
bool scanner_idle_{true};
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
bool ble_was_disabled_{true};
|
||||
bool raw_advertisements_{false};
|
||||
bool parse_advertisements_{false};
|
||||
SemaphoreHandle_t scan_result_lock_;
|
||||
SemaphoreHandle_t scan_end_lock_;
|
||||
size_t scan_result_index_{0};
|
||||
#ifdef USE_PSRAM
|
||||
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32;
|
||||
|
||||
@@ -17,7 +17,7 @@ static const char *const TAG = "esp32_can";
|
||||
static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) {
|
||||
switch (bitrate) {
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H6)
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
case canbus::CAN_1KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS();
|
||||
return true;
|
||||
|
||||
@@ -58,7 +58,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
channel.flags.io_loop_back = 0;
|
||||
channel.flags.io_od_mode = 0;
|
||||
channel.flags.invert_out = 0;
|
||||
channel.flags.with_dma = 0;
|
||||
channel.flags.with_dma = this->use_dma_;
|
||||
channel.intr_priority = 0;
|
||||
if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Channel creation failed");
|
||||
|
||||
@@ -51,6 +51,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
|
||||
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
|
||||
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
|
||||
void set_use_dma(bool use_dma) { this->use_dma_ = use_dma; }
|
||||
void set_use_psram(bool use_psram) { this->use_psram_ = use_psram; }
|
||||
|
||||
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
|
||||
@@ -85,7 +86,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
rmt_encoder_handle_t encoder_{nullptr};
|
||||
rmt_symbol_word_t *rmt_buf_{nullptr};
|
||||
rmt_symbol_word_t bit0_, bit1_, reset_;
|
||||
uint32_t rmt_symbols_;
|
||||
uint32_t rmt_symbols_{48};
|
||||
#else
|
||||
rmt_item32_t *rmt_buf_{nullptr};
|
||||
rmt_item32_t bit0_, bit1_, reset_;
|
||||
@@ -94,11 +95,12 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
|
||||
uint8_t pin_;
|
||||
uint16_t num_leds_;
|
||||
bool is_rgbw_;
|
||||
bool is_wrgb_;
|
||||
bool use_psram_;
|
||||
bool is_rgbw_{false};
|
||||
bool is_wrgb_{false};
|
||||
bool use_dma_{false};
|
||||
bool use_psram_{false};
|
||||
|
||||
RGBOrder rgb_order_;
|
||||
RGBOrder rgb_order_{ORDER_RGB};
|
||||
|
||||
uint32_t last_refresh_{0};
|
||||
optional<uint32_t> max_refresh_rate_{};
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_rmt, light
|
||||
from esphome.components import esp32, esp32_rmt, light
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHIPSET,
|
||||
@@ -15,6 +15,7 @@ from esphome.const import (
|
||||
CONF_RGB_ORDER,
|
||||
CONF_RMT_CHANNEL,
|
||||
CONF_RMT_SYMBOLS,
|
||||
CONF_USE_DMA,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
@@ -138,6 +139,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
|
||||
cv.Optional(CONF_USE_DMA): cv.All(
|
||||
esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32S3]),
|
||||
cv.only_with_esp_idf,
|
||||
cv.boolean,
|
||||
),
|
||||
cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean,
|
||||
cv.Inclusive(
|
||||
CONF_BIT0_HIGH,
|
||||
@@ -211,6 +217,8 @@ async def to_code(config):
|
||||
|
||||
if esp32_rmt.use_new_rmt_driver():
|
||||
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
|
||||
if CONF_USE_DMA in config:
|
||||
cg.add(var.set_use_dma(config[CONF_USE_DMA]))
|
||||
else:
|
||||
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
|
||||
cg.add(
|
||||
|
||||
@@ -10,9 +10,11 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_URL,
|
||||
PLATFORM_HOST,
|
||||
__version__,
|
||||
)
|
||||
from esphome.core import CORE, Lambda
|
||||
from esphome.helpers import IS_MACOS
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
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)
|
||||
HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent)
|
||||
HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
|
||||
HttpRequestHost = http_request_ns.class_("HttpRequestHost", HttpRequestComponent)
|
||||
|
||||
HttpContainer = http_request_ns.class_("HttpContainer")
|
||||
|
||||
@@ -43,10 +46,13 @@ CONF_REDIRECT_LIMIT = "redirect_limit"
|
||||
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
|
||||
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
|
||||
CONF_BUFFER_SIZE_TX = "buffer_size_tx"
|
||||
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
|
||||
|
||||
CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
|
||||
CONF_ON_RESPONSE = "on_response"
|
||||
CONF_HEADERS = "headers"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_COLLECT_HEADERS = "collect_headers"
|
||||
CONF_BODY = "body"
|
||||
CONF_JSON = "json"
|
||||
CONF_CAPTURE_RESPONSE = "capture_response"
|
||||
@@ -85,6 +91,8 @@ def validate_ssl_verification(config):
|
||||
|
||||
|
||||
def _declare_request_class(value):
|
||||
if CORE.is_host:
|
||||
return cv.declare_id(HttpRequestHost)(value)
|
||||
if CORE.using_esp_idf:
|
||||
return cv.declare_id(HttpRequestIDF)(value)
|
||||
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.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),
|
||||
cv.require_framework_version(
|
||||
@@ -126,6 +138,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
esp_idf=cv.Version(0, 0, 0),
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
host=cv.Version(0, 0, 0),
|
||||
),
|
||||
validate_ssl_verification,
|
||||
)
|
||||
@@ -168,6 +181,21 @@ async def to_code(config):
|
||||
cg.add_library("ESP8266HTTPClient", None)
|
||||
if CORE.is_rp2040 and CORE.using_arduino:
|
||||
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)
|
||||
|
||||
@@ -176,9 +204,13 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HttpRequestComponent),
|
||||
cv.Required(CONF_URL): cv.templatable(validate_url),
|
||||
cv.Optional(CONF_HEADERS): cv.All(
|
||||
cv.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.Optional(CONF_COLLECT_HEADERS): cv.ensure_list(cv.string),
|
||||
cv.Optional(CONF_VERIFY_SSL): cv.invalid(
|
||||
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_:
|
||||
template_ = await cg.templatable(json_[key], args, cg.std_string)
|
||||
cg.add(var.add_json(key, template_))
|
||||
for key in config.get(CONF_HEADERS, []):
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_HEADERS][key], args, cg.const_char_ptr
|
||||
)
|
||||
cg.add(var.add_header(key, template_))
|
||||
for key in config.get(CONF_REQUEST_HEADERS, []):
|
||||
template_ = await cg.templatable(key, args, cg.std_string)
|
||||
cg.add(var.add_request_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, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
|
||||
@@ -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 esphome
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -95,9 +96,19 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
|
||||
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:
|
||||
size_t bytes_read_{0};
|
||||
bool secure_{false};
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
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_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(std::string url, std::list<Header> headers) {
|
||||
return this->start(std::move(url), "GET", "", std::move(headers));
|
||||
std::shared_ptr<HttpContainer> get(const std::string &url) { return this->start(url, "GET", "", {}); }
|
||||
std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers) {
|
||||
return this->start(url, "GET", "", request_headers);
|
||||
}
|
||||
std::shared_ptr<HttpContainer> post(std::string url, std::string body) {
|
||||
return this->start(std::move(url), "POST", std::move(body), {});
|
||||
std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers,
|
||||
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) {
|
||||
return this->start(std::move(url), "POST", std::move(body), std::move(headers));
|
||||
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
|
||||
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::list<Header> headers) = 0;
|
||||
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
|
||||
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:
|
||||
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};
|
||||
bool follow_redirects_{};
|
||||
uint16_t redirect_limit_{};
|
||||
@@ -149,7 +185,11 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
TEMPLATABLE_VALUE(std::string, body)
|
||||
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}); }
|
||||
|
||||
@@ -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);
|
||||
body = json::build_json(f);
|
||||
}
|
||||
std::list<Header> headers;
|
||||
for (const auto &item : this->headers_) {
|
||||
std::list<Header> request_headers;
|
||||
for (const auto &item : this->request_headers_) {
|
||||
auto val = item.second;
|
||||
Header header;
|
||||
header.name = item.first;
|
||||
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) {
|
||||
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); }
|
||||
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::function<void(Ts..., JsonObject)> json_func_{nullptr};
|
||||
std::vector<HttpRequestResponseTrigger *> response_triggers_{};
|
||||
|
||||
@@ -14,8 +14,9 @@ namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.arduino";
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::string method, std::string body,
|
||||
std::list<Header> headers) {
|
||||
std::shared_ptr<HttpContainer> HttpRequestArduino::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()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
// returned needed headers must be collected before the requests
|
||||
static const char *header_keys[] = {"Content-Length", "Content-Type"};
|
||||
static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]);
|
||||
container->client_.collectHeaders(header_keys, HEADER_COUNT);
|
||||
const char *header_keys[collect_headers.size()];
|
||||
int index = 0;
|
||||
for (auto const &header_name : collect_headers) {
|
||||
header_keys[index++] = header_name.c_str();
|
||||
}
|
||||
container->client_.collectHeaders(header_keys, index);
|
||||
|
||||
App.feed_wdt();
|
||||
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
|
||||
}
|
||||
|
||||
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();
|
||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||
container->content_length = (size_t) content_length;
|
||||
|
||||
@@ -29,9 +29,10 @@ class HttpContainerArduino : public HttpContainer {
|
||||
};
|
||||
|
||||
class HttpRequestArduino : public HttpRequestComponent {
|
||||
public:
|
||||
std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
|
||||
std::list<Header> headers) override;
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace http_request
|
||||
|
||||
141
esphome/components/http_request/http_request_host.cpp
Normal file
141
esphome/components/http_request/http_request_host.cpp
Normal 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
|
||||
37
esphome/components/http_request/http_request_host.h
Normal file
37
esphome/components/http_request/http_request_host.h
Normal 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
|
||||
@@ -19,14 +19,41 @@ namespace http_request {
|
||||
|
||||
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() {
|
||||
HttpRequestComponent::dump_config();
|
||||
ESP_LOGCONFIG(TAG, " Buffer Size RX: %u", this->buffer_size_rx_);
|
||||
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,
|
||||
std::list<Header> headers) {
|
||||
esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
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()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
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();
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -124,6 +155,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
container->set_response_headers(user_data.response_headers);
|
||||
if (is_success(container->status_code)) {
|
||||
container->duration_ms = millis() - start;
|
||||
return container;
|
||||
|
||||
@@ -21,6 +21,10 @@ class HttpContainerIDF : public HttpContainer {
|
||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||
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:
|
||||
esp_http_client_handle_t client_;
|
||||
};
|
||||
@@ -29,16 +33,19 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
public:
|
||||
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_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
|
||||
|
||||
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
|
||||
uint16_t buffer_size_rx_{};
|
||||
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
|
||||
|
||||
9691
esphome/components/http_request/httplib.h
Normal file
9691
esphome/components/http_request/httplib.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,15 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
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
|
||||
import esphome.final_validate as fv
|
||||
|
||||
@@ -35,6 +43,9 @@ CONF_MONO = "mono"
|
||||
CONF_LEFT = "left"
|
||||
CONF_RIGHT = "right"
|
||||
CONF_STEREO = "stereo"
|
||||
CONF_BOTH = "both"
|
||||
|
||||
CONF_USE_LEGACY = "use_legacy"
|
||||
|
||||
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
|
||||
I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component)
|
||||
@@ -50,6 +61,12 @@ I2S_MODE_OPTIONS = {
|
||||
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
|
||||
I2S_PORTS = {
|
||||
VARIANT_ESP32: 2,
|
||||
@@ -60,10 +77,23 @@ I2S_PORTS = {
|
||||
|
||||
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
|
||||
I2S_CHANNELS = {
|
||||
CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT,
|
||||
CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
|
||||
CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_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, # mono data
|
||||
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, # mono data
|
||||
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")
|
||||
@@ -83,8 +113,19 @@ I2S_BITS_PER_CHANNEL = {
|
||||
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,
|
||||
}
|
||||
|
||||
_validate_bits = cv.float_with_unit("bits", "bit")
|
||||
|
||||
_use_legacy_driver = None
|
||||
|
||||
|
||||
def i2s_audio_component_schema(
|
||||
class_: MockObjClass,
|
||||
@@ -97,20 +138,22 @@ def i2s_audio_component_schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
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(
|
||||
min=1
|
||||
),
|
||||
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(
|
||||
I2S_MODE_OPTIONS, lower=True
|
||||
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.one_of(
|
||||
*I2S_MODE_OPTIONS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All(
|
||||
cv.Any(cv.float_with_unit("bits", "bit"), "default"),
|
||||
cv.enum(I2S_BITS_PER_CHANNEL),
|
||||
cv.one_of(*I2S_BITS_PER_CHANNEL),
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -118,22 +161,60 @@ def i2s_audio_component_schema(
|
||||
|
||||
async def register_i2s_audio_component(var, config):
|
||||
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
|
||||
|
||||
cg.add(var.set_i2s_mode(config[CONF_I2S_MODE]))
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
if use_legacy():
|
||||
cg.add(var.set_i2s_mode(I2S_MODE_OPTIONS[config[CONF_I2S_MODE]]))
|
||||
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_CHANNEL]])
|
||||
)
|
||||
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]))
|
||||
|
||||
|
||||
CONFIG_SCHEMA = 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,
|
||||
}
|
||||
def validate_use_legacy(value):
|
||||
global _use_legacy_driver # noqa: PLW0603
|
||||
if CONF_USE_LEGACY in value:
|
||||
if (_use_legacy_driver is not None) and (
|
||||
_use_legacy_driver != value[CONF_USE_LEGACY]
|
||||
):
|
||||
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 +229,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
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
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]))
|
||||
if CONF_I2S_BCLK_PIN in config:
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <driver/i2s.h>
|
||||
#include "esphome/core/component.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 i2s_audio {
|
||||
@@ -13,19 +18,33 @@ class I2SAudioComponent;
|
||||
|
||||
class I2SAudioBase : public Parented<I2SAudioComponent> {
|
||||
public:
|
||||
#ifdef USE_I2S_LEGACY
|
||||
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_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_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; }
|
||||
|
||||
protected:
|
||||
#ifdef USE_I2S_LEGACY
|
||||
i2s_mode_t i2s_mode_{};
|
||||
i2s_channel_fmt_t channel_;
|
||||
uint32_t sample_rate_;
|
||||
i2s_bits_per_sample_t bits_per_sample_;
|
||||
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_;
|
||||
};
|
||||
|
||||
@@ -37,6 +56,7 @@ class I2SAudioComponent : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
i2s_pin_config_t get_pin_config() const {
|
||||
return {
|
||||
.mck_io_num = this->mclk_pin_,
|
||||
@@ -46,6 +66,20 @@ class I2SAudioComponent : public Component {
|
||||
.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_bclk_pin(int pin) { this->bclk_pin_ = pin; }
|
||||
@@ -62,9 +96,13 @@ class I2SAudioComponent : public Component {
|
||||
|
||||
I2SAudioIn *audio_in_{nullptr};
|
||||
I2SAudioOut *audio_out_{nullptr};
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
int mclk_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_;
|
||||
i2s_port_t port_{};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ from .. import (
|
||||
I2SAudioComponent,
|
||||
I2SAudioOut,
|
||||
i2s_audio_ns,
|
||||
use_legacy,
|
||||
)
|
||||
|
||||
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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -6,12 +6,15 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NUMBER
|
||||
|
||||
from .. import (
|
||||
CONF_CHANNEL,
|
||||
CONF_I2S_DIN_PIN,
|
||||
CONF_MONO,
|
||||
CONF_RIGHT,
|
||||
I2SAudioIn,
|
||||
i2s_audio_component_schema,
|
||||
i2s_audio_ns,
|
||||
register_i2s_audio_component,
|
||||
use_legacy,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -43,6 +46,12 @@ def validate_esp32_variant(config):
|
||||
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
|
||||
|
||||
|
||||
BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
||||
i2s_audio_component_schema(
|
||||
I2SAudioMicrophone,
|
||||
@@ -71,9 +80,19 @@ CONFIG_SCHEMA = cv.All(
|
||||
key=CONF_ADC_TYPE,
|
||||
),
|
||||
validate_esp32_variant,
|
||||
validate_channel,
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
#include <driver/i2s.h>
|
||||
#else
|
||||
#include <driver/i2s_std.h>
|
||||
#include <driver/i2s_pdm.h>
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -16,6 +21,7 @@ static const char *const TAG = "i2s_audio.microphone";
|
||||
|
||||
void I2SAudioMicrophone::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone...");
|
||||
#ifdef USE_I2S_LEGACY
|
||||
#if SOC_I2S_SUPPORTS_ADC
|
||||
if (this->adc_) {
|
||||
if (this->parent_->get_port() != I2S_NUM_0) {
|
||||
@@ -24,6 +30,7 @@ void I2SAudioMicrophone::setup() {
|
||||
return;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
{
|
||||
if (this->pdm_) {
|
||||
@@ -47,6 +54,9 @@ void I2SAudioMicrophone::start_() {
|
||||
if (!this->parent_->try_lock()) {
|
||||
return; // Waiting for another i2s to return lock
|
||||
}
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
i2s_driver_config_t config = {
|
||||
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX),
|
||||
.sample_rate = this->sample_rate_,
|
||||
@@ -63,8 +73,6 @@ void I2SAudioMicrophone::start_() {
|
||||
.bits_per_chan = this->bits_per_channel_,
|
||||
};
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
#if SOC_I2S_SUPPORTS_ADC
|
||||
if (this->adc_) {
|
||||
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN);
|
||||
@@ -111,6 +119,109 @@ void I2SAudioMicrophone::start_() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#else
|
||||
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;
|
||||
}
|
||||
|
||||
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_) {
|
||||
i2s_pdm_rx_clk_config_t clk_cfg = {
|
||||
.sample_rate_hz = this->sample_rate_,
|
||||
.clk_src = clk_src,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
|
||||
.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 = I2S_MCLK_MULTIPLE_256,
|
||||
};
|
||||
i2s_data_bit_width_t data_bit_width;
|
||||
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_8BIT) {
|
||||
data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
|
||||
} else {
|
||||
data_bit_width = I2S_DATA_BIT_WIDTH_8BIT;
|
||||
}
|
||||
i2s_std_slot_config_t std_slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(data_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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->state_ = microphone::STATE_RUNNING;
|
||||
this->high_freq_.start();
|
||||
this->status_clear_error();
|
||||
@@ -128,6 +239,7 @@ void I2SAudioMicrophone::stop() {
|
||||
|
||||
void I2SAudioMicrophone::stop_() {
|
||||
esp_err_t err;
|
||||
#ifdef USE_I2S_LEGACY
|
||||
#if SOC_I2S_SUPPORTS_ADC
|
||||
if (this->adc_) {
|
||||
err = i2s_adc_disable(this->parent_->get_port());
|
||||
@@ -150,27 +262,50 @@ void I2SAudioMicrophone::stop_() {
|
||||
this->status_set_error();
|
||||
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->state_ = microphone::STATE_STOPPED;
|
||||
this->high_freq_.stop();
|
||||
this->status_clear_error();
|
||||
}
|
||||
|
||||
size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
|
||||
size_t I2SAudioMicrophone::read(int16_t *buf, size_t len, TickType_t ticks_to_wait) {
|
||||
size_t bytes_read = 0;
|
||||
esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, (100 / portTICK_PERIOD_MS));
|
||||
if (err != ESP_OK) {
|
||||
#ifdef USE_I2S_LEGACY
|
||||
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));
|
||||
this->status_set_warning();
|
||||
return 0;
|
||||
}
|
||||
if (bytes_read == 0) {
|
||||
if ((bytes_read == 0) && (ticks_to_wait > 0)) {
|
||||
this->status_set_warning();
|
||||
return 0;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
// ESP-IDF I2S implementation right-extends 8-bit data to 16 bits,
|
||||
// and 24-bit data to 32 bits.
|
||||
#ifdef USE_I2S_LEGACY
|
||||
switch (this->bits_per_sample_) {
|
||||
case I2S_BITS_PER_SAMPLE_8BIT:
|
||||
case I2S_BITS_PER_SAMPLE_16BIT:
|
||||
@@ -188,12 +323,36 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
|
||||
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
#ifndef USE_ESP32_VARIANT_ESP32
|
||||
// For newer ESP32 variants 8 bit data needs to be extended to 16 bit.
|
||||
if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_8BIT) {
|
||||
size_t samples_read = bytes_read / sizeof(int8_t);
|
||||
for (size_t i = samples_read - 1; i >= 0; i--) {
|
||||
int16_t temp = static_cast<int16_t>(reinterpret_cast<int8_t *>(buf)[i]) << 8;
|
||||
buf[i] = temp;
|
||||
}
|
||||
return samples_read * sizeof(int16_t);
|
||||
}
|
||||
#else
|
||||
// For ESP32 8/16 bit standard mono mode samples need to be switched.
|
||||
if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) {
|
||||
size_t samples_read = bytes_read / sizeof(int16_t);
|
||||
for (int i = 0; i < samples_read; i += 2) {
|
||||
int16_t tmp = buf[i];
|
||||
buf[i] = buf[i + 1];
|
||||
buf[i + 1] = tmp;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return bytes_read;
|
||||
#endif
|
||||
}
|
||||
|
||||
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));
|
||||
size_t bytes_read = this->read(samples.data(), BUFFER_SIZE * sizeof(int16_t), 0);
|
||||
samples.resize(bytes_read / sizeof(int16_t));
|
||||
this->data_callbacks_.call(samples);
|
||||
}
|
||||
|
||||
@@ -17,17 +17,24 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
void stop() override;
|
||||
|
||||
void loop() override;
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
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; }
|
||||
|
||||
size_t read(int16_t *buf, size_t len) override;
|
||||
size_t read(int16_t *buf, size_t len, TickType_t ticks_to_wait);
|
||||
size_t read(int16_t *buf, size_t len) override { return this->read(buf, len, pdMS_TO_TICKS(100)); }
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
#if SOC_I2S_SUPPORTS_ADC
|
||||
void set_adc_channel(adc1_channel_t channel) {
|
||||
this->adc_channel_ = channel;
|
||||
this->adc_ = true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
protected:
|
||||
@@ -35,10 +42,15 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
void stop_();
|
||||
void read_();
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
int8_t din_pin_{I2S_PIN_NO_CHANGE};
|
||||
#if SOC_I2S_SUPPORTS_ADC
|
||||
adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX};
|
||||
bool adc_{false};
|
||||
#endif
|
||||
#else
|
||||
gpio_num_t din_pin_{I2S_GPIO_UNUSED};
|
||||
i2s_chan_handle_t rx_handle_;
|
||||
#endif
|
||||
bool pdm_{false};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from .. import (
|
||||
i2s_audio_component_schema,
|
||||
i2s_audio_ns,
|
||||
register_i2s_audio_component,
|
||||
use_legacy,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
@@ -60,7 +61,7 @@ I2C_COMM_FMT_OPTIONS = {
|
||||
"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):
|
||||
@@ -101,7 +102,7 @@ def _validate_esp32_variant(config):
|
||||
if config[CONF_DAC_TYPE] != "internal":
|
||||
return config
|
||||
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")
|
||||
return config
|
||||
|
||||
@@ -143,8 +144,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(
|
||||
CONF_I2S_DOUT_PIN
|
||||
): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum(
|
||||
I2C_COMM_FMT_OPTIONS, lower=True
|
||||
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of(
|
||||
*I2C_COMM_FMT_OPTIONS, lower=True
|
||||
),
|
||||
}
|
||||
),
|
||||
@@ -157,6 +158,19 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -167,7 +181,17 @@ async def to_code(config):
|
||||
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
|
||||
else:
|
||||
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:
|
||||
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
#include <driver/i2s.h>
|
||||
#else
|
||||
#include <driver/i2s_std.h>
|
||||
#endif
|
||||
|
||||
#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.
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
i2s_event_t i2s_event;
|
||||
while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) {
|
||||
if (i2s_event.type == I2S_EVENT_TX_Q_OVF) {
|
||||
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_) {
|
||||
// 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_);
|
||||
}
|
||||
|
||||
#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
|
||||
// callback.
|
||||
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_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_) {
|
||||
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));
|
||||
@@ -336,6 +361,10 @@ void I2SAudioSpeaker::speaker_task(void *params) {
|
||||
audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written,
|
||||
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();
|
||||
|
||||
@@ -369,8 +398,12 @@ void I2SAudioSpeaker::speaker_task(void *params) {
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
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();
|
||||
}
|
||||
@@ -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) {
|
||||
#ifdef USE_I2S_LEGACY
|
||||
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
|
||||
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_) {
|
||||
#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
|
||||
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;
|
||||
}
|
||||
|
||||
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_;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
|
||||
|
||||
i2s_driver_config_t config = {
|
||||
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
|
||||
.sample_rate = audio_stream_info.get_sample_rate(),
|
||||
@@ -498,7 +541,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
|
||||
.communication_format = this->i2s_comm_fmt_,
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
.dma_buf_count = DMA_BUFFERS_COUNT,
|
||||
.dma_buf_len = dma_buffer_length,
|
||||
.dma_buf_len = (int) dma_buffer_length,
|
||||
.use_apll = this->use_apll_,
|
||||
.tx_desc_auto_clear = true,
|
||||
.fixed_mclk = I2S_PIN_NO_CHANGE,
|
||||
@@ -545,6 +588,89 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
|
||||
i2s_driver_uninstall(this->parent_->get_port());
|
||||
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 = I2S_MCLK_MULTIPLE_256,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -564,6 +690,15 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
|
||||
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 esphome
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#include "../i2s_audio.h"
|
||||
|
||||
#include <driver/i2s.h>
|
||||
|
||||
#include <freertos/event_groups.h>
|
||||
#include <freertos/queue.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_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
|
||||
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
|
||||
#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; }
|
||||
#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 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
|
||||
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
|
||||
/// @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.
|
||||
@@ -121,7 +128,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
|
||||
uint32_t buffer_duration_ms_;
|
||||
|
||||
optional<uint32_t> timeout_;
|
||||
uint8_t dout_pin_;
|
||||
|
||||
bool task_created_{false};
|
||||
bool pause_state_{false};
|
||||
@@ -130,10 +136,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
|
||||
|
||||
size_t bytes_written_{0};
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
#if SOC_I2S_SUPPORTS_DAC
|
||||
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
|
||||
#endif
|
||||
uint8_t dout_pin_;
|
||||
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};
|
||||
};
|
||||
|
||||
@@ -291,6 +291,8 @@ SOURCE_WEB = "web"
|
||||
|
||||
Image_ = image_ns.class_("Image")
|
||||
|
||||
INSTANCE_TYPE = Image_
|
||||
|
||||
|
||||
def compute_local_image_path(value) -> Path:
|
||||
url = value[CONF_URL] if isinstance(value, dict) else value
|
||||
|
||||
@@ -9,7 +9,7 @@ uint8_t temprature_sens_read();
|
||||
}
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C2)
|
||||
defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#include "driver/temp_sensor.h"
|
||||
#else
|
||||
@@ -33,7 +33,8 @@ static const char *const TAG = "internal_temperature";
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \
|
||||
(defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2))
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4))
|
||||
static temperature_sensor_handle_t tsensNew = NULL;
|
||||
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT
|
||||
#endif // USE_ESP32
|
||||
@@ -49,7 +50,7 @@ void InternalTemperatureSensor::update() {
|
||||
success = (raw != 128);
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C2)
|
||||
defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();
|
||||
temp_sensor_set_config(tsens);
|
||||
@@ -100,7 +101,8 @@ void InternalTemperatureSensor::setup() {
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \
|
||||
(defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2))
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4))
|
||||
ESP_LOGCONFIG(TAG, "Setting up temperature sensor...");
|
||||
|
||||
temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from esphome.automation import build_automation, register_action, validate_automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DRAW_ROUNDING
|
||||
from esphome.components.display import Display
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -24,7 +25,7 @@ from esphome.helpers import write_file_if_changed
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid
|
||||
from .automation import disp_update, focused_widgets, update_to_code
|
||||
from .defines import CONF_DRAW_ROUNDING, add_define
|
||||
from .defines import add_define
|
||||
from .encoders import (
|
||||
ENCODERS_CONFIG,
|
||||
encoders_to_code,
|
||||
@@ -323,7 +324,7 @@ async def to_code(configs):
|
||||
displays,
|
||||
frac,
|
||||
config[df.CONF_FULL_REFRESH],
|
||||
config[df.CONF_DRAW_ROUNDING],
|
||||
config[CONF_DRAW_ROUNDING],
|
||||
config[df.CONF_RESUME_ON_INPUT],
|
||||
)
|
||||
await cg.register_component(lv_component, config)
|
||||
@@ -413,7 +414,7 @@ LVGL_SCHEMA = cv.All(
|
||||
df.CONF_DEFAULT_FONT, default="montserrat_14"
|
||||
): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
|
||||
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
*df.LV_LOG_LEVELS, upper=True
|
||||
|
||||
@@ -424,7 +424,6 @@ CONF_DEFAULT_FONT = "default_font"
|
||||
CONF_DEFAULT_GROUP = "default_group"
|
||||
CONF_DIR = "dir"
|
||||
CONF_DISPLAYS = "displays"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_EDITING = "editing"
|
||||
CONF_ENCODERS = "encoders"
|
||||
CONF_END_ANGLE = "end_angle"
|
||||
|
||||
@@ -120,6 +120,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
|
||||
void LvglComponent::add_page(LvPageType *page) {
|
||||
this->pages_.push_back(page);
|
||||
page->set_parent(this);
|
||||
lv_disp_set_default(this->disp_);
|
||||
page->setup(this->pages_.size() - 1);
|
||||
}
|
||||
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
|
||||
@@ -433,7 +434,11 @@ void LvglComponent::setup() {
|
||||
auto height = display->get_height();
|
||||
size_t buffer_pixels = width * height / this->buffer_frac_;
|
||||
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
|
||||
auto *buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
|
||||
void *buffer = nullptr;
|
||||
if (this->buffer_frac_ >= 4)
|
||||
buffer = malloc(buf_bytes); // NOLINT
|
||||
if (buffer == nullptr)
|
||||
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
|
||||
if (buffer == nullptr) {
|
||||
this->mark_failed();
|
||||
this->status_set_error("Memory allocation failure");
|
||||
|
||||
@@ -67,12 +67,13 @@ class ArcType(NumberType):
|
||||
lv.arc_set_mode(w.obj, literal(config[CONF_MODE]))
|
||||
lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE])
|
||||
|
||||
if config.get(CONF_ADJUSTABLE) is False:
|
||||
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
|
||||
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
|
||||
elif CONF_GROUP not in config:
|
||||
# For some reason arc does not get automatically added to the default group
|
||||
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
|
||||
if CONF_ADJUSTABLE in config:
|
||||
if not config[CONF_ADJUSTABLE]:
|
||||
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
|
||||
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
|
||||
elif CONF_GROUP not in config:
|
||||
# For some reason arc does not get automatically added to the default group
|
||||
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
|
||||
|
||||
value = await get_start_value(config)
|
||||
if value is not None:
|
||||
|
||||
@@ -36,7 +36,6 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_SYMBOL): lv_text,
|
||||
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
|
||||
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
|
||||
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
|
||||
}
|
||||
)
|
||||
@@ -44,12 +43,14 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
|
||||
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
DROPDOWN_UPDATE_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_OPTIONS): cv.ensure_list(option_string),
|
||||
cv.Optional(CONF_DIR): DIRECTIONS.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ CONF_IMAGE = "image"
|
||||
|
||||
BASE_IMG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PIVOT_X, default="50%"): size,
|
||||
cv.Optional(CONF_PIVOT_Y, default="50%"): size,
|
||||
cv.Optional(CONF_PIVOT_X): size,
|
||||
cv.Optional(CONF_PIVOT_Y): size,
|
||||
cv.Optional(CONF_ANGLE): angle,
|
||||
cv.Optional(CONF_ZOOM): zoom,
|
||||
cv.Optional(CONF_OFFSET_X): size,
|
||||
@@ -63,10 +63,11 @@ class ImgType(WidgetType):
|
||||
async def to_code(self, w: Widget, config):
|
||||
if src := config.get(CONF_SRC):
|
||||
lv.img_set_src(w.obj, await lv_image.process(src))
|
||||
if (cf_angle := config.get(CONF_ANGLE)) is not None:
|
||||
pivot_x = config[CONF_PIVOT_X]
|
||||
pivot_y = config[CONF_PIVOT_Y]
|
||||
if (pivot_x := config.get(CONF_PIVOT_X)) and (
|
||||
pivot_y := config.get(CONF_PIVOT_Y)
|
||||
):
|
||||
lv.img_set_pivot(w.obj, pivot_x, pivot_y)
|
||||
if (cf_angle := config.get(CONF_ANGLE)) is not None:
|
||||
lv.img_set_angle(w.obj, cf_angle)
|
||||
if (img_zoom := config.get(CONF_ZOOM)) is not None:
|
||||
lv.img_set_zoom(w.obj, img_zoom)
|
||||
|
||||
134
esphome/components/mapping/__init__.py
Normal file
134
esphome/components/mapping/__init__.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import difflib
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FROM, CONF_ID, CONF_TO
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global
|
||||
from esphome.loader import get_component
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
MULTI_CONF = True
|
||||
|
||||
map_ = cg.std_ns.class_("map")
|
||||
|
||||
CONF_ENTRIES = "entries"
|
||||
CONF_CLASS = "class"
|
||||
|
||||
|
||||
class IndexType:
|
||||
"""
|
||||
Represents a type of index in a map.
|
||||
"""
|
||||
|
||||
def __init__(self, validator, data_type, conversion):
|
||||
self.validator = validator
|
||||
self.data_type = data_type
|
||||
self.conversion = conversion
|
||||
|
||||
|
||||
INDEX_TYPES = {
|
||||
"int": IndexType(cv.int_, cg.int_, int),
|
||||
"string": IndexType(cv.string, cg.std_string, str),
|
||||
}
|
||||
|
||||
|
||||
def to_schema(value):
|
||||
"""
|
||||
Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name.
|
||||
:param value:
|
||||
:return:
|
||||
"""
|
||||
return cv.Any(
|
||||
cv.one_of(*INDEX_TYPES, lower=True),
|
||||
cv.one_of(*CORE.id_classes.keys()),
|
||||
)(value)
|
||||
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(map_),
|
||||
cv.Required(CONF_FROM): cv.one_of(*INDEX_TYPES, lower=True),
|
||||
cv.Required(CONF_TO): cv.string,
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def get_object_type(to_):
|
||||
"""
|
||||
Get the object type from a string. Possible formats:
|
||||
xxx The name of a component which defines INSTANCE_TYPE
|
||||
esphome::xxx::yyy A C++ class name defined in a component
|
||||
xxx::yyy A C++ class name defined in a component
|
||||
yyy A C++ class name defined in the core
|
||||
"""
|
||||
|
||||
if cls := CORE.id_classes.get(to_):
|
||||
return cls
|
||||
if cls := CORE.id_classes.get(to_.removeprefix("esphome::")):
|
||||
return cls
|
||||
# get_component will throw a wobbly if we don't check this first.
|
||||
if "." in to_:
|
||||
return None
|
||||
if component := get_component(to_):
|
||||
return component.instance_type
|
||||
return None
|
||||
|
||||
|
||||
def map_schema(config):
|
||||
config = BASE_SCHEMA(config)
|
||||
if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict):
|
||||
raise cv.Invalid("an entries list is required for a map")
|
||||
entries = config[CONF_ENTRIES]
|
||||
if len(entries) == 0:
|
||||
raise cv.Invalid("Map must have at least one entry")
|
||||
to_ = config[CONF_TO]
|
||||
if to_ in INDEX_TYPES:
|
||||
value_type = INDEX_TYPES[to_].validator
|
||||
else:
|
||||
value_type = get_object_type(to_)
|
||||
if value_type is None:
|
||||
matches = difflib.get_close_matches(to_, CORE.id_classes)
|
||||
raise cv.Invalid(
|
||||
f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?"
|
||||
)
|
||||
value_type = cv.use_id(value_type)
|
||||
config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()}
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = map_schema
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
entries = config[CONF_ENTRIES]
|
||||
from_ = config[CONF_FROM]
|
||||
to_ = config[CONF_TO]
|
||||
index_conversion = INDEX_TYPES[from_].conversion
|
||||
index_type = INDEX_TYPES[from_].data_type
|
||||
if to_ in INDEX_TYPES:
|
||||
value_conversion = INDEX_TYPES[to_].conversion
|
||||
value_type = INDEX_TYPES[to_].data_type
|
||||
entries = {
|
||||
index_conversion(key): value_conversion(value)
|
||||
for key, value in entries.items()
|
||||
}
|
||||
else:
|
||||
entries = {
|
||||
index_conversion(key): await cg.get_variable(value)
|
||||
for key, value in entries.items()
|
||||
}
|
||||
value_type = get_object_type(to_)
|
||||
if list(entries.values())[0].op != ".":
|
||||
value_type = value_type.operator("ptr")
|
||||
varid = config[CONF_ID]
|
||||
varid.type = map_.template(index_type, value_type)
|
||||
var = MockObj(varid, ".")
|
||||
decl = VariableDeclarationExpression(varid.type, "", varid)
|
||||
add_global(decl)
|
||||
CORE.register_variable(varid, var)
|
||||
|
||||
for key, value in entries.items():
|
||||
cg.add(var.insert((key, value)))
|
||||
return var
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_MDNS
|
||||
#include "mdns_component.h"
|
||||
#include "esphome/core/version.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/version.h"
|
||||
#include "mdns_component.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
@@ -62,7 +62,11 @@ void MDNSComponent::compile_records_() {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"});
|
||||
if (api::global_api_server->get_noise_ctx()->has_psk()) {
|
||||
service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"});
|
||||
} else {
|
||||
service.txt_records.push_back({"api_encryption_supported", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
|
||||
@@ -61,6 +61,29 @@ void MicroWakeWord::dump_config() {
|
||||
void MicroWakeWord::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up microWakeWord...");
|
||||
|
||||
this->microphone_->add_data_callback([this](const std::vector<int16_t> &data) {
|
||||
if (this->state_ != State::DETECTING_WAKE_WORD) {
|
||||
return;
|
||||
}
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_;
|
||||
if (this->ring_buffer_.use_count() == 2) {
|
||||
// mWW still owns the ring buffer and temp_ring_buffer does as well, proceed to copy audio into ring buffer
|
||||
|
||||
size_t bytes_free = temp_ring_buffer->free();
|
||||
|
||||
if (bytes_free < data.size() * sizeof(int16_t)) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
"Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). "
|
||||
"Resetting the ring buffer. Wake word detection accuracy will be reduced.",
|
||||
bytes_free, data.size());
|
||||
|
||||
temp_ring_buffer->reset();
|
||||
}
|
||||
temp_ring_buffer->write((void *) data.data(), data.size() * sizeof(int16_t));
|
||||
}
|
||||
});
|
||||
|
||||
if (!this->register_streaming_ops_(this->streaming_op_resolver_)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -107,7 +130,6 @@ void MicroWakeWord::loop() {
|
||||
ESP_LOGD(TAG, "Starting Microphone");
|
||||
this->microphone_->start();
|
||||
this->set_state_(State::STARTING_MICROPHONE);
|
||||
this->high_freq_.start();
|
||||
break;
|
||||
case State::STARTING_MICROPHONE:
|
||||
if (this->microphone_->is_running()) {
|
||||
@@ -115,21 +137,19 @@ void MicroWakeWord::loop() {
|
||||
}
|
||||
break;
|
||||
case State::DETECTING_WAKE_WORD:
|
||||
while (!this->has_enough_samples_()) {
|
||||
this->read_microphone_();
|
||||
}
|
||||
this->update_model_probabilities_();
|
||||
if (this->detect_wake_words_()) {
|
||||
ESP_LOGD(TAG, "Wake Word '%s' Detected", (this->detected_wake_word_).c_str());
|
||||
this->detected_ = true;
|
||||
this->set_state_(State::STOP_MICROPHONE);
|
||||
while (this->has_enough_samples_()) {
|
||||
this->update_model_probabilities_();
|
||||
if (this->detect_wake_words_()) {
|
||||
ESP_LOGD(TAG, "Wake Word '%s' Detected", (this->detected_wake_word_).c_str());
|
||||
this->detected_ = true;
|
||||
this->set_state_(State::STOP_MICROPHONE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case State::STOP_MICROPHONE:
|
||||
ESP_LOGD(TAG, "Stopping Microphone");
|
||||
this->microphone_->stop();
|
||||
this->set_state_(State::STOPPING_MICROPHONE);
|
||||
this->high_freq_.stop();
|
||||
this->unload_models_();
|
||||
this->deallocate_buffers_();
|
||||
break;
|
||||
@@ -157,6 +177,11 @@ void MicroWakeWord::start() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->state_ != State::IDLE) {
|
||||
ESP_LOGW(TAG, "Wake word is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->load_models_() || !this->allocate_buffers_()) {
|
||||
ESP_LOGE(TAG, "Failed to load the wake word model(s) or allocate buffers");
|
||||
this->status_set_error();
|
||||
@@ -169,11 +194,6 @@ void MicroWakeWord::start() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->state_ != State::IDLE) {
|
||||
ESP_LOGW(TAG, "Wake word is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
this->reset_states_();
|
||||
this->set_state_(State::START_MICROPHONE);
|
||||
}
|
||||
@@ -196,26 +216,6 @@ void MicroWakeWord::set_state_(State state) {
|
||||
this->state_ = state;
|
||||
}
|
||||
|
||||
size_t MicroWakeWord::read_microphone_() {
|
||||
size_t bytes_read = this->microphone_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
|
||||
if (bytes_read == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t bytes_free = this->ring_buffer_->free();
|
||||
|
||||
if (bytes_free < bytes_read) {
|
||||
ESP_LOGW(TAG,
|
||||
"Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). "
|
||||
"Resetting the ring buffer. Wake word detection accuracy will be reduced.",
|
||||
bytes_free, bytes_read);
|
||||
|
||||
this->ring_buffer_->reset();
|
||||
}
|
||||
|
||||
return this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
|
||||
}
|
||||
|
||||
bool MicroWakeWord::allocate_buffers_() {
|
||||
ExternalRAMAllocator<int16_t> audio_samples_allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
|
||||
@@ -235,9 +235,9 @@ bool MicroWakeWord::allocate_buffers_() {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->ring_buffer_ == nullptr) {
|
||||
if (this->ring_buffer_.use_count() == 0) {
|
||||
this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t));
|
||||
if (this->ring_buffer_ == nullptr) {
|
||||
if (this->ring_buffer_.use_count() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate ring buffer");
|
||||
return false;
|
||||
}
|
||||
@@ -248,10 +248,17 @@ bool MicroWakeWord::allocate_buffers_() {
|
||||
|
||||
void MicroWakeWord::deallocate_buffers_() {
|
||||
ExternalRAMAllocator<int16_t> audio_samples_allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
audio_samples_allocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
|
||||
this->input_buffer_ = nullptr;
|
||||
audio_samples_allocator.deallocate(this->preprocessor_audio_buffer_, this->new_samples_to_get_());
|
||||
this->preprocessor_audio_buffer_ = nullptr;
|
||||
if (this->input_buffer_ != nullptr) {
|
||||
audio_samples_allocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
|
||||
this->input_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
if (this->preprocessor_audio_buffer_ != nullptr) {
|
||||
audio_samples_allocator.deallocate(this->preprocessor_audio_buffer_, this->new_samples_to_get_());
|
||||
this->preprocessor_audio_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
this->ring_buffer_.reset();
|
||||
}
|
||||
|
||||
bool MicroWakeWord::load_models_() {
|
||||
|
||||
@@ -62,9 +62,8 @@ class MicroWakeWord : public Component {
|
||||
microphone::Microphone *microphone_{nullptr};
|
||||
Trigger<std::string> *wake_word_detected_trigger_ = new Trigger<std::string>();
|
||||
State state_{State::IDLE};
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
std::unique_ptr<RingBuffer> ring_buffer_;
|
||||
std::shared_ptr<RingBuffer> ring_buffer_;
|
||||
|
||||
std::vector<WakeWordModel> wake_word_models_;
|
||||
|
||||
@@ -98,15 +97,6 @@ class MicroWakeWord : public Component {
|
||||
/// @return True if enough samples, false otherwise.
|
||||
bool has_enough_samples_();
|
||||
|
||||
/** Reads audio from microphone into the ring buffer
|
||||
*
|
||||
* Audio data (16000 kHz with int16 samples) is read into the input_buffer_.
|
||||
* Verifies the ring buffer has enough space for all audio data. If not, it logs
|
||||
* a warning and resets the ring buffer entirely.
|
||||
* @return Number of bytes written to the ring buffer
|
||||
*/
|
||||
size_t read_microphone_();
|
||||
|
||||
/// @brief Allocates memory for input_buffer_, preprocessor_audio_buffer_, and ring_buffer_
|
||||
/// @return True if successful, false otherwise
|
||||
bool allocate_buffers_();
|
||||
|
||||
@@ -138,7 +138,11 @@ void MQTTClientComponent::send_device_info_() {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
if (api::global_api_server->get_noise_ctx()->has_psk()) {
|
||||
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
} else {
|
||||
root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
}
|
||||
#endif
|
||||
},
|
||||
2, this->discovery_info_.retain);
|
||||
|
||||
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
esp8266_arduino=cv.Version(0, 0, 0),
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
bk72xx_libretiny=cv.Version(1, 7, 0),
|
||||
bk72xx_arduino=cv.Version(1, 7, 0),
|
||||
),
|
||||
cv.boolean_false,
|
||||
),
|
||||
|
||||
@@ -111,7 +111,7 @@ void OnlineImage::update() {
|
||||
case ImageFormat::BMP:
|
||||
accept_mime_type = "image/bmp";
|
||||
break;
|
||||
#endif // ONLINE_IMAGE_BMP_SUPPORT
|
||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
case ImageFormat::JPEG:
|
||||
accept_mime_type = "image/jpeg";
|
||||
@@ -121,7 +121,7 @@ void OnlineImage::update() {
|
||||
case ImageFormat::PNG:
|
||||
accept_mime_type = "image/png";
|
||||
break;
|
||||
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
default:
|
||||
accept_mime_type = "image/*";
|
||||
}
|
||||
@@ -159,7 +159,7 @@ void OnlineImage::update() {
|
||||
ESP_LOGD(TAG, "Allocating BMP decoder");
|
||||
this->decoder_ = make_unique<BmpDecoder>(this);
|
||||
}
|
||||
#endif // ONLINE_IMAGE_BMP_SUPPORT
|
||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
if (this->format_ == ImageFormat::JPEG) {
|
||||
ESP_LOGD(TAG, "Allocating JPEG decoder");
|
||||
@@ -171,7 +171,7 @@ void OnlineImage::update() {
|
||||
ESP_LOGD(TAG, "Allocating PNG decoder");
|
||||
this->decoder_ = make_unique<PngDecoder>(this);
|
||||
}
|
||||
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
if (!this->decoder_) {
|
||||
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
|
||||
@@ -185,7 +185,7 @@ void OnlineImage::update() {
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size);
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = ::time(nullptr);
|
||||
}
|
||||
|
||||
|
||||
1
esphome/components/pm2005/__init__.py
Normal file
1
esphome/components/pm2005/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""PM2005/2105 component for ESPHome."""
|
||||
123
esphome/components/pm2005/pm2005.cpp
Normal file
123
esphome/components/pm2005/pm2005.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "pm2005.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pm2005 {
|
||||
|
||||
static const char *const TAG = "pm2005";
|
||||
|
||||
// Converts a sensor situation to a human readable string
|
||||
static const LogString *pm2005_get_situation_string(int status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return LOG_STR("Close");
|
||||
case 2:
|
||||
return LOG_STR("Malfunction");
|
||||
case 3:
|
||||
return LOG_STR("Under detecting");
|
||||
case 0x80:
|
||||
return LOG_STR("Detecting completed");
|
||||
default:
|
||||
return LOG_STR("Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a sensor measuring mode to a human readable string
|
||||
static const LogString *pm2005_get_measuring_mode_string(int status) {
|
||||
switch (status) {
|
||||
case 2:
|
||||
return LOG_STR("Single");
|
||||
case 3:
|
||||
return LOG_STR("Continuous");
|
||||
case 5:
|
||||
return LOG_STR("Dynamic");
|
||||
default:
|
||||
return LOG_STR("Timing");
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint16_t get_sensor_value(const uint8_t *data, uint8_t i) { return data[i] * 0x100 + data[i + 1]; }
|
||||
|
||||
void PM2005Component::setup() {
|
||||
if (this->sensor_type_ == PM2005) {
|
||||
ESP_LOGCONFIG(TAG, "Setting up PM2005...");
|
||||
|
||||
this->situation_value_index_ = 3;
|
||||
this->pm_1_0_value_index_ = 4;
|
||||
this->pm_2_5_value_index_ = 6;
|
||||
this->pm_10_0_value_index_ = 8;
|
||||
this->measuring_value_index_ = 10;
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Setting up PM2105...");
|
||||
|
||||
this->situation_value_index_ = 2;
|
||||
this->pm_1_0_value_index_ = 3;
|
||||
this->pm_2_5_value_index_ = 5;
|
||||
this->pm_10_0_value_index_ = 7;
|
||||
this->measuring_value_index_ = 9;
|
||||
}
|
||||
|
||||
if (this->read(this->data_buffer_, 12) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Communication failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void PM2005Component::update() {
|
||||
if (this->read(this->data_buffer_, 12) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Read result failed.");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->sensor_situation_ == this->data_buffer_[this->situation_value_index_]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->sensor_situation_ = this->data_buffer_[this->situation_value_index_];
|
||||
ESP_LOGD(TAG, "Sensor situation: %s.", LOG_STR_ARG(pm2005_get_situation_string(this->sensor_situation_)));
|
||||
if (this->sensor_situation_ == 2) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (this->sensor_situation_ != 0x80) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t pm1 = get_sensor_value(this->data_buffer_, this->pm_1_0_value_index_);
|
||||
uint16_t pm25 = get_sensor_value(this->data_buffer_, this->pm_2_5_value_index_);
|
||||
uint16_t pm10 = get_sensor_value(this->data_buffer_, this->pm_10_0_value_index_);
|
||||
uint16_t sensor_measuring_mode = get_sensor_value(this->data_buffer_, this->measuring_value_index_);
|
||||
ESP_LOGD(TAG, "PM1.0: %d, PM2.5: %d, PM10: %d, Measuring mode: %s.", pm1, pm25, pm10,
|
||||
LOG_STR_ARG(pm2005_get_measuring_mode_string(sensor_measuring_mode)));
|
||||
|
||||
if (this->pm_1_0_sensor_ != nullptr) {
|
||||
this->pm_1_0_sensor_->publish_state(pm1);
|
||||
}
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
this->pm_2_5_sensor_->publish_state(pm25);
|
||||
}
|
||||
if (this->pm_10_0_sensor_ != nullptr) {
|
||||
this->pm_10_0_sensor_->publish_state(pm10);
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void PM2005Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PM2005:");
|
||||
ESP_LOGCONFIG(TAG, " Type: PM2%u05", this->sensor_type_ == PM2105);
|
||||
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with PM2%u05 failed!", this->sensor_type_ == PM2105);
|
||||
}
|
||||
|
||||
LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
|
||||
LOG_SENSOR(" ", "PM10 ", this->pm_10_0_sensor_);
|
||||
}
|
||||
|
||||
} // namespace pm2005
|
||||
} // namespace esphome
|
||||
46
esphome/components/pm2005/pm2005.h
Normal file
46
esphome/components/pm2005/pm2005.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pm2005 {
|
||||
|
||||
enum SensorType {
|
||||
PM2005,
|
||||
PM2105,
|
||||
};
|
||||
|
||||
class PM2005Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
|
||||
|
||||
void set_sensor_type(SensorType sensor_type) { this->sensor_type_ = sensor_type; }
|
||||
|
||||
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { this->pm_1_0_sensor_ = pm_1_0_sensor; }
|
||||
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { this->pm_2_5_sensor_ = pm_2_5_sensor; }
|
||||
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { this->pm_10_0_sensor_ = pm_10_0_sensor; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
uint8_t sensor_situation_{0};
|
||||
uint8_t data_buffer_[12];
|
||||
SensorType sensor_type_{PM2005};
|
||||
|
||||
sensor::Sensor *pm_1_0_sensor_{nullptr};
|
||||
sensor::Sensor *pm_2_5_sensor_{nullptr};
|
||||
sensor::Sensor *pm_10_0_sensor_{nullptr};
|
||||
|
||||
uint8_t situation_value_index_{3};
|
||||
uint8_t pm_1_0_value_index_{4};
|
||||
uint8_t pm_2_5_value_index_{6};
|
||||
uint8_t pm_10_0_value_index_{8};
|
||||
uint8_t measuring_value_index_{10};
|
||||
};
|
||||
|
||||
} // namespace pm2005
|
||||
} // namespace esphome
|
||||
86
esphome/components/pm2005/sensor.py
Normal file
86
esphome/components/pm2005/sensor.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""PM2005/2105 Sensor component for ESPHome."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PM_1_0,
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_10_0,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
ICON_CHEMICAL_WEAPON,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
CODEOWNERS = ["@andrewjswan"]
|
||||
|
||||
pm2005_ns = cg.esphome_ns.namespace("pm2005")
|
||||
PM2005Component = pm2005_ns.class_(
|
||||
"PM2005Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
SensorType = pm2005_ns.enum("SensorType")
|
||||
SENSOR_TYPE = {
|
||||
"PM2005": SensorType.PM2005,
|
||||
"PM2105": SensorType.PM2105,
|
||||
}
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PM2005Component),
|
||||
cv.Optional(CONF_TYPE, default="PM2005"): cv.enum(SENSOR_TYPE, upper=True),
|
||||
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
},
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x28)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config) -> None:
|
||||
"""Code generation entry point."""
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_sensor_type(config[CONF_TYPE]))
|
||||
|
||||
if pm_1_0_config := config.get(CONF_PM_1_0):
|
||||
sens = await sensor.new_sensor(pm_1_0_config)
|
||||
cg.add(var.set_pm_1_0_sensor(sens))
|
||||
|
||||
if pm_2_5_config := config.get(CONF_PM_2_5):
|
||||
sens = await sensor.new_sensor(pm_2_5_config)
|
||||
cg.add(var.set_pm_2_5_sensor(sens))
|
||||
|
||||
if pm_10_0_config := config.get(CONF_PM_10_0):
|
||||
sens = await sensor.new_sensor(pm_10_0_config)
|
||||
cg.add(var.set_pm_10_0_sensor(sens))
|
||||
@@ -89,6 +89,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
|
||||
this->valve_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
this->climate_type_(stream);
|
||||
for (auto *obj : App.get_climates())
|
||||
this->climate_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
req->send(stream);
|
||||
}
|
||||
|
||||
@@ -824,6 +830,174 @@ void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *ob
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
void PrometheusHandler::climate_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_climate_setting gauge\n"));
|
||||
stream->print(F("#TYPE esphome_climate_value gauge\n"));
|
||||
stream->print(F("#TYPE esphome_climate_failed gauge\n"));
|
||||
}
|
||||
|
||||
void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name, std::string &setting,
|
||||
const LogString *setting_value) {
|
||||
stream->print(F("esphome_climate_setting{id=\""));
|
||||
stream->print(relabel_id_(obj).c_str());
|
||||
add_area_label_(stream, area);
|
||||
add_node_label_(stream, node);
|
||||
add_friendly_name_label_(stream, friendly_name);
|
||||
stream->print(F("\",name=\""));
|
||||
stream->print(relabel_name_(obj).c_str());
|
||||
stream->print(F("\",category=\""));
|
||||
stream->print(setting.c_str());
|
||||
stream->print(F("\",setting_value=\""));
|
||||
stream->print(LOG_STR_ARG(setting_value));
|
||||
stream->print(F("\"} "));
|
||||
stream->print(F("1.0"));
|
||||
stream->print(F("\n"));
|
||||
}
|
||||
|
||||
void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name, std::string &category,
|
||||
std::string &climate_value) {
|
||||
stream->print(F("esphome_climate_value{id=\""));
|
||||
stream->print(relabel_id_(obj).c_str());
|
||||
add_area_label_(stream, area);
|
||||
add_node_label_(stream, node);
|
||||
add_friendly_name_label_(stream, friendly_name);
|
||||
stream->print(F("\",name=\""));
|
||||
stream->print(relabel_name_(obj).c_str());
|
||||
stream->print(F("\",category=\""));
|
||||
stream->print(category.c_str());
|
||||
stream->print(F("\"} "));
|
||||
stream->print(climate_value.c_str());
|
||||
stream->print(F("\n"));
|
||||
}
|
||||
|
||||
void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name, std::string &category,
|
||||
bool is_failed_value) {
|
||||
stream->print(F("esphome_climate_failed{id=\""));
|
||||
stream->print(relabel_id_(obj).c_str());
|
||||
add_area_label_(stream, area);
|
||||
add_node_label_(stream, node);
|
||||
add_friendly_name_label_(stream, friendly_name);
|
||||
stream->print(F("\",name=\""));
|
||||
stream->print(relabel_name_(obj).c_str());
|
||||
stream->print(F("\",category=\""));
|
||||
stream->print(category.c_str());
|
||||
stream->print(F("\"} "));
|
||||
if (is_failed_value) {
|
||||
stream->print(F("1.0"));
|
||||
} else {
|
||||
stream->print(F("0.0"));
|
||||
}
|
||||
stream->print(F("\n"));
|
||||
}
|
||||
|
||||
void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
// Data itself
|
||||
bool any_failures = false;
|
||||
std::string climate_mode_category = "mode";
|
||||
const auto *climate_mode_value = climate::climate_mode_to_string(obj->mode);
|
||||
climate_setting_row_(stream, obj, area, node, friendly_name, climate_mode_category, climate_mode_value);
|
||||
const auto traits = obj->get_traits();
|
||||
// Now see if traits is supported
|
||||
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
|
||||
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
|
||||
// max temp
|
||||
std::string max_temp = "maximum_temperature";
|
||||
auto max_temp_value = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, max_temp, max_temp_value);
|
||||
// max temp
|
||||
std::string min_temp = "mininum_temperature";
|
||||
auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value);
|
||||
// now check optional traits
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
std::string current_temp = "current_temperature";
|
||||
if (std::isnan(obj->current_temperature)) {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true);
|
||||
any_failures = true;
|
||||
} else {
|
||||
auto current_temp_value = value_accuracy_to_string(obj->current_temperature, current_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, current_temp, current_temp_value);
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_current_humidity()) {
|
||||
std::string current_humidity = "current_humidity";
|
||||
if (std::isnan(obj->current_humidity)) {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true);
|
||||
any_failures = true;
|
||||
} else {
|
||||
auto current_humidity_value = value_accuracy_to_string(obj->current_humidity, 0);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, current_humidity, current_humidity_value);
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_target_humidity()) {
|
||||
std::string target_humidity = "target_humidity";
|
||||
if (std::isnan(obj->target_humidity)) {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true);
|
||||
any_failures = true;
|
||||
} else {
|
||||
auto target_humidity_value = value_accuracy_to_string(obj->target_humidity, 0);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, target_humidity, target_humidity_value);
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
std::string target_temp_low = "target_temperature_low";
|
||||
auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value);
|
||||
std::string target_temp_high = "target_temperature_high";
|
||||
auto target_temp_high_value = value_accuracy_to_string(obj->target_temperature_high, target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, target_temp_high, target_temp_high_value);
|
||||
} else {
|
||||
std::string target_temp = "target_temperature";
|
||||
auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value);
|
||||
}
|
||||
if (traits.get_supports_action()) {
|
||||
std::string climate_trait_category = "action";
|
||||
const auto *climate_trait_value = climate::climate_action_to_string(obj->action);
|
||||
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);
|
||||
}
|
||||
if (traits.get_supports_fan_modes()) {
|
||||
std::string climate_trait_category = "fan_mode";
|
||||
if (obj->fan_mode.has_value()) {
|
||||
const auto *climate_trait_value = climate::climate_fan_mode_to_string(obj->fan_mode.value());
|
||||
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, false);
|
||||
} else {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, true);
|
||||
any_failures = true;
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_presets()) {
|
||||
std::string climate_trait_category = "preset";
|
||||
if (obj->preset.has_value()) {
|
||||
const auto *climate_trait_value = climate::climate_preset_to_string(obj->preset.value());
|
||||
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, false);
|
||||
} else {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, true);
|
||||
any_failures = true;
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
std::string climate_trait_category = "swing_mode";
|
||||
const auto *climate_trait_value = climate::climate_swing_mode_to_string(obj->swing_mode);
|
||||
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);
|
||||
}
|
||||
std::string all_climate_category = "all";
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, all_climate_category, any_failures);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace prometheus
|
||||
} // namespace esphome
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/controller.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#ifdef USE_CLIMATE
|
||||
#include "esphome/core/log.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace prometheus {
|
||||
@@ -169,6 +172,20 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
/// Return the type for prometheus
|
||||
void climate_type_(AsyncResponseStream *stream);
|
||||
/// Return the climate state as prometheus data point
|
||||
void climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
void climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name, std::string &category, bool is_failed_value);
|
||||
void climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name, std::string &setting, const LogString *setting_value);
|
||||
void climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name, std::string &category, std::string &climate_value);
|
||||
#endif
|
||||
|
||||
web_server_base::WebServerBase *base_;
|
||||
bool include_internal_{false};
|
||||
std::map<EntityBase *, std::string> relabel_map_id_;
|
||||
|
||||
@@ -16,6 +16,8 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_SPEED,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
@@ -110,11 +112,11 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(f"{SPIRAM_MODES[config[CONF_MODE]]}", True)
|
||||
add_idf_sdkconfig_option(f"{SPIRAM_SPEEDS[config[CONF_SPEED]]}", True)
|
||||
if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240", True)
|
||||
# This works only on IDF 5.4.x but does no harm on earlier versions
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True
|
||||
)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240", True)
|
||||
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True
|
||||
)
|
||||
if config[CONF_ENABLE_ECC]:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "psram.h"
|
||||
#ifdef USE_ESP_IDF
|
||||
#include <esp_idf_version.h>
|
||||
#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5
|
||||
#include <esp_psram.h>
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
@@ -15,7 +16,7 @@ static const char *const TAG = "psram";
|
||||
|
||||
void PsramComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PSRAM:");
|
||||
#ifdef USE_ESP_IDF
|
||||
#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5
|
||||
bool available = esp_psram_is_initialized();
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.const import CONF_DRAW_ROUNDING
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
@@ -24,7 +25,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
|
||||
from . import CONF_DRAW_FROM_ORIGIN, CONF_DRAW_ROUNDING
|
||||
from . import CONF_DRAW_FROM_ORIGIN
|
||||
from .models import DriverChip
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# Commands
|
||||
from esphome.components.const import CONF_DRAW_ROUNDING
|
||||
from esphome.const import CONF_INVERT_COLORS, CONF_SWAP_XY
|
||||
|
||||
from . import CONF_DRAW_ROUNDING
|
||||
|
||||
SW_RESET_CMD = 0x01
|
||||
SLEEP_IN = 0x10
|
||||
SLEEP_OUT = 0x11
|
||||
|
||||
@@ -208,7 +208,6 @@ void RemoteReceiverComponent::loop() {
|
||||
this->store_.buffer_read = next_read;
|
||||
|
||||
if (!this->temp_.empty()) {
|
||||
this->temp_.push_back(-this->idle_us_);
|
||||
this->call_listeners_dumpers_();
|
||||
}
|
||||
}
|
||||
@@ -219,11 +218,9 @@ void RemoteReceiverComponent::loop() {
|
||||
this->decode_rmt_(item, len / sizeof(rmt_item32_t));
|
||||
vRingbufferReturnItem(this->ringbuf_, item);
|
||||
|
||||
if (this->temp_.empty())
|
||||
return;
|
||||
|
||||
this->temp_.push_back(-this->idle_us_);
|
||||
this->call_listeners_dumpers_();
|
||||
if (!this->temp_.empty()) {
|
||||
this->call_listeners_dumpers_();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -234,6 +231,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c
|
||||
void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) {
|
||||
#endif
|
||||
bool prev_level = false;
|
||||
bool idle_level = false;
|
||||
uint32_t prev_length = 0;
|
||||
this->temp_.clear();
|
||||
int32_t multiplier = this->pin_->is_inverted() ? -1 : 1;
|
||||
@@ -266,7 +264,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count)
|
||||
} else if ((bool(item[i].level0) == prev_level) || (item[i].duration0 < filter_ticks)) {
|
||||
prev_length += item[i].duration0;
|
||||
} else {
|
||||
if (prev_length > 0) {
|
||||
if (prev_length >= filter_ticks) {
|
||||
if (prev_level) {
|
||||
this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier);
|
||||
} else {
|
||||
@@ -276,6 +274,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count)
|
||||
prev_level = bool(item[i].level0);
|
||||
prev_length = item[i].duration0;
|
||||
}
|
||||
idle_level = !bool(item[i].level0);
|
||||
|
||||
if (item[i].duration1 == 0u) {
|
||||
// EOF, sometimes garbage follows, break early
|
||||
@@ -283,7 +282,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count)
|
||||
} else if ((bool(item[i].level1) == prev_level) || (item[i].duration1 < filter_ticks)) {
|
||||
prev_length += item[i].duration1;
|
||||
} else {
|
||||
if (prev_length > 0) {
|
||||
if (prev_length >= filter_ticks) {
|
||||
if (prev_level) {
|
||||
this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier);
|
||||
} else {
|
||||
@@ -293,14 +292,22 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count)
|
||||
prev_level = bool(item[i].level1);
|
||||
prev_length = item[i].duration1;
|
||||
}
|
||||
idle_level = !bool(item[i].level1);
|
||||
}
|
||||
if (prev_length > 0) {
|
||||
if (prev_length >= filter_ticks && prev_level != idle_level) {
|
||||
if (prev_level) {
|
||||
this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier);
|
||||
} else {
|
||||
this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier);
|
||||
}
|
||||
}
|
||||
if (!this->temp_.empty()) {
|
||||
if (idle_level) {
|
||||
this->temp_.push_back(this->idle_us_ * multiplier);
|
||||
} else {
|
||||
this->temp_.push_back(-int32_t(this->idle_us_) * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace remote_receiver
|
||||
|
||||
@@ -18,6 +18,8 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION,
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -33,10 +35,7 @@ ForceRecalibrationWithReference = scd30_ns.class_(
|
||||
"ForceRecalibrationWithReference", automation.Action
|
||||
)
|
||||
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
|
||||
CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
|
||||
@@ -20,6 +20,10 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION,
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION,
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE,
|
||||
CONF_MEASUREMENT_MODE,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@sjtrny", "@martgras"]
|
||||
@@ -47,11 +51,6 @@ FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action)
|
||||
|
||||
|
||||
CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source"
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
|
||||
CONF_MEASUREMENT_MODE = "measurement_mode"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_HECTOPASCAL,
|
||||
CONF_MEASUREMENT_MODE,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -22,7 +23,7 @@ MEASUREMENT_MODE = {
|
||||
"mass_flow": MeasurementMode.MASS_FLOW_AVG,
|
||||
"differential_pressure": MeasurementMode.DP_AVG,
|
||||
}
|
||||
CONF_MEASUREMENT_MODE = "measurement_mode"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
|
||||
@@ -12,14 +12,22 @@ void SHT4XComponent::start_heater_() {
|
||||
uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]};
|
||||
|
||||
ESP_LOGD(TAG, "Heater turning on");
|
||||
this->write(cmd, 1);
|
||||
if (this->write(cmd, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_error("Failed to turn on heater");
|
||||
}
|
||||
}
|
||||
|
||||
void SHT4XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up sht4x...");
|
||||
|
||||
if (this->duty_cycle_ > 0.0) {
|
||||
uint32_t heater_interval = (uint32_t) (this->heater_time_ / this->duty_cycle_);
|
||||
auto err = this->write(nullptr, 0);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) {
|
||||
uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
|
||||
ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval);
|
||||
|
||||
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
|
||||
@@ -47,37 +55,50 @@ void SHT4XComponent::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); }
|
||||
void SHT4XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SHT4x:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with SHT4x failed!");
|
||||
}
|
||||
}
|
||||
|
||||
void SHT4XComponent::update() {
|
||||
// Send command
|
||||
this->write_command(MEASURECOMMANDS[this->precision_]);
|
||||
if (!this->write_command(MEASURECOMMANDS[this->precision_])) {
|
||||
// Warning will be printed only if warning status is not set yet
|
||||
this->status_set_warning("Failed to send measurement command");
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(10, [this]() {
|
||||
uint16_t buffer[2];
|
||||
|
||||
// Read measurement
|
||||
bool read_status = this->read_data(buffer, 2);
|
||||
if (!this->read_data(buffer, 2)) {
|
||||
// Using ESP_LOGW to force the warning to be printed
|
||||
ESP_LOGW(TAG, "Sensor read failed");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (read_status) {
|
||||
// Evaluate and publish measurements
|
||||
if (this->temp_sensor_ != nullptr) {
|
||||
// Temp is contained in the first result word
|
||||
float sensor_value_temp = buffer[0];
|
||||
float temp = -45 + 175 * sensor_value_temp / 65535;
|
||||
this->status_clear_warning();
|
||||
|
||||
this->temp_sensor_->publish_state(temp);
|
||||
}
|
||||
// Evaluate and publish measurements
|
||||
if (this->temp_sensor_ != nullptr) {
|
||||
// Temp is contained in the first result word
|
||||
float sensor_value_temp = buffer[0];
|
||||
float temp = -45 + 175 * sensor_value_temp / 65535;
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
// Relative humidity is in the second result word
|
||||
float sensor_value_rh = buffer[1];
|
||||
float rh = -6 + 125 * sensor_value_rh / 65535;
|
||||
this->temp_sensor_->publish_state(temp);
|
||||
}
|
||||
|
||||
this->humidity_sensor_->publish_state(rh);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Sensor read failed");
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
// Relative humidity is in the second result word
|
||||
float sensor_value_rh = buffer[1];
|
||||
float rh = -6 + 125 * sensor_value_rh / 65535;
|
||||
|
||||
this->humidity_sensor_->publish_state(rh);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ enum SHT4XPRECISION { SHT4X_PRECISION_HIGH = 0, SHT4X_PRECISION_MED, SHT4X_PRECI
|
||||
|
||||
enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEATERPOWER_LOW };
|
||||
|
||||
enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 };
|
||||
enum SHT4XHEATERTIME : uint16_t { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 };
|
||||
|
||||
class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
|
||||
@@ -1,19 +1,59 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER
|
||||
from esphome.const import (
|
||||
CONF_FORMAT,
|
||||
CONF_HOURS,
|
||||
CONF_ID,
|
||||
CONF_MINUTES,
|
||||
CONF_SECONDS,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_TIMER,
|
||||
)
|
||||
|
||||
uptime_ns = cg.esphome_ns.namespace("uptime")
|
||||
UptimeTextSensor = uptime_ns.class_(
|
||||
"UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent
|
||||
)
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(
|
||||
UptimeTextSensor,
|
||||
icon=ICON_TIMER,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
).extend(cv.polling_component_schema("30s"))
|
||||
|
||||
CONF_SEPARATOR = "separator"
|
||||
CONF_DAYS = "days"
|
||||
CONF_EXPAND = "expand"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
text_sensor.text_sensor_schema(
|
||||
UptimeTextSensor,
|
||||
icon=ICON_TIMER,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_FORMAT, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_DAYS, default="d"): cv.string_strict,
|
||||
cv.Optional(CONF_HOURS, default="h"): cv.string_strict,
|
||||
cv.Optional(CONF_MINUTES, default="m"): cv.string_strict,
|
||||
cv.Optional(CONF_SECONDS, default="s"): cv.string_strict,
|
||||
cv.Optional(CONF_SEPARATOR, default=""): cv.string_strict,
|
||||
cv.Optional(CONF_EXPAND, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("30s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
format = config[CONF_FORMAT]
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
format[CONF_DAYS],
|
||||
format[CONF_HOURS],
|
||||
format[CONF_MINUTES],
|
||||
format[CONF_SECONDS],
|
||||
format[CONF_SEPARATOR],
|
||||
format[CONF_EXPAND],
|
||||
)
|
||||
await text_sensor.register_text_sensor(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -16,6 +16,11 @@ void UptimeTextSensor::setup() {
|
||||
this->update();
|
||||
}
|
||||
|
||||
void UptimeTextSensor::insert_buffer_(std::string &buffer, const char *key, unsigned value) const {
|
||||
buffer.insert(0, this->separator_);
|
||||
buffer.insert(0, str_sprintf("%u%s", value, key));
|
||||
}
|
||||
|
||||
void UptimeTextSensor::update() {
|
||||
auto now = millis();
|
||||
// get whole seconds since last update. Note that even if the millis count has overflowed between updates,
|
||||
@@ -32,25 +37,25 @@ void UptimeTextSensor::update() {
|
||||
unsigned remainder = uptime % 60;
|
||||
uptime /= 60;
|
||||
if (interval < 30) {
|
||||
buffer.insert(0, str_sprintf("%us", remainder));
|
||||
if (uptime == 0)
|
||||
this->insert_buffer_(buffer, this->seconds_text_, remainder);
|
||||
if (!this->expand_ && uptime == 0)
|
||||
break;
|
||||
}
|
||||
remainder = uptime % 60;
|
||||
uptime /= 60;
|
||||
if (interval < 1800) {
|
||||
buffer.insert(0, str_sprintf("%um", remainder));
|
||||
if (uptime == 0)
|
||||
this->insert_buffer_(buffer, this->minutes_text_, remainder);
|
||||
if (!this->expand_ && uptime == 0)
|
||||
break;
|
||||
}
|
||||
remainder = uptime % 24;
|
||||
uptime /= 24;
|
||||
if (interval < 12 * 3600) {
|
||||
buffer.insert(0, str_sprintf("%uh", remainder));
|
||||
if (uptime == 0)
|
||||
this->insert_buffer_(buffer, this->hours_text_, remainder);
|
||||
if (!this->expand_ && uptime == 0)
|
||||
break;
|
||||
}
|
||||
buffer.insert(0, str_sprintf("%ud", (unsigned) uptime));
|
||||
this->insert_buffer_(buffer, this->days_text_, (unsigned) uptime);
|
||||
break;
|
||||
}
|
||||
this->publish_state(buffer);
|
||||
|
||||
@@ -10,13 +10,32 @@ namespace uptime {
|
||||
|
||||
class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
public:
|
||||
UptimeTextSensor(const char *days_text, const char *hours_text, const char *minutes_text, const char *seconds_text,
|
||||
const char *separator, bool expand)
|
||||
: days_text_(days_text),
|
||||
hours_text_(hours_text),
|
||||
minutes_text_(minutes_text),
|
||||
seconds_text_(seconds_text),
|
||||
separator_(separator),
|
||||
expand_(expand) {}
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
void set_days(const char *days_text) { this->days_text_ = days_text; }
|
||||
void set_hours(const char *hours_text) { this->hours_text_ = hours_text; }
|
||||
void set_minutes(const char *minutes_text) { this->minutes_text_ = minutes_text; }
|
||||
void set_seconds(const char *seconds_text) { this->seconds_text_ = seconds_text; }
|
||||
|
||||
protected:
|
||||
void insert_buffer_(std::string &buffer, const char *key, unsigned value) const;
|
||||
const char *days_text_;
|
||||
const char *hours_text_;
|
||||
const char *minutes_text_;
|
||||
const char *seconds_text_;
|
||||
const char *separator_;
|
||||
bool expand_{};
|
||||
uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years
|
||||
uint32_t last_ms_{0};
|
||||
};
|
||||
|
||||
@@ -94,8 +94,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
media_player.MediaPlayer
|
||||
),
|
||||
cv.Optional(CONF_USE_WAKE_WORD, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VAD_THRESHOLD): cv.All(
|
||||
cv.requires_component("esp_adf"), cv.only_with_esp_idf, cv.uint8_t
|
||||
cv.Optional(CONF_VAD_THRESHOLD): cv.invalid(
|
||||
"VAD threshold is no longer supported, as it requires the deprecated esp_adf external component. Use an i2s_audio microphone/speaker instead. Additionally, you may need to configure the audio_adc and audio_dac components depending on your hardware."
|
||||
),
|
||||
cv.Optional(CONF_NOISE_SUPPRESSION_LEVEL, default=0): cv.int_range(0, 4),
|
||||
cv.Optional(CONF_AUTO_GAIN, default="0dBFS"): cv.All(
|
||||
|
||||
@@ -18,14 +18,25 @@ static const char *const TAG = "voice_assistant";
|
||||
#endif
|
||||
|
||||
static const size_t SAMPLE_RATE_HZ = 16000;
|
||||
static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
|
||||
static const size_t BUFFER_SIZE = 512 * SAMPLE_RATE_HZ / 1000;
|
||||
static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t);
|
||||
|
||||
static const size_t RING_BUFFER_SAMPLES = 512 * SAMPLE_RATE_HZ / 1000; // 512 ms * 16 kHz/ 1000 ms
|
||||
static const size_t RING_BUFFER_SIZE = RING_BUFFER_SAMPLES * sizeof(int16_t);
|
||||
static const size_t SEND_BUFFER_SAMPLES = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
|
||||
static const size_t SEND_BUFFER_SIZE = SEND_BUFFER_SAMPLES * sizeof(int16_t);
|
||||
static const size_t RECEIVE_SIZE = 1024;
|
||||
static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE;
|
||||
|
||||
VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; }
|
||||
|
||||
void VoiceAssistant::setup() {
|
||||
this->mic_->add_data_callback([this](const std::vector<int16_t> &data) {
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_;
|
||||
if (this->ring_buffer_.use_count() > 1) {
|
||||
temp_ring_buffer->write((void *) data.data(), data.size() * sizeof(int16_t));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
bool VoiceAssistant::start_udp_socket_() {
|
||||
@@ -72,12 +83,8 @@ bool VoiceAssistant::start_udp_socket_() {
|
||||
}
|
||||
|
||||
bool VoiceAssistant::allocate_buffers_() {
|
||||
if (this->send_buffer_ != nullptr) {
|
||||
return true; // Already allocated
|
||||
}
|
||||
|
||||
#ifdef USE_SPEAKER
|
||||
if (this->speaker_ != nullptr) {
|
||||
if ((this->speaker_ != nullptr) && (this->speaker_buffer_ == nullptr)) {
|
||||
ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
|
||||
if (this->speaker_buffer_ == nullptr) {
|
||||
@@ -87,28 +94,21 @@ bool VoiceAssistant::allocate_buffers_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE);
|
||||
if (this->input_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate input buffer");
|
||||
return false;
|
||||
if (this->ring_buffer_.use_count() == 0) {
|
||||
this->ring_buffer_ = RingBuffer::create(RING_BUFFER_SIZE);
|
||||
if (this->ring_buffer_.use_count() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate ring buffer");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_ESP_ADF
|
||||
this->vad_instance_ = vad_create(VAD_MODE_4);
|
||||
#endif
|
||||
|
||||
this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t));
|
||||
if (this->ring_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate ring buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
|
||||
if (send_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate send buffer");
|
||||
return false;
|
||||
if (this->send_buffer_ == nullptr) {
|
||||
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
|
||||
if (send_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate send buffer");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -119,10 +119,6 @@ void VoiceAssistant::clear_buffers_() {
|
||||
memset(this->send_buffer_, 0, SEND_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
if (this->input_buffer_ != nullptr) {
|
||||
memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t));
|
||||
}
|
||||
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
this->ring_buffer_->reset();
|
||||
}
|
||||
@@ -139,26 +135,16 @@ void VoiceAssistant::clear_buffers_() {
|
||||
}
|
||||
|
||||
void VoiceAssistant::deallocate_buffers_() {
|
||||
ExternalRAMAllocator<uint8_t> send_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE);
|
||||
this->send_buffer_ = nullptr;
|
||||
if (this->send_buffer_ != nullptr) {
|
||||
ExternalRAMAllocator<uint8_t> send_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE);
|
||||
this->send_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
this->ring_buffer_.reset();
|
||||
this->ring_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP_ADF
|
||||
if (this->vad_instance_ != nullptr) {
|
||||
vad_destroy(this->vad_instance_);
|
||||
this->vad_instance_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
ExternalRAMAllocator<int16_t> input_deallocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
input_deallocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE);
|
||||
this->input_buffer_ = nullptr;
|
||||
|
||||
#ifdef USE_SPEAKER
|
||||
if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
|
||||
ExternalRAMAllocator<uint8_t> speaker_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
@@ -173,22 +159,6 @@ void VoiceAssistant::reset_conversation_id() {
|
||||
ESP_LOGD(TAG, "reset conversation ID");
|
||||
}
|
||||
|
||||
int VoiceAssistant::read_microphone_() {
|
||||
size_t bytes_read = 0;
|
||||
if (this->mic_->is_running()) { // Read audio into input buffer
|
||||
bytes_read = this->mic_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
|
||||
if (bytes_read == 0) {
|
||||
memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t));
|
||||
return 0;
|
||||
}
|
||||
// Write audio into ring buffer
|
||||
this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "microphone not running");
|
||||
}
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
void VoiceAssistant::loop() {
|
||||
if (this->api_client_ == nullptr && this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE &&
|
||||
this->state_ != State::STOPPING_MICROPHONE) {
|
||||
@@ -206,16 +176,9 @@ void VoiceAssistant::loop() {
|
||||
case State::IDLE: {
|
||||
if (this->continuous_ && this->desired_state_ == State::IDLE) {
|
||||
this->idle_trigger_->trigger();
|
||||
#ifdef USE_ESP_ADF
|
||||
if (this->use_wake_word_) {
|
||||
this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->set_state_(State::START_MICROPHONE, State::START_PIPELINE);
|
||||
}
|
||||
this->set_state_(State::START_MICROPHONE, State::START_PIPELINE);
|
||||
} else {
|
||||
this->high_freq_.stop();
|
||||
this->deallocate_buffers_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -231,7 +194,6 @@ void VoiceAssistant::loop() {
|
||||
this->clear_buffers_();
|
||||
|
||||
this->mic_->start();
|
||||
this->high_freq_.start();
|
||||
this->set_state_(State::STARTING_MICROPHONE);
|
||||
break;
|
||||
}
|
||||
@@ -241,42 +203,10 @@ void VoiceAssistant::loop() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
#ifdef USE_ESP_ADF
|
||||
case State::WAIT_FOR_VAD: {
|
||||
this->read_microphone_();
|
||||
ESP_LOGD(TAG, "Waiting for speech...");
|
||||
this->set_state_(State::WAITING_FOR_VAD);
|
||||
break;
|
||||
}
|
||||
case State::WAITING_FOR_VAD: {
|
||||
size_t bytes_read = this->read_microphone_();
|
||||
if (bytes_read > 0) {
|
||||
vad_state_t vad_state =
|
||||
vad_process(this->vad_instance_, this->input_buffer_, SAMPLE_RATE_HZ, VAD_FRAME_LENGTH_MS);
|
||||
if (vad_state == VAD_SPEECH) {
|
||||
if (this->vad_counter_ < this->vad_threshold_) {
|
||||
this->vad_counter_++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "VAD detected speech");
|
||||
this->set_state_(State::START_PIPELINE, State::STREAMING_MICROPHONE);
|
||||
|
||||
// Reset for next time
|
||||
this->vad_counter_ = 0;
|
||||
}
|
||||
} else {
|
||||
if (this->vad_counter_ > 0) {
|
||||
this->vad_counter_--;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case State::START_PIPELINE: {
|
||||
this->read_microphone_();
|
||||
ESP_LOGD(TAG, "Requesting start...");
|
||||
uint32_t flags = 0;
|
||||
if (this->use_wake_word_)
|
||||
if (!this->continue_conversation_ && this->use_wake_word_)
|
||||
flags |= api::enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD;
|
||||
if (this->silence_detection_)
|
||||
flags |= api::enums::VOICE_ASSISTANT_REQUEST_USE_VAD;
|
||||
@@ -306,11 +236,9 @@ void VoiceAssistant::loop() {
|
||||
break;
|
||||
}
|
||||
case State::STARTING_PIPELINE: {
|
||||
this->read_microphone_();
|
||||
break; // State changed when udp server port received
|
||||
}
|
||||
case State::STREAMING_MICROPHONE: {
|
||||
this->read_microphone_();
|
||||
size_t available = this->ring_buffer_->available();
|
||||
while (available >= SEND_BUFFER_SIZE) {
|
||||
size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0);
|
||||
@@ -387,6 +315,25 @@ void VoiceAssistant::loop() {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING);
|
||||
|
||||
if (playing && this->media_player_wait_for_announcement_start_) {
|
||||
// Announcement has started playing, wait for it to finish
|
||||
this->media_player_wait_for_announcement_start_ = false;
|
||||
this->media_player_wait_for_announcement_end_ = true;
|
||||
}
|
||||
|
||||
if (!playing && this->media_player_wait_for_announcement_end_) {
|
||||
// Announcement has finished playing
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
this->cancel_timeout("playing");
|
||||
ESP_LOGD(TAG, "Announcement finished playing");
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
|
||||
api::VoiceAssistantAnnounceFinished msg;
|
||||
msg.success = true;
|
||||
this->api_client_->send_voice_assistant_announce_finished(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (playing) {
|
||||
@@ -417,7 +364,11 @@ void VoiceAssistant::loop() {
|
||||
this->tts_stream_end_trigger_->trigger();
|
||||
}
|
||||
#endif
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
if (this->continue_conversation_) {
|
||||
this->set_state_(State::START_MICROPHONE, State::START_PIPELINE);
|
||||
} else {
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -574,19 +525,14 @@ void VoiceAssistant::request_start(bool continuous, bool silence_detection) {
|
||||
if (this->state_ == State::IDLE) {
|
||||
this->continuous_ = continuous;
|
||||
this->silence_detection_ = silence_detection;
|
||||
#ifdef USE_ESP_ADF
|
||||
if (this->use_wake_word_) {
|
||||
this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->set_state_(State::START_MICROPHONE, State::START_PIPELINE);
|
||||
}
|
||||
|
||||
this->set_state_(State::START_MICROPHONE, State::START_PIPELINE);
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceAssistant::request_stop() {
|
||||
this->continuous_ = false;
|
||||
this->continue_conversation_ = false;
|
||||
|
||||
switch (this->state_) {
|
||||
case State::IDLE:
|
||||
@@ -611,6 +557,16 @@ void VoiceAssistant::request_stop() {
|
||||
this->signal_stop_();
|
||||
break;
|
||||
case State::STREAMING_RESPONSE:
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
// Stop any ongoing media player announcement
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->media_player_->make_call()
|
||||
.set_command(media_player::MEDIA_PLAYER_COMMAND_STOP)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case State::RESPONSE_FINISHED:
|
||||
break; // Let the incoming audio stream finish then it will go to idle.
|
||||
}
|
||||
@@ -628,9 +584,9 @@ void VoiceAssistant::signal_stop_() {
|
||||
}
|
||||
|
||||
void VoiceAssistant::start_playback_timeout_() {
|
||||
this->set_timeout("playing", 100, [this]() {
|
||||
this->set_timeout("playing", 2000, [this]() {
|
||||
this->cancel_timeout("speaker-timeout");
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
|
||||
api::VoiceAssistantAnnounceFinished msg;
|
||||
msg.success = true;
|
||||
@@ -679,6 +635,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
for (auto arg : msg.data) {
|
||||
if (arg.name == "conversation_id") {
|
||||
this->conversation_id_ = std::move(arg.value);
|
||||
} else if (arg.name == "continue_conversation") {
|
||||
this->continue_conversation_ = (arg.value == "1");
|
||||
}
|
||||
}
|
||||
this->defer([this]() { this->intent_end_trigger_->trigger(); });
|
||||
@@ -722,6 +680,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
// Start the playback timeout, as the media player state isn't immediately updated
|
||||
this->start_playback_timeout_();
|
||||
}
|
||||
@@ -740,15 +701,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
} else if (this->state_ == State::STREAMING_MICROPHONE) {
|
||||
this->ring_buffer_->reset();
|
||||
#ifdef USE_ESP_ADF
|
||||
if (this->use_wake_word_) {
|
||||
// No need to stop the microphone since we didn't use the speaker
|
||||
this->set_state_(State::WAIT_FOR_VAD, State::WAITING_FOR_VAD);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
}
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
}
|
||||
this->defer([this]() { this->end_trigger_->trigger(); });
|
||||
break;
|
||||
@@ -888,8 +841,28 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->tts_start_trigger_->trigger(msg.text);
|
||||
this->media_player_->make_call().set_media_url(msg.media_id).set_announcement(true).perform();
|
||||
this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE);
|
||||
if (!msg.preannounce_media_id.empty()) {
|
||||
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform();
|
||||
}
|
||||
// Enqueueing a URL with an empty playlist will still play the file immediately
|
||||
this->media_player_->make_call()
|
||||
.set_command(media_player::MEDIA_PLAYER_COMMAND_ENQUEUE)
|
||||
.set_media_url(msg.media_id)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
this->continue_conversation_ = msg.start_conversation;
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
// Start the playback timeout, as the media player state isn't immediately updated
|
||||
this->start_playback_timeout_();
|
||||
|
||||
if (this->continuous_) {
|
||||
this->set_state_(State::STOP_MICROPHONE, State::STREAMING_RESPONSE);
|
||||
} else {
|
||||
this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE);
|
||||
}
|
||||
|
||||
this->tts_end_trigger_->trigger(msg.media_id);
|
||||
this->end_trigger_->trigger();
|
||||
}
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
#endif
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
#ifdef USE_ESP_ADF
|
||||
#include <esp_vad.h>
|
||||
#endif
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -41,6 +37,7 @@ enum VoiceAssistantFeature : uint32_t {
|
||||
FEATURE_API_AUDIO = 1 << 2,
|
||||
FEATURE_TIMERS = 1 << 3,
|
||||
FEATURE_ANNOUNCE = 1 << 4,
|
||||
FEATURE_START_CONVERSATION = 1 << 5,
|
||||
};
|
||||
|
||||
enum class State {
|
||||
@@ -95,6 +92,7 @@ class VoiceAssistant : public Component {
|
||||
VoiceAssistant();
|
||||
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
void start_streaming();
|
||||
void start_streaming(struct sockaddr_storage *addr, uint16_t port);
|
||||
@@ -140,6 +138,7 @@ class VoiceAssistant : public Component {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
flags |= VoiceAssistantFeature::FEATURE_ANNOUNCE;
|
||||
flags |= VoiceAssistantFeature::FEATURE_START_CONVERSATION;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -161,9 +160,6 @@ class VoiceAssistant : public Component {
|
||||
bool is_continuous() const { return this->continuous_; }
|
||||
|
||||
void set_use_wake_word(bool use_wake_word) { this->use_wake_word_ = use_wake_word; }
|
||||
#ifdef USE_ESP_ADF
|
||||
void set_vad_threshold(uint8_t vad_threshold) { this->vad_threshold_ = vad_threshold; }
|
||||
#endif
|
||||
|
||||
void set_noise_suppression_level(uint8_t noise_suppression_level) {
|
||||
this->noise_suppression_level_ = noise_suppression_level;
|
||||
@@ -212,7 +208,6 @@ class VoiceAssistant : public Component {
|
||||
void clear_buffers_();
|
||||
void deallocate_buffers_();
|
||||
|
||||
int read_microphone_();
|
||||
void set_state_(State state);
|
||||
void set_state_(State state, State desired_state);
|
||||
void signal_stop_();
|
||||
@@ -267,6 +262,8 @@ class VoiceAssistant : public Component {
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
media_player::MediaPlayer *media_player_{nullptr};
|
||||
bool media_player_wait_for_announcement_start_{false};
|
||||
bool media_player_wait_for_announcement_end_{false};
|
||||
#endif
|
||||
|
||||
bool local_output_{false};
|
||||
@@ -275,14 +272,7 @@ class VoiceAssistant : public Component {
|
||||
|
||||
std::string wake_word_{""};
|
||||
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
#ifdef USE_ESP_ADF
|
||||
vad_handle_t vad_instance_;
|
||||
uint8_t vad_threshold_{5};
|
||||
uint8_t vad_counter_{0};
|
||||
#endif
|
||||
std::unique_ptr<RingBuffer> ring_buffer_;
|
||||
std::shared_ptr<RingBuffer> ring_buffer_;
|
||||
|
||||
bool use_wake_word_;
|
||||
uint8_t noise_suppression_level_;
|
||||
@@ -291,11 +281,12 @@ class VoiceAssistant : public Component {
|
||||
uint32_t conversation_timeout_;
|
||||
|
||||
uint8_t *send_buffer_{nullptr};
|
||||
int16_t *input_buffer_{nullptr};
|
||||
|
||||
bool continuous_{false};
|
||||
bool silence_detection_;
|
||||
|
||||
bool continue_conversation_{false};
|
||||
|
||||
State state_{State::IDLE};
|
||||
State desired_state_{State::IDLE};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#ifdef USE_ESP32
|
||||
#include <soc/soc_caps.h>
|
||||
#include "esp_idf_version.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
@@ -40,7 +41,7 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
esp_task_wdt_config_t wdt_config = {
|
||||
.timeout_ms = timeout_ms,
|
||||
.idle_core_mask = 0x03,
|
||||
.idle_core_mask = (1 << SOC_CPU_CORES_NUM) - 1,
|
||||
.trigger_panic = true,
|
||||
};
|
||||
esp_task_wdt_reconfigure(&wdt_config);
|
||||
|
||||
@@ -70,6 +70,9 @@ WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_(
|
||||
WaveshareEPaper4P2InBV2BWR = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper4P2InBV2BWR", WaveshareEPaperBWR
|
||||
)
|
||||
WaveshareEPaper5P65InF = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper5P65InF", WaveshareEPaper7C
|
||||
)
|
||||
WaveshareEPaper5P8In = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper5P8In", WaveshareEPaper
|
||||
)
|
||||
@@ -150,6 +153,7 @@ MODELS = {
|
||||
"4.20in": ("b", WaveshareEPaper4P2In),
|
||||
"4.20in-bv2": ("b", WaveshareEPaper4P2InBV2),
|
||||
"4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR),
|
||||
"5.65in-f": ("b", WaveshareEPaper5P65InF),
|
||||
"5.83in": ("b", WaveshareEPaper5P8In),
|
||||
"5.83inv2": ("b", WaveshareEPaper5P8InV2),
|
||||
"7.30in-f": ("b", WaveshareEPaper7P3InF),
|
||||
|
||||
@@ -258,6 +258,47 @@ void WaveshareEPaper7C::fill(Color color) {
|
||||
}
|
||||
}
|
||||
}
|
||||
void WaveshareEPaper7C::send_buffers_() {
|
||||
if (this->buffers_[0] == nullptr) {
|
||||
ESP_LOGE(TAG, "Buffer unavailable!");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS;
|
||||
uint8_t byte_to_send;
|
||||
for (auto &buffer : this->buffers_) {
|
||||
for (uint32_t buffer_pos = 0; buffer_pos < small_buffer_length; buffer_pos += 3) {
|
||||
std::bitset<24> triplet =
|
||||
buffer[buffer_pos + 0] << 16 | buffer[buffer_pos + 1] << 8 | buffer[buffer_pos + 2] << 0;
|
||||
// 8 bitset<3> are stored in 3 bytes
|
||||
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||
// | byte 1 | byte 2 | byte 3 |
|
||||
byte_to_send = ((triplet >> 17).to_ulong() & 0b01110000) | ((triplet >> 18).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
|
||||
byte_to_send = ((triplet >> 11).to_ulong() & 0b01110000) | ((triplet >> 12).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
|
||||
byte_to_send = ((triplet >> 5).to_ulong() & 0b01110000) | ((triplet >> 6).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
|
||||
byte_to_send = ((triplet << 1).to_ulong() & 0b01110000) | ((triplet << 0).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
}
|
||||
App.feed_wdt();
|
||||
}
|
||||
}
|
||||
void WaveshareEPaper7C::reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(20);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
|
||||
return;
|
||||
@@ -3307,6 +3348,175 @@ void WaveshareEPaper7P5In::dump_config() {
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// Waveshare 5.65F ========================================================
|
||||
|
||||
namespace cmddata_5P65InF {
|
||||
// WaveshareEPaper5P65InF commands
|
||||
// https://www.waveshare.com/wiki/5.65inch_e-Paper_Module_(F)
|
||||
|
||||
// R00H (PSR): Panel setting Register
|
||||
// UD(1): scan up
|
||||
// SHL(1) shift right
|
||||
// SHD_N(1) DC-DC on
|
||||
// RST_N(1) no reset
|
||||
static const uint8_t R00_CMD_PSR[] = {0x00, 0xEF, 0x08};
|
||||
|
||||
// R01H (PWR): Power setting Register
|
||||
// internal DC-DC power generation
|
||||
static const uint8_t R01_CMD_PWR[] = {0x01, 0x07, 0x00, 0x00, 0x00};
|
||||
|
||||
// R02H (POF): Power OFF Command
|
||||
static const uint8_t R02_CMD_POF[] = {0x02};
|
||||
|
||||
// R03H (PFS): Power off sequence setting Register
|
||||
// T_VDS_OFF (00) = 1 frame
|
||||
static const uint8_t R03_CMD_PFS[] = {0x03, 0x00};
|
||||
|
||||
// R04H (PON): Power ON Command
|
||||
static const uint8_t R04_CMD_PON[] = {0x04};
|
||||
|
||||
// R06h (BTST): Booster Soft Start
|
||||
static const uint8_t R06_CMD_BTST[] = {0x06, 0xC7, 0xC7, 0x1D};
|
||||
|
||||
// R07H (DSLP): Deep sleep#
|
||||
// Note Documentation @ Waveshare shows cmd code as 0x10 in table, but
|
||||
// 0x10 is DTM1.
|
||||
static const uint8_t R07_CMD_DSLP[] = {0x07, 0xA5};
|
||||
|
||||
// R10H (DTM1): Data Start Transmission 1
|
||||
|
||||
static const uint8_t R10_CMD_DTM1[] = {0x10};
|
||||
|
||||
// R11H (DSP): Data Stop
|
||||
static const uint8_t R11_CMD_DSP[] = {0x11};
|
||||
|
||||
// R12H (DRF): Display Refresh
|
||||
static const uint8_t R12_CMD_DRF[] = {0x12};
|
||||
|
||||
// R13H (IPC): Image Process Command
|
||||
static const uint8_t R13_CMD_IPC[] = {0x13, 0x00};
|
||||
|
||||
// R30H (PLL): PLL Control
|
||||
// 0x3C = 50Hz
|
||||
static const uint8_t R30_CMD_PLL[] = {0x30, 0x3C};
|
||||
|
||||
// R41H (TSE): Temperature Sensor Enable
|
||||
// TSE(0) enable, TO(0000) +0 degree offset
|
||||
static const uint8_t R41_CMD_TSE[] = {0x41, 0x00};
|
||||
|
||||
// R50H (CDI) VCOM and Data interval setting
|
||||
// CDI(0111) 10
|
||||
// DDX(1), VBD(001) Border output "White"
|
||||
static const uint8_t R50_CMD_CDI[] = {0x50, 0x37};
|
||||
|
||||
// R60H (TCON) Gate and Source non overlap period command
|
||||
// S2G(10) 12 units
|
||||
// G2S(10) 12 units
|
||||
static const uint8_t R60_CMD_TCON[] = {0x60, 0x22};
|
||||
|
||||
// R61H (TRES) Resolution Setting
|
||||
// 0x258 = 600
|
||||
// 0x1C0 = 448
|
||||
static const uint8_t R61_CMD_TRES[] = {0x61, 0x02, 0x58, 0x01, 0xC0};
|
||||
|
||||
// RE3H (PWS) Power Savings
|
||||
static const uint8_t RE3_CMD_PWS[] = {0xE3, 0xAA};
|
||||
} // namespace cmddata_5P65InF
|
||||
|
||||
void WaveshareEPaper5P65InF::initialize() {
|
||||
if (this->buffers_[0] == nullptr) {
|
||||
ESP_LOGE(TAG, "Buffer unavailable!");
|
||||
return;
|
||||
}
|
||||
|
||||
this->reset_();
|
||||
delay(20);
|
||||
this->wait_until_(IDLE);
|
||||
|
||||
using namespace cmddata_5P65InF;
|
||||
|
||||
this->cmd_data(R00_CMD_PSR, sizeof(R00_CMD_PSR));
|
||||
this->cmd_data(R01_CMD_PWR, sizeof(R01_CMD_PWR));
|
||||
this->cmd_data(R03_CMD_PFS, sizeof(R03_CMD_PFS));
|
||||
this->cmd_data(R06_CMD_BTST, sizeof(R06_CMD_BTST));
|
||||
this->cmd_data(R30_CMD_PLL, sizeof(R30_CMD_PLL));
|
||||
this->cmd_data(R41_CMD_TSE, sizeof(R41_CMD_TSE));
|
||||
this->cmd_data(R50_CMD_CDI, sizeof(R50_CMD_CDI));
|
||||
this->cmd_data(R60_CMD_TCON, sizeof(R60_CMD_TCON));
|
||||
this->cmd_data(R61_CMD_TRES, sizeof(R61_CMD_TRES));
|
||||
this->cmd_data(RE3_CMD_PWS, sizeof(RE3_CMD_PWS));
|
||||
|
||||
delay(100); // NOLINT
|
||||
this->cmd_data(R50_CMD_CDI, sizeof(R50_CMD_CDI));
|
||||
|
||||
ESP_LOGI(TAG, "Display initialized successfully");
|
||||
}
|
||||
|
||||
void HOT WaveshareEPaper5P65InF::display() {
|
||||
// INITIALIZATION
|
||||
ESP_LOGI(TAG, "Initialise the display");
|
||||
this->initialize();
|
||||
|
||||
using namespace cmddata_5P65InF;
|
||||
|
||||
// COMMAND DATA START TRANSMISSION
|
||||
ESP_LOGI(TAG, "Sending data to the display");
|
||||
this->cmd_data(R61_CMD_TRES, sizeof(R61_CMD_TRES));
|
||||
this->cmd_data(R10_CMD_DTM1, sizeof(R10_CMD_DTM1));
|
||||
this->send_buffers_();
|
||||
|
||||
// COMMAND POWER ON
|
||||
ESP_LOGI(TAG, "Power on the display");
|
||||
this->cmd_data(R04_CMD_PON, sizeof(R04_CMD_PON));
|
||||
this->wait_until_(IDLE);
|
||||
|
||||
// COMMAND REFRESH SCREEN
|
||||
ESP_LOGI(TAG, "Refresh the display");
|
||||
this->cmd_data(R12_CMD_DRF, sizeof(R12_CMD_DRF));
|
||||
this->wait_until_(IDLE);
|
||||
|
||||
// COMMAND POWER OFF
|
||||
ESP_LOGI(TAG, "Power off the display");
|
||||
this->cmd_data(R02_CMD_POF, sizeof(R02_CMD_POF));
|
||||
this->wait_until_(BUSY);
|
||||
|
||||
if (this->deep_sleep_between_updates_) {
|
||||
ESP_LOGI(TAG, "Set the display to deep sleep");
|
||||
this->cmd_data(R07_CMD_DSLP, sizeof(R07_CMD_DSLP));
|
||||
}
|
||||
}
|
||||
|
||||
int WaveshareEPaper5P65InF::get_width_internal() { return 600; }
|
||||
int WaveshareEPaper5P65InF::get_height_internal() { return 448; }
|
||||
uint32_t WaveshareEPaper5P65InF::idle_timeout_() { return 35000; }
|
||||
|
||||
void WaveshareEPaper5P65InF::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 5.65in-F");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
bool WaveshareEPaper5P65InF::wait_until_(WaitForState busy_state) {
|
||||
if (this->busy_pin_ == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t start = millis();
|
||||
while (busy_state != this->busy_pin_->digital_read()) {
|
||||
if (millis() - start > this->idle_timeout_()) {
|
||||
ESP_LOGE(TAG, "Timeout while displaying image!");
|
||||
return false;
|
||||
}
|
||||
App.feed_wdt();
|
||||
delay(10);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WaveshareEPaper7P3InF::initialize() {
|
||||
if (this->buffers_[0] == nullptr) {
|
||||
ESP_LOGE(TAG, "Buffer unavailable!");
|
||||
@@ -3411,11 +3621,6 @@ void WaveshareEPaper7P3InF::initialize() {
|
||||
ESP_LOGI(TAG, "Display initialized successfully");
|
||||
}
|
||||
void HOT WaveshareEPaper7P3InF::display() {
|
||||
if (this->buffers_[0] == nullptr) {
|
||||
ESP_LOGE(TAG, "Buffer unavailable!");
|
||||
return;
|
||||
}
|
||||
|
||||
// INITIALIZATION
|
||||
ESP_LOGI(TAG, "Initialise the display");
|
||||
this->initialize();
|
||||
@@ -3423,29 +3628,7 @@ void HOT WaveshareEPaper7P3InF::display() {
|
||||
// COMMAND DATA START TRANSMISSION
|
||||
ESP_LOGI(TAG, "Sending data to the display");
|
||||
this->command(0x10);
|
||||
uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS;
|
||||
uint8_t byte_to_send;
|
||||
for (auto &buffer : this->buffers_) {
|
||||
for (uint32_t buffer_pos = 0; buffer_pos < small_buffer_length; buffer_pos += 3) {
|
||||
std::bitset<24> triplet =
|
||||
buffer[buffer_pos + 0] << 16 | buffer[buffer_pos + 1] << 8 | buffer[buffer_pos + 2] << 0;
|
||||
// 8 bitset<3> are stored in 3 bytes
|
||||
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||
// | byte 1 | byte 2 | byte 3 |
|
||||
byte_to_send = ((triplet >> 17).to_ulong() & 0b01110000) | ((triplet >> 18).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
|
||||
byte_to_send = ((triplet >> 11).to_ulong() & 0b01110000) | ((triplet >> 12).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
|
||||
byte_to_send = ((triplet >> 5).to_ulong() & 0b01110000) | ((triplet >> 6).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
|
||||
byte_to_send = ((triplet << 1).to_ulong() & 0b01110000) | ((triplet << 0).to_ulong() & 0b00000111);
|
||||
this->data(byte_to_send);
|
||||
}
|
||||
App.feed_wdt();
|
||||
}
|
||||
this->send_buffers_();
|
||||
|
||||
// COMMAND POWER ON
|
||||
ESP_LOGI(TAG, "Power on the display");
|
||||
@@ -3464,9 +3647,11 @@ void HOT WaveshareEPaper7P3InF::display() {
|
||||
this->data(0x00);
|
||||
this->wait_until_idle_();
|
||||
|
||||
ESP_LOGI(TAG, "Set the display to deep sleep");
|
||||
this->command(0x07);
|
||||
this->data(0xA5);
|
||||
if (this->deep_sleep_between_updates_) {
|
||||
ESP_LOGI(TAG, "Set the display to deep sleep");
|
||||
this->command(0x07);
|
||||
this->data(0xA5);
|
||||
}
|
||||
}
|
||||
int WaveshareEPaper7P3InF::get_width_internal() { return 800; }
|
||||
int WaveshareEPaper7P3InF::get_height_internal() { return 480; }
|
||||
|
||||
@@ -94,7 +94,10 @@ class WaveshareEPaper7C : public WaveshareEPaperBase {
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
uint32_t get_buffer_length_() override;
|
||||
void setup() override;
|
||||
|
||||
void init_internal_7c_(uint32_t buffer_length);
|
||||
void send_buffers_();
|
||||
void reset_();
|
||||
|
||||
static const int NUM_BUFFERS = 10;
|
||||
uint8_t *buffers_[NUM_BUFFERS];
|
||||
@@ -683,6 +686,29 @@ class WaveshareEPaper5P8InV2 : public WaveshareEPaper {
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaper5P65InF : public WaveshareEPaper7C {
|
||||
public:
|
||||
void initialize() override;
|
||||
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
|
||||
int get_height_internal() override;
|
||||
|
||||
uint32_t idle_timeout_() override;
|
||||
|
||||
void deep_sleep() override { ; }
|
||||
|
||||
enum WaitForState { BUSY = true, IDLE = false };
|
||||
bool wait_until_(WaitForState state);
|
||||
|
||||
bool deep_sleep_between_updates_{true};
|
||||
};
|
||||
|
||||
class WaveshareEPaper7P3InF : public WaveshareEPaper7C {
|
||||
public:
|
||||
void initialize() override;
|
||||
@@ -703,17 +729,6 @@ class WaveshareEPaper7P3InF : public WaveshareEPaper7C {
|
||||
bool wait_until_idle_();
|
||||
|
||||
bool deep_sleep_between_updates_{true};
|
||||
|
||||
void reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(20);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(20);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
class WaveshareEPaper7P5In : public WaveshareEPaper {
|
||||
|
||||
@@ -56,7 +56,6 @@ from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
@@ -117,7 +116,7 @@ RequiredFieldInvalid = vol.RequiredFieldInvalid
|
||||
ROOT_CONFIG_PATH = object()
|
||||
|
||||
RESERVED_IDS = [
|
||||
# C++ keywords http://en.cppreference.com/w/cpp/keyword
|
||||
# C++ keywords https://en.cppreference.com/w/cpp/keyword
|
||||
"alarm",
|
||||
"alignas",
|
||||
"alignof",
|
||||
@@ -1942,70 +1941,28 @@ def platformio_version_constraint(value):
|
||||
|
||||
def require_framework_version(
|
||||
*,
|
||||
esp_idf=None,
|
||||
esp32_arduino=None,
|
||||
esp8266_arduino=None,
|
||||
rp2040_arduino=None,
|
||||
bk72xx_libretiny=None,
|
||||
host=None,
|
||||
max_version=False,
|
||||
extra_message=None,
|
||||
**kwargs,
|
||||
):
|
||||
def validator(value):
|
||||
core_data = CORE.data[KEY_CORE]
|
||||
framework = core_data[KEY_TARGET_FRAMEWORK]
|
||||
if framework == "esp-idf":
|
||||
if esp_idf is None:
|
||||
msg = "This feature is incompatible with esp-idf"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
required = esp_idf
|
||||
elif CORE.is_bk72xx and framework == "arduino":
|
||||
if bk72xx_libretiny is None:
|
||||
msg = "This feature is incompatible with BK72XX"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
required = bk72xx_libretiny
|
||||
elif CORE.is_esp32 and framework == "arduino":
|
||||
if esp32_arduino is None:
|
||||
msg = "This feature is incompatible with ESP32 using arduino framework"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
required = esp32_arduino
|
||||
elif CORE.is_esp8266 and framework == "arduino":
|
||||
if esp8266_arduino is None:
|
||||
msg = "This feature is incompatible with ESP8266"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
required = esp8266_arduino
|
||||
elif CORE.is_rp2040 and framework == "arduino":
|
||||
if rp2040_arduino is None:
|
||||
msg = "This feature is incompatible with RP2040"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
required = rp2040_arduino
|
||||
elif CORE.is_host and framework == "host":
|
||||
if host is None:
|
||||
msg = "This feature is incompatible with host platform"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
required = host
|
||||
else:
|
||||
raise Invalid(
|
||||
f"""
|
||||
Internal Error: require_framework_version does not support this platform configuration
|
||||
platform: {core_data[KEY_TARGET_PLATFORM]}
|
||||
framework: {framework}
|
||||
|
||||
Please report this issue on GitHub -> https://github.com/esphome/issues/issues/new?template=bug_report.yml.
|
||||
"""
|
||||
)
|
||||
if CORE.is_host and framework == "host":
|
||||
key = "host"
|
||||
elif framework == "esp-idf":
|
||||
key = "esp_idf"
|
||||
else:
|
||||
key = CORE.target_platform + "_" + framework
|
||||
|
||||
if key not in kwargs:
|
||||
msg = f"This feature is incompatible with {CORE.target_platform.upper()} using {framework} framework"
|
||||
if extra_message:
|
||||
msg += f". {extra_message}"
|
||||
raise Invalid(msg)
|
||||
|
||||
required = kwargs[key]
|
||||
|
||||
if max_version:
|
||||
if core_data[KEY_FRAMEWORK_VERSION] > required:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user