Merge remote-tracking branch 'upstream/dev' into proto_field_ifdefs

This commit is contained in:
J. Nick Koston 2025-07-15 15:19:26 -10:00
commit 8f38be0914
No known key found for this signature in database
197 changed files with 5145 additions and 3199 deletions

View File

@ -1 +1 @@
a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a 07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a

View File

@ -1,4 +1,4 @@
[run] [run]
omit = omit =
esphome/components/* esphome/components/*
tests/integration/* tests/integration/*

92
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,92 @@
name: Report an issue with ESPHome
description: Report an issue with ESPHome.
body:
- type: markdown
attributes:
value: |
This issue form is for reporting bugs only!
If you have a feature request or enhancement, please [request them here instead][fr].
[fr]: https://github.com/orgs/esphome/discussions
- type: textarea
validations:
required: true
id: problem
attributes:
label: The problem
description: >-
Describe the issue you are experiencing here to communicate to the
maintainers. Tell us what you were trying to do and what happened.
Provide a clear and concise description of what the problem is.
- type: markdown
attributes:
value: |
## Environment
- type: input
id: version
validations:
required: true
attributes:
label: Which version of ESPHome has the issue?
description: >
ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev.
- type: dropdown
validations:
required: true
id: installation
attributes:
label: What type of installation are you using?
options:
- Home Assistant Add-on
- Docker
- pip
- type: dropdown
validations:
required: true
id: platform
attributes:
label: What platform are you using?
options:
- ESP8266
- ESP32
- RP2040
- BK72XX
- RTL87XX
- LN882X
- Host
- Other
- type: input
id: component_name
attributes:
label: Component causing the issue
description: >
The name of the component or platform. For example, api/i2c or ultrasonic.
- type: markdown
attributes:
value: |
# Details
- type: textarea
id: config
attributes:
label: YAML Config
description: |
Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below.
render: yaml
- type: textarea
id: logs
attributes:
label: Anything in the logs that might be useful for us?
description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs.
render: txt
- type: textarea
id: additional
attributes:
label: Additional information
description: >
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.

View File

@ -1,15 +1,21 @@
--- ---
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Issue Tracker - name: Report an issue with the ESPHome documentation
url: https://github.com/esphome/issues url: https://github.com/esphome/esphome-docs/issues/new/choose
about: Please create bug reports in the dedicated issue tracker. about: Report an issue with the ESPHome documentation.
- name: Feature Request Tracker - name: Report an issue with the ESPHome web server
url: https://github.com/esphome/feature-requests url: https://github.com/esphome/esphome-webserver/issues/new/choose
about: | about: Report an issue with the ESPHome web server.
Please create feature requests in the dedicated feature request tracker. - name: Report an issue with the ESPHome Builder / Dashboard
url: https://github.com/esphome/dashboard/issues/new/choose
about: Report an issue with the ESPHome Builder / Dashboard.
- name: Report an issue with the ESPHome API client
url: https://github.com/esphome/aioesphomeapi/issues/new/choose
about: Report an issue with the ESPHome API client.
- name: Make a Feature Request
url: https://github.com/orgs/esphome/discussions
about: Please create feature requests in the dedicated feature request tracker.
- name: Frequently Asked Question - name: Frequently Asked Question
url: https://esphome.io/guides/faq.html url: https://esphome.io/guides/faq.html
about: | about: Please view the FAQ for common questions and what to include in a bug report.
Please view the FAQ for common questions and what
to include in a bug report.

View File

@ -73,4 +73,3 @@ jobs:
}); });
} }
} }

View File

@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
@ -58,55 +58,9 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install -r requirements.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt pre-commit
pip install -e . pip install -e .
ruff:
name: Check ruff
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run Ruff
run: |
. venv/bin/activate
ruff format esphome tests
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
flake8:
name: Check flake8
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run flake8
run: |
. venv/bin/activate
flake8 esphome
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
pylint: pylint:
name: Check pylint name: Check pylint
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -130,29 +84,6 @@ jobs:
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() if: always()
pyupgrade:
name: Check pyupgrade
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run pyupgrade
run: |
. venv/bin/activate
pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f`
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
ci-custom: ci-custom:
name: Run script/ci-custom name: Run script/ci-custom
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -248,7 +179,6 @@ jobs:
outputs: outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }} integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-format: ${{ steps.determine.outputs.clang-format }}
python-linters: ${{ steps.determine.outputs.python-linters }} python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }} changed-components: ${{ steps.determine.outputs.changed-components }}
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
@ -276,7 +206,6 @@ jobs:
# Extract individual fields # Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-format=$(echo "$output" | jq -r '.clang_format')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
@ -317,46 +246,11 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/ pytest -vv --no-cov --tb=native -n auto tests/integration/
clang-format:
name: Check clang-format
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-format == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install clang-format
run: |
. venv/bin/activate
pip install clang-format -c requirements_dev.txt
- name: Run clang-format
run: |
. venv/bin/activate
script/clang-format -i
git diff-index --quiet HEAD --
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
clang-tidy: clang-tidy:
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- ruff
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- determine-jobs - determine-jobs
if: needs.determine-jobs.outputs.clang-tidy == 'true' if: needs.determine-jobs.outputs.clang-tidy == 'true'
env: env:
@ -562,24 +456,41 @@ jobs:
./script/test_build_components -e compile -c $component ./script/test_build_components -e compile -c $component
done done
pre-commit-ci-lite:
name: pre-commit.ci lite
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: pre-commit/action@v3.0.1
env:
SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@v1.1.0
if: always()
ci-status: ci-status:
name: CI Status name: CI Status
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- ruff
- ci-custom - ci-custom
- clang-format
- flake8
- pylint - pylint
- pytest - pytest
- integration-tests - integration-tests
- pyupgrade
- clang-tidy - clang-tidy
- determine-jobs - determine-jobs
- test-build-components - test-build-components
- test-build-components-splitter - test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite
if: always() if: always()
steps: steps:
- name: Success - name: Success

View File

@ -1,25 +0,0 @@
---
name: YAML lint
on:
push:
branches: [dev, beta, release]
paths:
- "**.yaml"
- "**.yml"
pull_request:
paths:
- "**.yaml"
- "**.yml"
jobs:
yamllint:
name: yamllint
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Run yamllint
uses: frenck/action-yamllint@v1.5.0
with:
strict: true

View File

@ -1,6 +1,13 @@
--- ---
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_commit_msg: 'pre-commit: autoupdate'
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
# Skip hooks that have issues in pre-commit CI environment
skip: [pylint, clang-tidy-hash]
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
@ -20,13 +27,15 @@ repos:
- pydocstyle==5.1.1 - pydocstyle==5.1.1
files: ^(esphome|tests)/.+\.py$ files: ^(esphome|tests)/.+\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0 rev: v5.0.0
hooks: hooks:
- id: no-commit-to-branch - id: no-commit-to-branch
args: args:
- --branch=dev - --branch=dev
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.20.0 rev: v3.20.0
hooks: hooks:
@ -36,6 +45,7 @@ repos:
rev: v1.37.1 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v13.0.1 rev: v13.0.1
hooks: hooks:

View File

@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81 esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages esphome/components/online_image/* @clydebarrow @guillempages
@ -378,6 +379,7 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
@ -535,5 +537,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97 esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zio_ultrasonic/* @kahrendt

View File

@ -51,82 +51,83 @@ SAMPLING_MODES = {
"max": sampling_mode.MAX, "max": sampling_mode.MAX,
} }
adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True)
# pin to adc1 channel mapping # pin to adc1 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0, 36: adc_channel_t.ADC_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1, 37: adc_channel_t.ADC_CHANNEL_1,
38: adc1_channel_t.ADC1_CHANNEL_2, 38: adc_channel_t.ADC_CHANNEL_2,
39: adc1_channel_t.ADC1_CHANNEL_3, 39: adc_channel_t.ADC_CHANNEL_3,
32: adc1_channel_t.ADC1_CHANNEL_4, 32: adc_channel_t.ADC_CHANNEL_4,
33: adc1_channel_t.ADC1_CHANNEL_5, 33: adc_channel_t.ADC_CHANNEL_5,
34: adc1_channel_t.ADC1_CHANNEL_6, 34: adc_channel_t.ADC_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7, 35: adc_channel_t.ADC_CHANNEL_7,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: { VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: { VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: { VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5, 5: adc_channel_t.ADC_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6, 6: adc_channel_t.ADC_CHANNEL_6,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: { VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6, 7: adc_channel_t.ADC_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7, 8: adc_channel_t.ADC_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6, 7: adc_channel_t.ADC_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7, 8: adc_channel_t.ADC_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
} }
@ -135,24 +136,24 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0, 4: adc_channel_t.ADC_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1, 0: adc_channel_t.ADC_CHANNEL_1,
2: adc2_channel_t.ADC2_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
15: adc2_channel_t.ADC2_CHANNEL_3, 15: adc_channel_t.ADC_CHANNEL_3,
13: adc2_channel_t.ADC2_CHANNEL_4, 13: adc_channel_t.ADC_CHANNEL_4,
12: adc2_channel_t.ADC2_CHANNEL_5, 12: adc_channel_t.ADC_CHANNEL_5,
14: adc2_channel_t.ADC2_CHANNEL_6, 14: adc_channel_t.ADC_CHANNEL_6,
27: adc2_channel_t.ADC2_CHANNEL_7, 27: adc_channel_t.ADC_CHANNEL_7,
25: adc2_channel_t.ADC2_CHANNEL_8, 25: adc_channel_t.ADC_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9, 26: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: { VARIANT_ESP32C2: {
5: adc2_channel_t.ADC2_CHANNEL_0, 5: adc_channel_t.ADC_CHANNEL_0,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: { VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0, 5: adc_channel_t.ADC_CHANNEL_0,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2 VARIANT_ESP32C6: {}, # no ADC2
@ -160,29 +161,29 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32H2: {}, # no ADC2 VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc_channel_t.ADC_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2, 13: adc_channel_t.ADC_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3, 14: adc_channel_t.ADC_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4, 15: adc_channel_t.ADC_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5, 16: adc_channel_t.ADC_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6, 17: adc_channel_t.ADC_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7, 18: adc_channel_t.ADC_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc_channel_t.ADC_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2, 13: adc_channel_t.ADC_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3, 14: adc_channel_t.ADC_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4, 15: adc_channel_t.ADC_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5, 16: adc_channel_t.ADC_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6, 17: adc_channel_t.ADC_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7, 18: adc_channel_t.ADC_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
} }

View File

@ -3,12 +3,15 @@
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h" #include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_adc_cal.h> #include "esp_adc/adc_cali.h"
#include "driver/adc.h" #include "esp_adc/adc_cali_scheme.h"
#endif // USE_ESP32 #include "esp_adc/adc_oneshot.h"
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
#endif // USE_ESP32
namespace esphome { namespace esphome {
namespace adc { namespace adc {
@ -49,33 +52,72 @@ class Aggregator {
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public: public:
/// Update the sensor's state by reading the current ADC value.
/// This method is called periodically based on the update interval.
void update() override;
/// Set up the ADC sensor by initializing hardware and calibration parameters.
/// This method is called once during device initialization.
void setup() override;
/// Output the configuration details of the ADC sensor for debugging purposes.
/// This method is called during the ESPHome setup process to log the configuration.
void dump_config() override;
/// Return the setup priority for this component.
/// Components with higher priority are initialized earlier during setup.
/// @return A float representing the setup priority.
float get_setup_priority() const override;
/// Set the GPIO pin to be used by the ADC sensor.
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
/// Enable or disable the output of raw ADC values (unprocessed data).
/// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false).
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
/// Set the number of samples to be taken for ADC readings to improve accuracy.
/// A higher sample count reduces noise but increases the reading time.
/// @param sample_count The number of samples (e.g., 1, 4, 8).
void set_sample_count(uint8_t sample_count);
/// Set the sampling mode for how multiple ADC samples are combined into a single measurement.
///
/// When multiple samples are taken (controlled by set_sample_count), they can be combined
/// in one of three ways:
/// - SamplingMode::AVG: Compute the average (default)
/// - SamplingMode::MIN: Use the lowest sample value
/// - SamplingMode::MAX: Use the highest sample value
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
void set_sampling_mode(SamplingMode sampling_mode);
/// Perform a single ADC sampling operation and return the measured value.
/// This function handles raw readings, calibration, and averaging as needed.
/// @return The sampled value as a float.
float sample() override;
#ifdef USE_ESP32 #ifdef USE_ESP32
/// Set the attenuation for this pin. Only available on the ESP32. /// Set the ADC attenuation level to adjust the input voltage range.
/// This determines how the ADC interprets input voltages, allowing for greater precision
/// or the ability to measure higher voltages depending on the chosen attenuation level.
/// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11).
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
void set_channel1(adc1_channel_t channel) {
this->channel1_ = channel; /// Configure the ADC to use a specific channel on ADC1.
this->channel2_ = ADC2_CHANNEL_MAX; /// This sets the channel for single-shot or continuous ADC measurements.
} /// @param channel The ADC1 channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc.
void set_channel2(adc2_channel_t channel) { void set_channel(adc_unit_t unit, adc_channel_t channel) {
this->channel2_ = channel; this->adc_unit_ = unit;
this->channel1_ = ADC1_CHANNEL_MAX; this->channel_ = channel;
} }
/// Set whether autoranging should be enabled for the ADC.
/// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages.
/// @param autorange Boolean indicating whether to enable autoranging.
void set_autorange(bool autorange) { this->autorange_ = autorange; } void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32 #endif // USE_ESP32
/// Update ADC values
void update() override;
/// Setup ADC
void setup() override;
void dump_config() override;
/// `HARDWARE_LATE` setup priority
float get_setup_priority() const override;
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
void set_sample_count(uint8_t sample_count);
void set_sampling_mode(SamplingMode sampling_mode);
float sample() override;
#ifdef USE_ESP8266 #ifdef USE_ESP8266
std::string unique_id() override; std::string unique_id() override;
#endif // USE_ESP8266 #endif // USE_ESP8266
@ -90,17 +132,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
InternalGPIOPin *pin_; InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG}; SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_ESP32
float sample_autorange_();
float sample_fixed_attenuation_();
bool autorange_{false};
adc_oneshot_unit_handle_t adc_handle_{nullptr};
adc_cali_handle_t calibration_handle_{nullptr};
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc_channel_t channel_;
adc_unit_t adc_unit_;
struct SetupFlags {
uint8_t init_complete : 1;
uint8_t config_complete : 1;
uint8_t handle_init_complete : 1;
uint8_t calibration_complete : 1;
uint8_t reserved : 4;
} setup_flags_{};
static adc_oneshot_unit_handle_t shared_adc_handles[2];
#endif // USE_ESP32
#ifdef USE_RP2040 #ifdef USE_RP2040
bool is_temperature_{false}; bool is_temperature_{false};
#endif // USE_RP2040 #endif // USE_RP2040
#ifdef USE_ESP32
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false};
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#endif // USE_ESP32
}; };
} // namespace adc } // namespace adc

View File

@ -8,145 +8,308 @@ namespace adc {
static const char *const TAG = "adc.esp32"; static const char *const TAG = "adc.esp32";
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
#ifndef SOC_ADC_RTC_MAX_BITWIDTH const LogString *attenuation_to_str(adc_atten_t attenuation) {
#if USE_ESP32_VARIANT_ESP32S2 switch (attenuation) {
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; case ADC_ATTEN_DB_0:
#else return LOG_STR("0 dB");
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; case ADC_ATTEN_DB_2_5:
#endif // USE_ESP32_VARIANT_ESP32S2 return LOG_STR("2.5 dB");
#endif // SOC_ADC_RTC_MAX_BITWIDTH case ADC_ATTEN_DB_6:
return LOG_STR("6 dB");
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; case ADC_ATTEN_DB_12_COMPAT:
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; return LOG_STR("12 dB");
default:
void ADCSensor::setup() { return LOG_STR("Unknown Attenuation");
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
if (!this->autorange_) {
adc1_config_channel_atten(this->channel1_, this->attenuation_);
}
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
if (!this->autorange_) {
adc2_config_channel_atten(this->channel2_, this->attenuation_);
}
}
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
1100, // default vref
&this->cal_characteristics_[i]);
switch (cal_value) {
case ESP_ADC_CAL_VAL_EFUSE_VREF:
ESP_LOGV(TAG, "Using eFuse Vref for calibration");
break;
case ESP_ADC_CAL_VAL_EFUSE_TP:
ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration");
break;
case ESP_ADC_CAL_VAL_DEFAULT_VREF:
default:
break;
}
} }
} }
void ADCSensor::dump_config() { const LogString *adc_unit_to_str(adc_unit_t unit) {
static const char *const ATTEN_AUTO_STR = "auto"; switch (unit) {
static const char *const ATTEN_0DB_STR = "0 db"; case ADC_UNIT_1:
static const char *const ATTEN_2_5DB_STR = "2.5 db"; return LOG_STR("ADC1");
static const char *const ATTEN_6DB_STR = "6 db"; case ADC_UNIT_2:
static const char *const ATTEN_12DB_STR = "12 db"; return LOG_STR("ADC2");
const char *atten_str = ATTEN_AUTO_STR; default:
return LOG_STR("Unknown ADC Unit");
}
}
LOG_SENSOR("", "ADC Sensor", this); void ADCSensor::setup() {
LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
// Check if another sensor already initialized this ADC unit
if (!this->autorange_) { if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) {
switch (this->attenuation_) { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
case ADC_ATTEN_DB_0: init_config.unit_id = this->adc_unit_;
atten_str = ATTEN_0DB_STR; init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
break; #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
case ADC_ATTEN_DB_2_5: init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
atten_str = ATTEN_2_5DB_STR; #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
break; esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
case ADC_ATTEN_DB_6: if (err != ESP_OK) {
atten_str = ATTEN_6DB_STR; ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
break; this->mark_failed();
case ADC_ATTEN_DB_12_COMPAT: return;
atten_str = ATTEN_12DB_STR;
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
} }
} }
this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_];
this->setup_flags_.handle_init_complete = true;
adc_oneshot_chan_cfg_t config = {
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error configuring channel: %d", err);
this->mark_failed();
return;
}
this->setup_flags_.config_complete = true;
// Initialize ADC calibration
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
esp_err_t err;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
// RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.unit_id = this->adc_unit_;
cali_config.atten = this->attenuation_;
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
if (err == ESP_OK) {
this->calibration_handle_ = handle;
this->setup_flags_.calibration_complete = true;
ESP_LOGV(TAG, "Using curve fitting calibration");
} else {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // Other ESP32 variants use line fitting calibration
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2)
.default_vref = 1100, // Default reference voltage in mV
#endif // !defined(USE_ESP32_VARIANT_ESP32S2)
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
if (err == ESP_OK) {
this->calibration_handle_ = handle;
this->setup_flags_.calibration_complete = true;
ESP_LOGV(TAG, "Using line fitting calibration");
} else {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2
}
this->setup_flags_.init_complete = true;
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Attenuation: %s\n" " Channel: %d\n"
" Samples: %i\n" " Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s", " Sampling mode: %s",
atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
ESP_LOGCONFIG(
TAG,
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
if (!this->autorange_) { if (this->autorange_) {
auto aggr = Aggregator(this->sampling_mode_); return this->sample_autorange_();
} else {
return this->sample_fixed_attenuation_();
}
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { float ADCSensor::sample_fixed_attenuation_() {
int raw = -1; auto aggr = Aggregator(this->sampling_mode_);
if (this->channel1_ != ADC1_CHANNEL_MAX) {
raw = adc1_get_raw(this->channel1_);
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
}
if (raw == -1) {
return NAN;
}
aggr.add_sample(raw); for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int raw;
esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed with error %d", err);
continue;
} }
if (this->output_raw_) {
return aggr.aggregate(); if (raw == -1) {
ESP_LOGW(TAG, "Invalid ADC reading");
continue;
} }
uint32_t mv =
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); aggr.add_sample(raw);
return mv / 1000.0f;
} }
int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; uint32_t final_value = aggr.aggregate();
if (this->channel1_ != ADC1_CHANNEL_MAX) { if (this->output_raw_) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); return final_value;
raw12 = adc1_get_raw(this->channel1_); }
if (raw12 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); if (this->calibration_handle_ != nullptr) {
raw6 = adc1_get_raw(this->channel1_); int voltage_mv;
if (raw6 < ADC_MAX) { esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv);
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); if (err == ESP_OK) {
raw2 = adc1_get_raw(this->channel1_); return voltage_mv / 1000.0f;
if (raw2 < ADC_MAX) { } else {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
raw0 = adc1_get_raw(this->channel1_); if (this->calibration_handle_ != nullptr) {
} #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2
this->calibration_handle_ = nullptr;
} }
} }
} else if (this->channel2_ != ADC2_CHANNEL_MAX) { }
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); return final_value * 3.3f / 4095.0f;
if (raw12 < ADC_MAX) { }
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); float ADCSensor::sample_autorange_() {
if (raw6 < ADC_MAX) { // Auto-range mode
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> {
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); // First reconfigure the attenuation for this reading
if (raw2 < ADC_MAX) { adc_oneshot_chan_cfg_t config = {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); .atten = atten,
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); .bitwidth = ADC_BITWIDTH_DEFAULT,
} };
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err);
return {-1, 0.0f};
}
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
}
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
#endif
cali_config.unit_id = this->adc_unit_;
cali_config.atten = atten;
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2)
.default_vref = 1100,
#endif
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
#endif
int raw;
err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
return {-1, 0.0f};
}
float voltage = 0.0f;
if (handle != nullptr) {
int voltage_mv;
err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv);
if (err == ESP_OK) {
voltage = voltage_mv / 1000.0f;
} else {
voltage = raw * 3.3f / 4095.0f;
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
voltage = raw * 3.3f / 4095.0f;
}
return {raw, voltage};
};
auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12);
if (raw12 == -1) {
ESP_LOGE(TAG, "Failed to read ADC in autorange mode");
return NAN;
}
int raw6 = 4095, raw2 = 4095, raw0 = 4095;
float mv6 = 0, mv2 = 0, mv0 = 0;
if (raw12 < 4095) {
auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6);
raw6 = raw6_val;
mv6 = mv6_val;
if (raw6 < 4095 && raw6 != -1) {
auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5);
raw2 = raw2_val;
mv2 = mv2_val;
if (raw2 < 4095 && raw2 != -1) {
auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0);
raw0 = raw0_val;
mv0 = mv0_val;
} }
} }
} }
@ -155,19 +318,19 @@ float ADCSensor::sample() {
return NAN; return NAN;
} }
uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); const int adc_half = 2048;
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); uint32_t c12 = std::min(raw12, adc_half);
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); uint32_t c6 = adc_half - std::abs(raw6 - adc_half);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); uint32_t c2 = adc_half - std::abs(raw2 - adc_half);
uint32_t c0 = std::min(4095 - raw0, adc_half);
uint32_t c12 = std::min(raw12, ADC_HALF);
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
uint32_t csum = c12 + c6 + c2 + c0; uint32_t csum = c12 + c6 + c2 + c0;
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); if (csum == 0) {
return mv_scaled / (float) (csum * 1000U); ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
return NAN;
}
return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
} }
} // namespace adc } // namespace adc

View File

@ -10,13 +10,11 @@ from esphome.const import (
CONF_NUMBER, CONF_NUMBER,
CONF_PIN, CONF_PIN,
CONF_RAW, CONF_RAW,
CONF_WIFI,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.core import CORE from esphome.core import CORE
import esphome.final_validate as fv
from . import ( from . import (
ATTENUATION_MODES, ATTENUATION_MODES,
@ -24,6 +22,7 @@ from . import (
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
SAMPLING_MODES, SAMPLING_MODES,
adc_ns, adc_ns,
adc_unit_t,
validate_adc_pin, validate_adc_pin,
) )
@ -57,21 +56,6 @@ def validate_config(config):
return config return config
def final_validate_config(config):
if CORE.is_esp32:
variant = get_esp32_variant()
if (
CONF_WIFI in fv.full_config.get()
and config[CONF_PIN][CONF_NUMBER]
in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
):
raise cv.Invalid(
f"{variant} doesn't support ADC on this pin when Wi-Fi is configured"
)
return config
ADCSensor = adc_ns.class_( ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
) )
@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All(
validate_config, validate_config,
) )
FINAL_VALIDATE_SCHEMA = final_validate_config
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@ -119,13 +101,13 @@ async def to_code(config):
cg.add(var.set_sample_count(config[CONF_SAMPLES])) cg.add(var.set_sample_count(config[CONF_SAMPLES]))
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))
else:
cg.add(var.set_attenuation(attenuation))
if CORE.is_esp32: if CORE.is_esp32:
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))
else:
cg.add(var.set_attenuation(attenuation))
variant = get_esp32_variant() variant = get_esp32_variant()
pin_num = config[CONF_PIN][CONF_NUMBER] pin_num = config[CONF_PIN][CONF_NUMBER]
if ( if (
@ -133,10 +115,10 @@ async def to_code(config):
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
): ):
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel1(chan)) cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan))
elif ( elif (
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
): ):
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel2(chan)) cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))

View File

@ -86,8 +86,8 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
api_error_to_str(err), errno); errno);
return; return;
} }
this->client_info_ = helper_->getpeername(); this->client_info_ = helper_->getpeername();
@ -119,7 +119,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno); api_error_to_str(err), errno);
return; return;
} }
@ -136,14 +136,8 @@ void APIConnection::loop() {
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); errno);
} else if (err == APIError::CONNECTION_CLOSED) {
ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
@ -1435,6 +1429,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
} }
void APIConnection::complete_authentication_() {
// Early return if already authenticated
if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) {
return;
}
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
this->send_time_request();
}
#endif
}
HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info; this->client_info_ = msg.client_info;
this->client_peername_ = this->helper_->getpeername(); this->client_peername_ = this->helper_->getpeername();
@ -1450,7 +1462,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.name = App.get_name(); resp.name = App.get_name();
#ifdef USE_API_PASSWORD
// Password required - wait for authentication
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED);
#else
// No password configured - auto-authenticate
this->complete_authentication_();
#endif
return resp; return resp;
} }
ConnectResponse APIConnection::connect(const ConnectRequest &msg) { ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
@ -1463,23 +1482,14 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
// bool invalid_password = 1; // bool invalid_password = 1;
resp.invalid_password = !correct; resp.invalid_password = !correct;
if (correct) { if (correct) {
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->complete_authentication_();
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
this->send_time_request();
}
#endif
} }
return resp; return resp;
} }
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
DeviceInfoResponse resp{}; DeviceInfoResponse resp{};
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
resp.uses_password = this->parent_->uses_password(); resp.uses_password = true;
#else #else
resp.uses_password = false; resp.uses_password = false;
#endif #endif
@ -1598,7 +1608,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno); api_error_to_str(err), errno);
return false; return false;
} }
@ -1619,12 +1629,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); api_error_to_str(err), errno);
} else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
return false; return false;
} }
// Do not set last_traffic_ on send // Do not set last_traffic_ on send
@ -1632,11 +1638,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
} }
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
} }
void APIConnection::on_no_setup_connection() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@ -1801,12 +1807,8 @@ void APIConnection::process_batch_() {
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); errno);
} else {
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@ -273,6 +273,9 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected: protected:
// Helper function to handle authentication completion
void complete_authentication_();
// Helper function to fill common entity info fields // Helper function to fill common entity info fields
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types

File diff suppressed because it is too large Load Diff

View File

@ -219,8 +219,6 @@ void APIServer::dump_config() {
} }
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
bool APIServer::uses_password() const { return !this->password_.empty(); }
bool APIServer::check_password(const std::string &password) const { bool APIServer::check_password(const std::string &password) const {
// depend only on input password length // depend only on input password length
const char *a = this->password_.c_str(); const char *a = this->password_.c_str();
@ -428,7 +426,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGD(TAG, "Noise PSK saved"); ESP_LOGD(TAG, "Noise PSK saved");
if (make_active) { if (make_active) {
this->set_timeout(100, [this, psk]() { this->set_timeout(100, [this, psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(psk); this->set_noise_psk(psk);
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
c->send_message(DisconnectRequest()); c->send_message(DisconnectRequest());

View File

@ -39,7 +39,6 @@ class APIServer : public Component, public Controller {
bool teardown() override; bool teardown() override;
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
bool check_password(const std::string &password) const; bool check_password(const std::string &password) const;
bool uses_password() const;
void set_password(const std::string &password); void set_password(const std::string &password);
#endif #endif
void set_port(uint16_t port); void set_port(uint16_t port);

View File

@ -175,23 +175,7 @@ class Proto32Bit {
const uint32_t value_; const uint32_t value_;
}; };
class Proto64Bit { // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
public:
explicit Proto64Bit(uint64_t value) : value_(value) {}
uint64_t as_fixed64() const { return this->value_; }
int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); }
double as_double() const {
union {
uint64_t raw;
double value;
} s{};
s.raw = this->value_;
return s.value;
}
protected:
const uint64_t value_;
};
class ProtoWriteBuffer { class ProtoWriteBuffer {
public: public:
@ -258,20 +242,10 @@ class ProtoWriteBuffer {
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF); this->write((value >> 24) & 0xFF);
} }
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
if (value == 0 && !force) // not supported to reduce overhead on embedded systems. All ESPHome devices are
return; // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
// is needed in the future, the necessary encoding/decoding functions must be added.
this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
this->write((value >> 32) & 0xFF);
this->write((value >> 40) & 0xFF);
this->write((value >> 48) & 0xFF);
this->write((value >> 56) & 0xFF);
}
void encode_float(uint32_t field_id, float value, bool force = false) { void encode_float(uint32_t field_id, float value, bool force = false) {
if (value == 0.0f && !force) if (value == 0.0f && !force)
return; return;
@ -337,7 +311,7 @@ class ProtoMessage {
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } // NOTE: decode_64bit removed - wire type 1 not supported
}; };
class ProtoSize { class ProtoSize {
@ -662,33 +636,8 @@ class ProtoSize {
total_size += field_id_size + varint(value); total_size += field_id_size + varint(value);
} }
/** // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
* @brief Calculates and adds the size of a sint64 field to the total message size // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version)
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
total_size += field_id_size + varint(zigzag);
}
/** /**
* @brief Calculates and adds the size of a string/bytes field to the total message size * @brief Calculates and adds the size of a string/bytes field to the total message size

View File

@ -16,6 +16,8 @@ class UserServiceDescriptor {
virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0; virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
bool is_internal() { return false; }
}; };
template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);

View File

@ -3,8 +3,6 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/as3935/as3935.h" #include "esphome/components/as3935/as3935.h"
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome { namespace esphome {
namespace as3935_spi { namespace as3935_spi {

View File

@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny: if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/ESP32Async/AsyncTCP # https://github.com/ESP32Async/AsyncTCP
cg.add_library("ESP32Async/AsyncTCP", "3.4.4") cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
elif CORE.is_esp8266: elif CORE.is_esp8266:
# https://github.com/ESP32Async/ESPAsyncTCP # https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")

View File

@ -3,6 +3,7 @@
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"

View File

@ -20,14 +20,16 @@ adjusted_ids = set()
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.ensure_list( cv.ensure_list(
{ cv.COMPONENT_SCHEMA.extend(
cv.GenerateID(): cv.declare_id(EspLdo), {
cv.Required(CONF_VOLTAGE): cv.All( cv.GenerateID(): cv.declare_id(EspLdo),
cv.voltage, cv.float_range(min=0.5, max=2.7) cv.Required(CONF_VOLTAGE): cv.All(
), cv.voltage, cv.float_range(min=0.5, max=2.7)
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), ),
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
} cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
}
)
), ),
cv.only_with_esp_idf, cv.only_with_esp_idf,
only_on_variant(supported=[VARIANT_ESP32P4]), only_on_variant(supported=[VARIANT_ESP32P4]),

View File

@ -17,6 +17,9 @@ class EspLdo : public Component {
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
void set_voltage(float voltage) { this->voltage_ = voltage; } void set_voltage(float voltage) { this->voltage_ = voltage; }
void adjust_voltage(float voltage); void adjust_voltage(float voltage);
float get_setup_priority() const override {
return setup_priority::BUS; // LDO setup should be done early
}
protected: protected:
int channel_; int channel_;

View File

@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
return {}; return {};
} }
void Fan::save_state_() { void Fan::save_state_() {
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
return;
}
FanRestoreState state{}; FanRestoreState state{};
state.state = this->state; state.state = this->state;
state.oscillating = this->oscillating; state.oscillating = this->oscillating;

View File

@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) {
container.reset(); // Release ownership of the container's shared_ptr container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool { valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; return false;
} }
@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
this_update->update_info_.latest_version = root["version"].as<std::string>(); this_update->update_info_.latest_version = root["version"].as<std::string>();
for (auto build : root["builds"].as<JsonArray>()) { for (auto build : root["builds"].as<JsonArray>()) {
if (!build.containsKey("chipFamily")) { if (!build["chipFamily"].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; return false;
} }
if (build["chipFamily"] == ESPHOME_VARIANT) { if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!build.containsKey("ota")) { if (!build["ota"].is<JsonObject>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; return false;
} }
auto ota = build["ota"]; JsonObject ota = build["ota"].as<JsonObject>();
if (!ota.containsKey("path") || !ota.containsKey("md5")) { if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; return false;
} }
this_update->update_info_.firmware_url = ota["path"].as<std::string>(); this_update->update_info_.firmware_url = ota["path"].as<std::string>();
this_update->update_info_.md5 = ota["md5"].as<std::string>(); this_update->update_info_.md5 = ota["md5"].as<std::string>();
if (ota.containsKey("summary")) if (ota["summary"].is<const char *>())
this_update->update_info_.summary = ota["summary"].as<std::string>(); this_update->update_info_.summary = ota["summary"].as<std::string>();
if (ota.containsKey("release_url")) if (ota["release_url"].is<const char *>())
this_update->update_info_.release_url = ota["release_url"].as<std::string>(); this_update->update_info_.release_url = ota["release_url"].as<std::string>();
return true; return true;

View File

@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(1.0) @coroutine_with_priority(1.0)
async def to_code(config): async def to_code(config):
cg.add_library("bblanchon/ArduinoJson", "6.18.5") cg.add_library("bblanchon/ArduinoJson", "7.4.2")
cg.add_define("USE_JSON") cg.add_define("USE_JSON")
cg.add_global(json_ns.using) cg.add_global(json_ns.using)

View File

@ -1,83 +1,76 @@
#include "json_util.h" #include "json_util.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
namespace esphome { namespace esphome {
namespace json { namespace json {
static const char *const TAG = "json"; static const char *const TAG = "json";
static std::vector<char> global_json_build_buffer; // NOLINT // Build an allocator for the JSON Library using the RAMAllocator class
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL); struct SpiRamAllocator : ArduinoJson::Allocator {
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
void deallocate(void *pointer) override {
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
// RAMAllocator::deallocate() requires the size, which we don't have access to here.
// RAMAllocator::deallocate implementation just calls free() regardless of whether
// the memory was allocated with heap_caps_malloc or malloc.
// This is safe because ESP-IDF's heap implementation internally tracks the memory region
// and routes free() to the appropriate heap.
free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
}
void *reallocate(void *ptr, size_t new_size) override {
return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
}
protected:
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
};
std::string build_json(const json_build_t &f) { std::string build_json(const json_build_t &f) {
// Here we are allocating up to 5kb of memory, // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
// with the heap size minus 2kb to be safe if less than 5kb auto doc_allocator = SpiRamAllocator();
// as we can not have a true dynamic sized document. JsonDocument json_document(&doc_allocator);
// The excess memory is freed below with `shrinkToFit()` if (json_document.overflowed()) {
auto free_heap = ALLOCATOR.get_max_free_block_size(); ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
size_t request_size = std::min(free_heap, (size_t) 512); return "{}";
while (true) {
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes",
request_size, free_heap);
return "{}";
}
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
if (request_size == free_heap) {
ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes",
free_heap);
return "{}";
}
request_size = std::min(request_size * 2, free_heap);
continue;
}
json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
std::string output;
serializeJson(json_document, output);
return output;
} }
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return "{}";
}
std::string output;
serializeJson(json_document, output);
return output;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
bool parse_json(const std::string &data, const json_parse_t &f) { bool parse_json(const std::string &data, const json_parse_t &f) {
// Here we are allocating 1.5 times the data size, // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
// with the heap size minus 2kb to be safe if less than that auto doc_allocator = SpiRamAllocator();
// as we can not have a true dynamic sized document. JsonDocument json_document(&doc_allocator);
// The excess memory is freed below with `shrinkToFit()` if (json_document.overflowed()) {
auto free_heap = ALLOCATOR.get_max_free_block_size(); ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); return false;
while (true) { }
DynamicJsonDocument json_document(request_size); DeserializationError err = deserializeJson(json_document, data);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size,
free_heap);
return false;
}
DeserializationError err = deserializeJson(json_document, data);
json_document.shrinkToFit();
JsonObject root = json_document.as<JsonObject>(); JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) { if (err == DeserializationError::Ok) {
return f(root); return f(root);
} else if (err == DeserializationError::NoMemory) { } else if (err == DeserializationError::NoMemory) {
if (request_size * 2 >= free_heap) { ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); return false;
return false; }
} ESP_LOGE(TAG, "Parse error: %s", err.c_str());
ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2;
continue;
} else {
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
}
};
return false; return false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
} // namespace json } // namespace json

View File

@ -9,6 +9,7 @@ namespace light {
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
void LightJSONSchema::dump_json(LightState &state, JsonObject root) { void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (state.supports_effects()) if (state.supports_effects())
root["effect"] = state.get_effect_name(); root["effect"] = state.get_effect_name();
@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
if (values.get_color_mode() & ColorCapability::BRIGHTNESS) if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
root["brightness"] = uint8_t(values.get_brightness() * 255); root["brightness"] = uint8_t(values.get_brightness() * 255);
JsonObject color = root.createNestedObject("color"); JsonObject color = root["color"].to<JsonObject>();
if (values.get_color_mode() & ColorCapability::RGB) { if (values.get_color_mode() & ColorCapability::RGB) {
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
} }
void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) {
if (root.containsKey("state")) { if (root["state"].is<const char *>()) {
auto val = parse_on_off(root["state"]); auto val = parse_on_off(root["state"]);
switch (val) { switch (val) {
case PARSE_ON: case PARSE_ON:
@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
} }
} }
if (root.containsKey("brightness")) { if (root["brightness"].is<uint8_t>()) {
call.set_brightness(float(root["brightness"]) / 255.0f); call.set_brightness(float(root["brightness"]) / 255.0f);
} }
if (root.containsKey("color")) { if (root["color"].is<JsonObject>()) {
JsonObject color = root["color"]; JsonObject color = root["color"];
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
float max_rgb = 0.0f; float max_rgb = 0.0f;
if (color.containsKey("r")) { if (color["r"].is<uint8_t>()) {
float r = float(color["r"]) / 255.0f; float r = float(color["r"]) / 255.0f;
max_rgb = fmaxf(max_rgb, r); max_rgb = fmaxf(max_rgb, r);
call.set_red(r); call.set_red(r);
} }
if (color.containsKey("g")) { if (color["g"].is<uint8_t>()) {
float g = float(color["g"]) / 255.0f; float g = float(color["g"]) / 255.0f;
max_rgb = fmaxf(max_rgb, g); max_rgb = fmaxf(max_rgb, g);
call.set_green(g); call.set_green(g);
} }
if (color.containsKey("b")) { if (color["b"].is<uint8_t>()) {
float b = float(color["b"]) / 255.0f; float b = float(color["b"]) / 255.0f;
max_rgb = fmaxf(max_rgb, b); max_rgb = fmaxf(max_rgb, b);
call.set_blue(b); call.set_blue(b);
} }
if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) {
call.set_color_brightness(max_rgb); call.set_color_brightness(max_rgb);
} }
if (color.containsKey("c")) { if (color["c"].is<uint8_t>()) {
call.set_cold_white(float(color["c"]) / 255.0f); call.set_cold_white(float(color["c"]) / 255.0f);
} }
if (color.containsKey("w")) { if (color["w"].is<uint8_t>()) {
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
// white channel in RGBWW. // white channel in RGBWW.
if (color.containsKey("c")) { if (color["c"].is<uint8_t>()) {
call.set_warm_white(float(color["w"]) / 255.0f); call.set_warm_white(float(color["w"]) / 255.0f);
} else { } else {
call.set_white(float(color["w"]) / 255.0f); call.set_white(float(color["w"]) / 255.0f);
@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
} }
} }
if (root.containsKey("white_value")) { // legacy API if (root["white_value"].is<uint8_t>()) { // legacy API
call.set_white(float(root["white_value"]) / 255.0f); call.set_white(float(root["white_value"]) / 255.0f);
} }
if (root.containsKey("color_temp")) { if (root["color_temp"].is<uint16_t>()) {
call.set_color_temperature(float(root["color_temp"])); call.set_color_temperature(float(root["color_temp"]));
} }
} }
@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
LightJSONSchema::parse_color_json(state, call, root); LightJSONSchema::parse_color_json(state, call, root);
if (root.containsKey("flash")) { if (root["flash"].is<uint32_t>()) {
auto length = uint32_t(float(root["flash"]) * 1000); auto length = uint32_t(float(root["flash"]) * 1000);
call.set_flash_length(length); call.set_flash_length(length);
} }
if (root.containsKey("transition")) { if (root["transition"].is<uint16_t>()) {
auto length = uint32_t(float(root["transition"]) * 1000); auto length = uint32_t(float(root["transition"]) * 1000);
call.set_transition_length(length); call.set_transition_length(length);
} }
if (root.containsKey("effect")) { if (root["effect"].is<const char *>()) {
const char *effect = root["effect"]; const char *effect = root["effect"];
call.set_effect(effect); call.set_effect(effect);
} }

View File

@ -21,6 +21,11 @@ from esphome.components.libretiny.const import (
COMPONENT_LN882X, COMPONENT_LN882X,
COMPONENT_RTL87XX, COMPONENT_RTL87XX,
) )
from esphome.components.zephyr import (
zephyr_add_cdc_acm,
zephyr_add_overlay,
zephyr_add_prj_conf,
)
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -41,6 +46,7 @@ from esphome.const import (
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_LN882X, PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2040, PLATFORM_RP2040,
PLATFORM_RTL87XX, PLATFORM_RTL87XX,
PlatformFramework, PlatformFramework,
@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG]
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
UART_SELECTION_NRF52 = [USB_CDC, UART0]
HARDWARE_UART_TO_UART_SELECTION = { HARDWARE_UART_TO_UART_SELECTION = {
UART0: logger_ns.UART_SELECTION_UART0, UART0: logger_ns.UART_SELECTION_UART0,
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
@ -167,6 +175,8 @@ def uart_selection(value):
return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value)
if CORE.is_host: if CORE.is_host:
raise cv.Invalid("Uart selection not valid for host platform") raise cv.Invalid("Uart selection not valid for host platform")
if CORE.is_nrf52:
return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value)
raise NotImplementedError raise NotImplementedError
@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_(
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
) )
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All(
bk72xx=DEFAULT, bk72xx=DEFAULT,
ln882x=DEFAULT, ln882x=DEFAULT,
rtl87xx=DEFAULT, rtl87xx=DEFAULT,
nrf52=USB_CDC,
): cv.All( ): cv.All(
cv.only_on( cv.only_on(
[ [
@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_BK72XX, PLATFORM_BK72XX,
PLATFORM_LN882X, PLATFORM_LN882X,
PLATFORM_RTL87XX, PLATFORM_RTL87XX,
PLATFORM_NRF52,
] ]
), ),
uart_selection, uart_selection,
@ -358,6 +371,15 @@ async def to_code(config):
except cv.Invalid: except cv.Invalid:
pass pass
if CORE.using_zephyr:
if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1:
zephyr_add_overlay("""&uart1 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == USB_CDC:
zephyr_add_prj_conf("UART_LINE_CTRL", True)
zephyr_add_cdc_acm(config, 0)
# Register at end for safe mode # Register at end for safe mode
await cg.register_component(log, config) await cg.register_component(log, config)
@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO, PlatformFramework.LN882X_ARDUINO,
}, },
"logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
"task_log_buffer.cpp": { "task_log_buffer.cpp": {
PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_IDF,

View File

@ -4,9 +4,9 @@
#include <memory> // For unique_ptr #include <memory> // For unique_ptr
#endif #endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace logger { namespace logger {
@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->main_task_ = xTaskGetCurrentTaskHandle(); this->main_task_ = xTaskGetCurrentTaskHandle();
#elif defined(USE_ZEPHYR)
this->main_task_ = k_current_get();
#endif #endif
} }
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
} }
#endif #endif
#ifndef USE_ZEPHYR
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
void Logger::loop() { void Logger::loop() {
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
@ -185,8 +188,13 @@ void Logger::loop() {
} }
opened = !opened; opened = !opened;
} }
#endif
this->process_messages_();
}
#endif
#endif #endif
void Logger::process_messages_() {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
// Process any buffered messages when available // Process any buffered messages when available
if (this->log_buffer_->has_messages()) { if (this->log_buffer_->has_messages()) {
@ -227,12 +235,11 @@ void Logger::loop() {
} }
#endif #endif
} }
#endif
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
#endif #endif

View File

@ -29,6 +29,11 @@
#include <driver/uart.h> #include <driver/uart.h>
#endif // USE_ESP_IDF #endif // USE_ESP_IDF
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
struct device;
#endif
namespace esphome { namespace esphome {
namespace logger { namespace logger {
@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = {
"VV", // VERY_VERBOSE "VV", // VERY_VERBOSE
}; };
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
* Advanced configuration (pin selection, etc) is not supported. * Advanced configuration (pin selection, etc) is not supported.
@ -82,7 +87,7 @@ enum UARTSelection : uint8_t {
UART_SELECTION_UART0_SWAP, UART_SELECTION_UART0_SWAP,
#endif // USE_ESP8266 #endif // USE_ESP8266
}; };
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR
/** /**
* @brief Logger component for all ESPHome logging. * @brief Logger component for all ESPHome logging.
@ -107,7 +112,7 @@ class Logger : public Component {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
void init_log_buffer(size_t total_buffer_size); void init_log_buffer(size_t total_buffer_size);
#endif #endif
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR)
void loop() override; void loop() override;
#endif #endif
/// Manually set the baud rate for serial, set to 0 to disable. /// Manually set the baud rate for serial, set to 0 to disable.
@ -122,7 +127,7 @@ class Logger : public Component {
#ifdef USE_ESP32 #ifdef USE_ESP32
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif #endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger. /// Get the UART used by the logger.
UARTSelection get_uart() const; UARTSelection get_uart() const;
@ -157,6 +162,7 @@ class Logger : public Component {
#endif #endif
protected: protected:
void process_messages_();
void write_msg_(const char *msg); void write_msg_(const char *msg);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
@ -164,7 +170,7 @@ class Logger : public Component {
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
va_list args, char *buffer, uint16_t *buffer_at, va_list args, char *buffer, uint16_t *buffer_at,
uint16_t buffer_size) { uint16_t buffer_size) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
#else #else
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
@ -231,7 +237,10 @@ class Logger : public Component {
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
Stream *hw_serial_{nullptr}; Stream *hw_serial_{nullptr};
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ZEPHYR)
const device *uart_dev_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
void *main_task_ = nullptr; // Only used for thread name identification void *main_task_ = nullptr; // Only used for thread name identification
#endif #endif
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -256,7 +265,7 @@ class Logger : public Component {
uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_at_{0};
uint16_t tx_buffer_size_{0}; uint16_t tx_buffer_size_{0};
uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR)
UARTSelection uart_{UART_SELECTION_UART0}; UARTSelection uart_{UART_SELECTION_UART0};
#endif #endif
#ifdef USE_LIBRETINY #ifdef USE_LIBRETINY
@ -268,9 +277,13 @@ class Logger : public Component {
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
const char *HOT get_thread_name_() { const char *HOT get_thread_name_() {
#ifdef USE_ZEPHYR
k_tid_t current_task = k_current_get();
#else
TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
#endif
if (current_task == main_task_) { if (current_task == main_task_) {
return nullptr; // Main task return nullptr; // Main task
} else { } else {
@ -278,6 +291,8 @@ class Logger : public Component {
return pcTaskGetName(current_task); return pcTaskGetName(current_task);
#elif defined(USE_LIBRETINY) #elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(current_task); return pcTaskGetTaskName(current_task);
#elif defined(USE_ZEPHYR)
return k_thread_name_get(current_task);
#endif #endif
} }
} }
@ -319,7 +334,7 @@ class Logger : public Component {
const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
if (thread_name != nullptr) { if (thread_name != nullptr) {
// Non-main task with thread name // Non-main task with thread name
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,

View File

@ -0,0 +1,88 @@
#ifdef USE_ZEPHYR
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "logger.h"
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
namespace esphome {
namespace logger {
static const char *const TAG = "logger";
void Logger::loop() {
#ifdef USE_LOGGER_USB_CDC
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
uint32_t dtr = 0;
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
/* Poll if the DTR flag was set, optional */
if (opened == dtr) {
return;
}
if (!opened) {
App.schedule_dump_config();
}
opened = !opened;
#endif
this->process_messages_();
}
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
break;
#endif
}
if (!device_is_ready(uart_dev)) {
ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_());
} else {
this->uart_dev_ = uart_dev;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};
const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; }
} // namespace logger
} // namespace esphome
#endif

View File

@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"]
DOMAIN = "mipi_spi" DOMAIN = "mipi_spi"
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
CONF_SPI_16 = "spi_16" CONF_SPI_16 = "spi_16"
CONF_PIXEL_MODE = "pixel_mode" CONF_PIXEL_MODE = "pixel_mode"
CONF_COLOR_DEPTH = "color_depth"
CONF_BUS_MODE = "bus_mode" CONF_BUS_MODE = "bus_mode"
CONF_USE_AXIS_FLIPS = "use_axis_flips" CONF_USE_AXIS_FLIPS = "use_axis_flips"
CONF_NATIVE_WIDTH = "native_width" CONF_NATIVE_WIDTH = "native_width"

View File

@ -3,11 +3,18 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.const import (
CONF_BYTE_ORDER,
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA from esphome.config_validation import ALLOW_EXTRA
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
CONF_BUFFER_SIZE,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
CONF_CS_PIN, CONF_CS_PIN,
CONF_DATA_RATE, CONF_DATA_RATE,
@ -24,19 +31,19 @@ from esphome.const import (
CONF_MODEL, CONF_MODEL,
CONF_OFFSET_HEIGHT, CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH, CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_ROTATION, CONF_ROTATION,
CONF_SWAP_XY, CONF_SWAP_XY,
CONF_TRANSFORM, CONF_TRANSFORM,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.core import TimePeriod from esphome.core import CORE, TimePeriod
from esphome.cpp_generator import TemplateArguments
from esphome.final_validate import full_config
from ..const import CONF_DRAW_ROUNDING
from ..lvgl.defines import CONF_COLOR_DEPTH
from . import ( from . import (
CONF_BUS_MODE, CONF_BUS_MODE,
CONF_DRAW_FROM_ORIGIN,
CONF_NATIVE_HEIGHT, CONF_NATIVE_HEIGHT,
CONF_NATIVE_WIDTH, CONF_NATIVE_WIDTH,
CONF_PIXEL_MODE, CONF_PIXEL_MODE,
@ -55,6 +62,7 @@ from .models import (
MADCTL_XFLIP, MADCTL_XFLIP,
MADCTL_YFLIP, MADCTL_YFLIP,
DriverChip, DriverChip,
adafruit,
amoled, amoled,
cyd, cyd,
ili, ili,
@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"]
LOGGER = logging.getLogger(DOMAIN) LOGGER = logging.getLogger(DOMAIN)
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
MipiSpi = mipi_spi_ns.class_( MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice)
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice MipiSpiBuffer = mipi_spi_ns.class_(
"MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice
) )
ColorOrder = display.display_ns.enum("ColorMode") ColorOrder = display.display_ns.enum("ColorMode")
ColorBitness = display.display_ns.enum("ColorBitness") ColorBitness = display.display_ns.enum("ColorBitness")
Model = mipi_spi_ns.enum("Model") Model = mipi_spi_ns.enum("Model")
PixelMode = mipi_spi_ns.enum("PixelMode")
BusType = mipi_spi_ns.enum("BusType")
COLOR_ORDERS = { COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR, MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
} }
COLOR_DEPTHS = { COLOR_DEPTHS = {
8: ColorBitness.COLOR_BITNESS_332, 8: PixelMode.PIXEL_MODE_8,
16: ColorBitness.COLOR_BITNESS_565, 16: PixelMode.PIXEL_MODE_16,
18: PixelMode.PIXEL_MODE_18,
} }
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
BusTypes = {
TYPE_SINGLE: BusType.BUS_TYPE_SINGLE,
TYPE_QUAD: BusType.BUS_TYPE_QUAD,
TYPE_OCTAL: BusType.BUS_TYPE_OCTAL,
}
DriverChip("CUSTOM", initsequence={}) DriverChip("CUSTOM")
MODELS = DriverChip.models MODELS = DriverChip.models
# These statements are noops, but serve to suppress linting of side-effect-only imports # This loop is a noop, but suppresses linting of side-effect-only imports
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit):
pass pass
PixelMode = mipi_spi_ns.enum("PixelMode")
PIXEL_MODE_18BIT = "18bit" DISPLAY_18BIT = "18bit"
PIXEL_MODE_16BIT = "16bit" DISPLAY_16BIT = "16bit"
PIXEL_MODES = { DISPLAY_PIXEL_MODES = {
PIXEL_MODE_16BIT: 0x55, DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16),
PIXEL_MODE_18BIT: 0x66, DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18),
} }
def get_dimensions(config):
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
return width, height, offset_width, offset_height
(width, height) = dimensions
return width, height, 0, 0
# Default dimensions, use model defaults
transform = get_transform(config)
model = MODELS[config[CONF_MODEL]]
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def denominator(config):
"""
Calculate the best denominator for a buffer size fraction.
The denominator must be a number between 2 and 16 that divides the display height evenly,
and the fraction represented by the denominator must be less than or equal to the given fraction.
:config: The configuration dictionary containing the buffer size fraction and display dimensions
:return: The denominator to use for the buffer size fraction
"""
frac = config.get(CONF_BUFFER_SIZE)
if frac is None or frac > 0.75:
return 1
height, _width, _offset_width, _offset_height = get_dimensions(config)
try:
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
except StopIteration:
raise cv.Invalid(
f"Buffer size fraction {frac} is not compatible with display height {height}"
) from StopIteration
def validate_dimension(rounding): def validate_dimension(rounding):
def validator(value): def validator(value):
value = cv.positive_int(value) value = cv.positive_int(value)
@ -158,41 +235,50 @@ def dimension_schema(rounding):
) )
def model_schema(bus_mode, model: DriverChip, swapsies: bool): def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
transform = cv.Schema( transform = cv.Schema(
{ {
cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model),
} }
) )
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
transform = transform.extend(
{
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by this model"
)
}
)
else:
transform = transform.extend(
{
cv.Required(CONF_SWAP_XY): cv.boolean,
}
)
# CUSTOM model will need to provide a custom init sequence # CUSTOM model will need to provide a custom init sequence
iseqconf = ( iseqconf = (
cv.Required(CONF_INIT_SEQUENCE) cv.Required(CONF_INIT_SEQUENCE)
if model.initsequence is None if model.initsequence is None
else cv.Optional(CONF_INIT_SEQUENCE) else cv.Optional(CONF_INIT_SEQUENCE)
) )
# Dimensions are optional if the model has a default width and the transform is not overridden # Dimensions are optional if the model has a default width and the x-y transform is not overridden
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
cv_dimensions = ( cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
) )
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,)
color_depth = ( color_depth = (
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
) )
other_options = [
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
if bus_mode == TYPE_SINGLE:
other_options.append(CONF_SPI_16)
schema = ( schema = (
display.FULL_DISPLAY_SCHEMA.extend( display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema( spi.spi_device_schema(
@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
COLOR_ORDERS, upper=True COLOR_ORDERS, upper=True
), ),
model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of(
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 2): power_of_two, model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
cv.one_of(*pixel_modes, lower=True), *pixel_modes, lower=True
cv.int_range(0, 255, min_included=True, max_included=True),
), ),
cv.Optional(CONF_TRANSFORM): transform, cv.Optional(CONF_TRANSFORM): transform,
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
), ),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
iseqconf: cv.ensure_list(map_sequence), iseqconf: cv.ensure_list(map_sequence),
cv.Optional(CONF_BUFFER_SIZE): cv.All(
cv.percentage, cv.Range(0.12, 1.0)
),
} }
) )
.extend( .extend({model.option(x): cv.boolean for x in other_options})
{
model.option(x): cv.boolean
for x in [
CONF_DRAW_FROM_ORIGIN,
CONF_SPI_16,
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
}
)
) )
if brightness := model.get_default(CONF_BRIGHTNESS): if brightness := model.get_default(CONF_BRIGHTNESS):
schema = schema.extend( schema = schema.extend(
@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
return schema return schema
def rotation_as_transform(model, config): def is_rotation_transformable(config):
""" """
Check if a rotation can be implemented in hardware using the MADCTL register. Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
""" """
model = MODELS[config[CONF_MODEL]]
rotation = config.get(CONF_ROTATION, 0) rotation = config.get(CONF_ROTATION, 0)
return rotation and ( return rotation and (
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
) )
def config_schema(config): def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
# First get the model and bus mode # First get the model and bus mode
config = cv.Schema( config = cv.Schema(
{ {
@ -288,29 +376,94 @@ def config_schema(config):
extra=ALLOW_EXTRA, extra=ALLOW_EXTRA,
)(config) )(config)
bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True config = model_schema(config)(config)
config = model_schema(bus_mode, model, swapsies)(config)
# Check for invalid combinations of MADCTL config # Check for invalid combinations of MADCTL config
if init_sequence := config.get(CONF_INIT_SEQUENCE): if init_sequence := config.get(CONF_INIT_SEQUENCE):
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: commands = [x[0] for x in init_sequence]
if MADCTL in commands and CONF_TRANSFORM in config:
raise cv.Invalid( raise cv.Invalid(
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
) )
if PIXFMT in commands:
raise cv.Invalid(
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
)
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
raise cv.Invalid("DC pin is not supported in quad mode") raise cv.Invalid("DC pin is not supported in quad mode")
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
raise cv.Invalid(f"DC pin is required in {bus_mode} mode") raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
denominator(config)
return config return config
CONFIG_SCHEMA = config_schema CONFIG_SCHEMA = customise_schema
def get_transform(model, config): def requires_buffer(config):
can_transform = rotation_as_transform(model, config) """
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
:param config:
:return: True if a buffer is required, False otherwise
"""
return any(
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
)
def get_color_depth(config):
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
if "psram" not in global_config and CONF_BUFFER_SIZE not in config:
if not requires_buffer(config):
return config # No buffer needed, so no need to set a buffer size
# If PSRAM is not enabled, choose a small buffer size by default
if not requires_buffer(config):
# not our problem.
return config
color_depth = get_color_depth(config)
frac = denominator(config)
height, width, _offset_width, _offset_height = get_dimensions(config)
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB
fraction = 20000.0 / buffer_size
try:
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
)
except StopIteration:
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
# PSRAM will be needed.
if CORE.is_esp32:
raise cv.Invalid(
"PSRAM is required for this display"
) from StopIteration
return config
FINAL_VALIDATE_SCHEMA = _final_validate
def get_transform(config):
"""
Get the transformation configuration for the display.
:param config:
:return:
"""
model = MODELS[config[CONF_MODEL]]
can_transform = is_rotation_transformable(config)
transform = config.get( transform = config.get(
CONF_TRANSFORM, CONF_TRANSFORM,
{ {
@ -350,16 +503,13 @@ def get_sequence(model, config):
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
commands = [x[0] for x in sequence] commands = [x[0] for x in sequence]
# Set pixel format if not already in the custom sequence # Set pixel format if not already in the custom sequence
if PIXFMT not in commands: pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]]
pixel_mode = config[CONF_PIXEL_MODE] sequence.append((PIXFMT, pixel_mode[0]))
if not isinstance(pixel_mode, int):
pixel_mode = PIXEL_MODES[pixel_mode]
sequence.append((PIXFMT, pixel_mode))
# Does the chip use the flipping bits for mirroring rather than the reverse order bits? # Does the chip use the flipping bits for mirroring rather than the reverse order bits?
use_flip = config[CONF_USE_AXIS_FLIPS] use_flip = config[CONF_USE_AXIS_FLIPS]
if MADCTL not in commands: if MADCTL not in commands:
madctl = 0 madctl = 0
transform = get_transform(model, config) transform = get_transform(config)
if transform.get(CONF_TRANSFORM): if transform.get(CONF_TRANSFORM):
LOGGER.info("Using hardware transform to implement rotation") LOGGER.info("Using hardware transform to implement rotation")
if transform.get(CONF_MIRROR_X): if transform.get(CONF_MIRROR_X):
@ -396,63 +546,62 @@ def get_sequence(model, config):
) )
def get_instance(config):
"""
Get the type of MipiSpi instance to create based on the configuration,
and the template arguments.
:param config:
:return: type, template arguments
"""
width, height, offset_width, offset_height = get_dimensions(config)
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
bufferpixels = COLOR_DEPTHS[color_depth]
display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1]
bus_type = config[CONF_BUS_MODE]
if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False):
# If the bus mode is single and spi_16 is set, use single 16-bit mode
bus_type = BusType.BUS_TYPE_SINGLE_16
else:
bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
rotation = DISPLAY_ROTATIONS[
0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0)
]
templateargs = [
buffer_type,
bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode,
bus_type,
width,
height,
offset_width,
offset_height,
]
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.append(rotation)
templateargs.append(frac)
return MipiSpiBuffer, templateargs
return MipiSpi, templateargs
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
transform = get_transform(model, config) var_id = config[CONF_ID]
if CONF_DIMENSIONS in config: var_id.type, templateargs = get_instance(config)
# Explicit dimensions, just use as is var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
else:
(width, height) = dimensions
offset_width = 0
offset_height = 0
else:
# Default dimensions, use model defaults and transform if needed
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(
CONF_NATIVE_WIDTH, width + offset_width * 2
)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
color_depth = config[CONF_COLOR_DEPTH]
if color_depth.endswith("bit"):
color_depth = color_depth[:-3]
color_depth = COLOR_DEPTHS[int(color_depth)]
var = cg.new_Pvariable(
config[CONF_ID], width, height, offset_width, offset_height, color_depth
)
cg.add(var.set_init_sequence(get_sequence(model, config))) cg.add(var.set_init_sequence(get_sequence(model, config)))
if rotation_as_transform(model, config): if is_rotation_transformable(config):
if CONF_TRANSFORM in config: if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else: else:
config[CONF_ROTATION] = 0 config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
cg.add(var.set_spi_16(config[CONF_SPI_16]))
if enable_pin := config.get(CONF_ENABLE_PIN): if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
cg.add(var.set_enable_pins(enable)) cg.add(var.set_enable_pins(enable))
@ -472,4 +621,5 @@ async def to_code(config):
cg.add(var.set_writer(lambda_)) cg.add(var.set_writer(lambda_))
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
# Displays are write-only, set the SPI device to write-only as well
cg.add(var.set_write_only(True)) cg.add(var.set_write_only(True))

View File

@ -2,489 +2,5 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace mipi_spi { namespace mipi_spi {} // namespace mipi_spi
void MipiSpi::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
this->bus_width_ = this->parent_->get_bus_width();
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
ESP_LOGD(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case PIXFMT:
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->setup_complete_ = true;
if (this->draw_from_origin_)
check_buffer_();
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::update() {
if (!this->setup_complete_ || this->is_failed()) {
return;
}
this->do_update_();
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
if (this->draw_from_origin_) {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_ - 1;
}
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
this->width_ - w - this->x_low_);
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
this->x_high_ = 0;
this->y_high_ = 0;
}
void MipiSpi::fill(Color color) {
if (!this->check_buffer_())
return;
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->get_height_internal() - 1;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
break;
}
default: {
auto new_color = display::ColorUtil::color_to_565(color);
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
} else {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
auto len = this->buffer_bytes_ / 2;
while (len--) {
*ptr_16++ = new_color;
}
}
}
}
}
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
if (!this->check_buffer_())
return;
size_t pos = (y * this->width_) + x;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
uint8_t new_color = display::ColorUtil::color_to_332(color);
if (this->buffer_[pos] == new_color)
return;
this->buffer_[pos] = new_color;
break;
}
case display::COLOR_BITNESS_565: {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
if (ptr_16[pos] == new_color)
return;
ptr_16[pos] = new_color;
break;
}
default:
return;
}
// low and high watermark may speed up drawing from buffer
if (x < this->x_low_)
this->x_low_ = x;
if (y < this->y_low_)
this->y_low_ = y;
if (x > this->x_high_)
this->x_high_ = x;
if (y > this->y_high_)
this->y_high_ = y;
}
void MipiSpi::reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
void MipiSpi::write_init_sequence_() {
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
const auto *ptr = vec.data() + index;
this->write_command_(cmd, ptr, num_args);
index += num_args;
}
}
this->setup_complete_ = true;
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += this->offset_width_;
x2 += this->offset_width_;
y1 += this->offset_height_;
y2 += this->offset_height_;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
}
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (!this->setup_complete_ || this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
if (this->draw_from_origin_) {
auto stride = x_offset + w + x_pad;
for (int y = 0; y != h; y++) {
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
}
ptr = this->buffer_;
w = this->width_;
h += y_start;
x_start = 0;
y_start = 0;
x_offset = 0;
y_offset = 0;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
// deal with byte swapping
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = color_val & 0xE0; // Red
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
transfer_buffer[idx++] = color_val << 6; // Blue
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
transfer_buffer[idx++] = (color_val & 0x3) << 3;
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
auto stride = x_offset + w + x_pad;
const auto *offset_ptr = ptr;
if (this->color_depth_ == display::COLOR_BITNESS_332) {
offset_ptr += y_offset * stride + x_offset;
} else {
stride *= 2;
offset_ptr += y_offset * stride + x_offset * 2;
}
switch (this->bus_width_) {
case 4:
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
// bother
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
} else {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
offset_ptr += stride;
}
}
break;
case 8:
this->write_command_(WDATA);
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
} else {
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
offset_ptr += stride;
}
}
break;
default:
this->write_command_(WDATA);
this->enable();
if (this->color_depth_ == display::COLOR_BITNESS_565) {
// Source buffer is 16-bit RGB565
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB565 to RGB666
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
} else {
// Direct RGB565 output
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_array(ptr, w * h * 2);
} else {
for (int y = 0; y != h; y++) {
this->write_array(offset_ptr, w * 2);
offset_ptr += stride;
}
}
}
} else {
// Source buffer is 8-bit RGB332
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB332 to RGB666
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
} else {
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
}
break;
}
}
this->disable();
}
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if (this->bus_width_ == 4) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if (this->bus_width_ == 8) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
if (this->spi_16_) {
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
} else {
this->enable();
this->write_array(bytes, len);
this->disable();
}
}
}
}
void MipiSpi::dump_config() {
ESP_LOGCONFIG(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %u\n"
" Height: %u",
this->model_, this->width_, this->height_);
if (this->offset_width_ != 0)
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
if (this->offset_height_ != 0)
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
ESP_LOGCONFIG(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Color depth: %d bits\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Pixel mode: %s",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)),
this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
if (this->brightness_.has_value())
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
if (this->spi_16_)
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
if (this->draw_from_origin_)
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
ESP_LOGCONFIG(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_);
}
} // namespace mipi_spi
} // namespace esphome } // namespace esphome

View File

@ -4,40 +4,39 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h" #include "esphome/components/display/display_color_utils.h"
namespace esphome { namespace esphome {
namespace mipi_spi { namespace mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi"; constexpr static const char *const TAG = "display.mipi_spi";
static const uint8_t SW_RESET_CMD = 0x01; static constexpr uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11; static constexpr uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13; static constexpr uint8_t NORON = 0x13;
static const uint8_t INVERT_OFF = 0x20; static constexpr uint8_t INVERT_OFF = 0x20;
static const uint8_t INVERT_ON = 0x21; static constexpr uint8_t INVERT_ON = 0x21;
static const uint8_t ALL_ON = 0x23; static constexpr uint8_t ALL_ON = 0x23;
static const uint8_t WRAM = 0x24; static constexpr uint8_t WRAM = 0x24;
static const uint8_t MIPI = 0x26; static constexpr uint8_t MIPI = 0x26;
static const uint8_t DISPLAY_ON = 0x29; static constexpr uint8_t DISPLAY_ON = 0x29;
static const uint8_t RASET = 0x2B; static constexpr uint8_t RASET = 0x2B;
static const uint8_t CASET = 0x2A; static constexpr uint8_t CASET = 0x2A;
static const uint8_t WDATA = 0x2C; static constexpr uint8_t WDATA = 0x2C;
static const uint8_t TEON = 0x35; static constexpr uint8_t TEON = 0x35;
static const uint8_t MADCTL_CMD = 0x36; static constexpr uint8_t MADCTL_CMD = 0x36;
static const uint8_t PIXFMT = 0x3A; static constexpr uint8_t PIXFMT = 0x3A;
static const uint8_t BRIGHTNESS = 0x51; static constexpr uint8_t BRIGHTNESS = 0x51;
static const uint8_t SWIRE1 = 0x5A; static constexpr uint8_t SWIRE1 = 0x5A;
static const uint8_t SWIRE2 = 0x5B; static constexpr uint8_t SWIRE2 = 0x5B;
static const uint8_t PAGESEL = 0xFE; static constexpr uint8_t PAGESEL = 0xFE;
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static const uint8_t DELAY_FLAG = 0xFF; static const uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian. // store a 16 bit value in a buffer, big endian.
@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[1] = value; buf[1] = value;
} }
// Buffer mode, conveniently also the number of bytes in a pixel
enum PixelMode { enum PixelMode {
PIXEL_MODE_16, PIXEL_MODE_8 = 1,
PIXEL_MODE_18, PIXEL_MODE_16 = 2,
PIXEL_MODE_18 = 3,
}; };
class MipiSpi : public display::DisplayBuffer, enum BusType {
BUS_TYPE_SINGLE = 1,
BUS_TYPE_QUAD = 4,
BUS_TYPE_OCTAL = 8,
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
};
/**
* Base class for MIPI SPI displays.
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* buffer
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
class MipiSpi : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> { spi::DATA_RATE_1MHZ> {
public: public:
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) MipiSpi() {}
: width_(width), void update() override { this->stop_poller(); }
height_(height), void draw_pixel_at(int x, int y, Color color) override {}
offset_width_(offset_width),
offset_height_(offset_height),
color_depth_(color_depth) {}
void set_model(const char *model) { this->model_ = model; } void set_model(const char *model) { this->model_ = model; }
void update() override;
void setup() override;
display::ColorOrder get_color_mode() {
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
}
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); } void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer,
this->brightness_ = brightness; this->brightness_ = brightness;
this->reset_params_(); this->reset_params_();
} }
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void dump_config() override;
int get_width_internal() override { return this->width_; } int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return this->height_; } int get_height_internal() override { return HEIGHT; }
bool can_proceed() override { return this->setup_complete_; }
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
// reset the display, and write the init sequence
void setup() override {
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
esph_log_d(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
esph_log_d(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
// init sequence no longer needed
this->init_sequence_.clear();
}
// Drawing operations
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
if (this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
// note that the usual logging macros are banned in header files, so use their replacement
esph_log_e(TAG, "Unsupported color depth or bit order");
return;
}
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
x_pad);
}
void dump_config() override {
esph_log_config(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %u\n"
" Height: %u",
this->model_, WIDTH, HEIGHT);
if constexpr (OFFSET_WIDTH != 0)
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
if constexpr (OFFSET_HEIGHT != 0)
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
esph_log_config(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
" Endianness: %s\n",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
if (this->brightness_.has_value())
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
if (this->cs_ != nullptr)
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
if (this->reset_pin_ != nullptr)
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
if (this->dc_pin_ != nullptr)
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
esph_log_config(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
}
protected: protected:
bool check_buffer_() { /* METHODS */
if (this->is_failed()) // convenience functions to write commands with or without data
return false;
if (this->buffer_ != nullptr)
return true;
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
if (this->buffer_ == nullptr) {
this->mark_failed();
return false;
}
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
return true;
}
void fill(Color color) override;
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
/**
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
* sample code.)
*
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
* sent in 1-dataline SPI. The second indicates quad mode.
* 1: 0x00
* 2: The command (register address) byte.
* 3: 0x00
*
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
* At the conclusion of the write, de-assert /CS.
*
* @param cmd
* @param bytes
* @param len
*/
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
void reset_params_();
void write_init_sequence_();
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
// Writes a command to the display, with the given bytes.
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_array(bytes, len);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
}
}
// write changed parameters to the display
void reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
// set the address window for the next data write
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += OFFSET_WIDTH;
x2 += OFFSET_WIDTH;
y1 += OFFSET_HEIGHT;
y2 += OFFSET_HEIGHT;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
if constexpr (BUS_TYPE != BUS_TYPE_QUAD) {
this->write_command_(WDATA);
}
}
// map the display color bitness to the pixel mode
static PixelMode get_pixel_mode(display::ColorBitness bitness) {
switch (bitness) {
case display::COLOR_BITNESS_888:
return PIXEL_MODE_18; // 18 bits per pixel
case display::COLOR_BITNESS_565:
return PIXEL_MODE_16; // 16 bits per pixel
default:
return PIXEL_MODE_8; // Default to 8 bits per pixel
}
}
/**
* Writes a buffer to the display.
* @param w Width of each line in bytes
* @param h Height of the buffer in rows
* @param pad Padding in bytes after each line
*/
void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) {
if (pad == 0) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w * h);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
}
} else {
for (size_t y = 0; y != h; y++) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8);
}
ptr += w + pad;
}
}
}
/**
* Writes a buffer to the display.
*
* The ptr is a pointer to the pixel data
* The other parameters are all in pixel units.
*/
void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
this->enable();
ptr += y_offset * (x_offset + w + x_pad) + x_offset;
if constexpr (BUFFERPIXEL == DISPLAYPIXEL) {
this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h,
x_pad * sizeof(BUFFERTYPE));
} else {
// type conversion required, do it in chunks
uint8_t dbuffer[DISPLAYPIXEL * 48];
uint8_t *dptr = dbuffer;
auto stride = x_offset + w + x_pad; // stride in pixels
for (size_t y = 0; y != h; y++) {
for (size_t x = 0; x != w; x++) {
auto color_val = ptr[y * stride + x];
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
// 16 to 18 bit conversion
if constexpr (IS_BIG_ENDIAN) {
*dptr++ = color_val & 0xF8;
*dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11;
*dptr++ = (color_val >> 5) & 0xF8;
} else {
*dptr++ = (color_val >> 8) & 0xF8; // Blue
*dptr++ = (color_val & 0x7E0) >> 3;
*dptr++ = color_val << 3;
}
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) {
// 8 bit to 18 bit conversion
*dptr++ = color_val << 6; // Blue
*dptr++ = (color_val & 0x1C) << 3; // Green
*dptr++ = (color_val & 0xE0); // Red
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) {
if constexpr (IS_BIG_ENDIAN) {
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
*dptr++ = (color_val & 3) << 3;
} else {
*dptr++ = (color_val & 3) << 3;
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
}
}
// buffer full? Flush.
if (dptr == dbuffer + sizeof(dbuffer)) {
this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0);
dptr = dbuffer;
}
}
}
// flush any remaining data
if (dptr != dbuffer) {
this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0);
}
}
this->disable();
}
/* PROPERTIES */
// GPIO pins
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{nullptr};
std::vector<GPIOPin *> enable_pins_{}; std::vector<GPIOPin *> enable_pins_{};
GPIOPin *dc_pin_{nullptr}; GPIOPin *dc_pin_{nullptr};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
uint16_t y_high_{0};
bool setup_complete_{};
// other properties set by configuration
bool invert_colors_{}; bool invert_colors_{};
size_t width_;
size_t height_;
int16_t offset_width_;
int16_t offset_height_;
size_t buffer_bytes_{0};
display::ColorBitness color_depth_;
PixelMode pixel_mode_{PIXEL_MODE_16};
uint8_t bus_width_{};
bool spi_16_{};
uint8_t madctl_{};
bool draw_from_origin_{false};
unsigned draw_rounding_{2}; unsigned draw_rounding_{2};
optional<uint8_t> brightness_{}; optional<uint8_t> brightness_{};
const char *model_{"Unknown"}; const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{}; std::vector<uint8_t> init_sequence_{};
uint8_t madctl_{};
}; };
/**
* Class for MIPI SPI displays with a buffer.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam ROTATION The rotation of the display
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
OFFSET_WIDTH, OFFSET_HEIGHT> {
public:
MipiSpiBuffer() { this->rotation_ = ROTATION; }
void dump_config() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::dump_config();
esph_log_config(TAG,
" Rotation: %d°\n"
" Buffer pixels: %d bits\n"
" Buffer fraction: 1/%d\n"
" Buffer bytes: %zu\n"
" Draw rounding: %u",
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
this->draw_rounding_);
}
void setup() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::setup();
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed("Buffer allocation failed");
}
}
void update() override {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto now = millis();
#endif
if (this->is_failed()) {
return;
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
if (this->auto_clear_enabled_) {
this->clear();
}
if (this->page_ != nullptr) {
this->page_->get_writer()(*this);
} else if (this->writer_.has_value()) {
(*this->writer_)(*this);
} else {
this->test_card();
}
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap);
lap = millis();
#endif
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_, WIDTH - w);
// invalidate watermarks
this->x_low_ = WIDTH;
this->y_low_ = HEIGHT;
this->x_high_ = 0;
this->y_high_ = 0;
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Write to display took %dms", millis() - lap);
lap = millis();
#endif
}
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Total update took %dms", millis() - now);
#endif
}
// Draw a pixel at the given coordinates.
void draw_pixel_at(int x, int y, Color color) override {
rotate_coordinates_(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
return;
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
if (x < this->x_low_) {
this->x_low_ = x;
}
if (x > this->x_high_) {
this->x_high_ = x;
}
if (y < this->y_low_) {
this->y_low_ = y;
}
if (y > this->y_high_) {
this->y_high_ = y;
}
}
// Fills the display with a color.
void fill(Color color) override {
this->x_low_ = 0;
this->y_low_ = this->start_line_;
this->x_high_ = WIDTH - 1;
this->y_high_ = this->end_line_ - 1;
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
}
int get_width() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
protected:
// Rotate the coordinates to match the display orientation.
void rotate_coordinates_(int &x, int &y) const {
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
// Convert a color to the buffer pixel format.
BUFFERTYPE convert_color_(Color &color) const {
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
if constexpr (IS_BIG_ENDIAN) {
return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5;
} else {
return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3;
}
}
return static_cast<BUFFERTYPE>(0);
}
BUFFERTYPE *buffer_{};
uint16_t x_low_{WIDTH};
uint16_t y_low_{HEIGHT};
uint16_t x_high_{0};
uint16_t y_high_{0};
uint16_t start_line_{0};
uint16_t end_line_{1};
};
} // namespace mipi_spi } // namespace mipi_spi
} // namespace esphome } // namespace esphome

View File

@ -0,0 +1,30 @@
from .ili import ST7789V
ST7789V.extend(
"ADAFRUIT-FUNHOUSE",
height=240,
width=240,
offset_height=0,
offset_width=0,
cs_pin=40,
dc_pin=39,
reset_pin=41,
invert_colors=True,
mirror_x=True,
mirror_y=True,
data_rate="80MHz",
)
ST7789V.extend(
"ADAFRUIT-S2-TFT-FEATHER",
height=240,
width=135,
offset_height=52,
offset_width=40,
cs_pin=7,
dc_pin=39,
reset_pin=40,
invert_colors=True,
)
models = {}

View File

@ -67,6 +67,14 @@ RM690B0 = DriverChip(
), ),
) )
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) T4_S3_AMOLED = RM690B0.extend(
"T4-S3",
width=450,
offset_width=16,
cs_pin=11,
reset_pin=13,
enable_pin=9,
bus_mode=TYPE_QUAD,
)
models = {} models = {}

View File

@ -1,3 +1,5 @@
import esphome.config_validation as cv
from . import DriverChip from . import DriverChip
from .ili import ILI9488_A from .ili import ILI9488_A
@ -128,6 +130,7 @@ DriverChip(
ILI9488_A.extend( ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5", "PICO-RESTOUCH-LCD-3.5",
swap_xy=cv.UNDEFINED,
spi_16=True, spi_16=True,
pixel_mode="16bit", pixel_mode="16bit",
mirror_x=True, mirror_x=True,

View File

@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
} }
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>();
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
if (acp_supported_features & ACP_FEAT_ARM_AWAY) { if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
supported_features.add("arm_away"); supported_features.add("arm_away");

View File

@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
} }
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->binary_sensor_->get_device_class().empty()) if (!this->binary_sensor_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
if (this->binary_sensor_->is_status_binary_sensor()) if (this->binary_sensor_->is_status_binary_sensor())

View File

@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() {
} }
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
config.state_topic = false; config.state_topic = false;
if (!this->button_->get_device_class().empty()) if (!this->button_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
std::string MQTTButtonComponent::component_type() const { return "button"; } std::string MQTTButtonComponent::component_type() const { return "button"; }

View File

@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() {
std::string topic = "esphome/discover/"; std::string topic = "esphome/discover/";
topic.append(App.get_name()); topic.append(App.get_name());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
this->publish_json( this->publish_json(
topic, topic,
[](JsonObject root) { [](JsonObject root) {
@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() {
#endif #endif
}, },
2, this->discovery_info_.retain); 2, this->discovery_info_.retain);
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
void MQTTClientComponent::dump_config() { void MQTTClientComponent::dump_config() {

View File

@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
using namespace esphome::climate; using namespace esphome::climate;
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto traits = this->device_->get_traits(); auto traits = this->device_->get_traits();
// current_temperature_topic // current_temperature_topic
if (traits.get_supports_current_temperature()) { if (traits.get_supports_current_temperature()) {
@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// mode_state_topic // mode_state_topic
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
// modes // modes
JsonArray modes = root.createNestedArray(MQTT_MODES); JsonArray modes = root[MQTT_MODES].to<JsonArray>();
// sort array for nice UI in HA // sort array for nice UI in HA
if (traits.supports_mode(CLIMATE_MODE_AUTO)) if (traits.supports_mode(CLIMATE_MODE_AUTO))
modes.add("auto"); modes.add("auto");
@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// preset_mode_state_topic // preset_mode_state_topic
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
// presets // presets
JsonArray presets = root.createNestedArray("preset_modes"); JsonArray presets = root["preset_modes"].to<JsonArray>();
if (traits.supports_preset(CLIMATE_PRESET_HOME)) if (traits.supports_preset(CLIMATE_PRESET_HOME))
presets.add("home"); presets.add("home");
if (traits.supports_preset(CLIMATE_PRESET_AWAY)) if (traits.supports_preset(CLIMATE_PRESET_AWAY))
@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// fan_mode_state_topic // fan_mode_state_topic
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
// fan_modes // fan_modes
JsonArray fan_modes = root.createNestedArray("fan_modes"); JsonArray fan_modes = root["fan_modes"].to<JsonArray>();
if (traits.supports_fan_mode(CLIMATE_FAN_ON)) if (traits.supports_fan_mode(CLIMATE_FAN_ON))
fan_modes.add("on"); fan_modes.add("on");
if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// swing_mode_state_topic // swing_mode_state_topic
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
// swing_modes // swing_modes
JsonArray swing_modes = root.createNestedArray("swing_modes"); JsonArray swing_modes = root["swing_modes"].to<JsonArray>();
if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
swing_modes.add("off"); swing_modes.add("off");
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
config.state_topic = false; config.state_topic = false;
config.command_topic = false; config.command_topic = false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
void MQTTClimateComponent::setup() { void MQTTClimateComponent::setup() {
auto traits = this->device_->get_traits(); auto traits = this->device_->get_traits();

View File

@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() {
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return global_mqtt_client->publish_json( return global_mqtt_client->publish_json(
this->get_discovery_topic_(discovery_info), this->get_discovery_topic_(discovery_info),
[this](JsonObject root) { [this](JsonObject root) {
@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() {
} }
std::string node_area = App.get_area(); std::string node_area = App.get_area();
JsonObject device_info = root.createNestedObject(MQTT_DEVICE); JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
const auto mac = get_mac_address(); const auto mac = get_mac_address();
device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
device_info[MQTT_DEVICE_NAME] = node_friendly_name; device_info[MQTT_DEVICE_NAME] = node_friendly_name;
@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
}, },
this->qos_, discovery_info.retain); this->qos_, discovery_info.retain);
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
uint8_t MQTTComponent::get_qos() const { return this->qos_; } uint8_t MQTTComponent::get_qos() const { return this->qos_; }

View File

@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() {
} }
} }
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->cover_->get_device_class().empty()) if (!this->cover_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();

View File

@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
void MQTTDateComponent::setup() { void MQTTDateComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->date_->make_call(); auto call = this->date_->make_call();
if (root.containsKey("year")) { if (root["year"].is<uint16_t>()) {
call.set_year(root["year"]); call.set_year(root["year"]);
} }
if (root.containsKey("month")) { if (root["month"].is<uint8_t>()) {
call.set_month(root["month"]); call.set_month(root["month"]);
} }
if (root.containsKey("day")) { if (root["day"].is<uint8_t>()) {
call.set_day(root["day"]); call.set_day(root["day"]);
} }
call.perform(); call.perform();
@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
} }
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["year"] = year; root["year"] = year;
root["month"] = month; root["month"] = month;
root["day"] = day; root["day"] = day;

View File

@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
void MQTTDateTimeComponent::setup() { void MQTTDateTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->datetime_->make_call(); auto call = this->datetime_->make_call();
if (root.containsKey("year")) { if (root["year"].is<uint16_t>()) {
call.set_year(root["year"]); call.set_year(root["year"]);
} }
if (root.containsKey("month")) { if (root["month"].is<uint8_t>()) {
call.set_month(root["month"]); call.set_month(root["month"]);
} }
if (root.containsKey("day")) { if (root["day"].is<uint8_t>()) {
call.set_day(root["day"]); call.set_day(root["day"]);
} }
if (root.containsKey("hour")) { if (root["hour"].is<uint8_t>()) {
call.set_hour(root["hour"]); call.set_hour(root["hour"]);
} }
if (root.containsKey("minute")) { if (root["minute"].is<uint8_t>()) {
call.set_minute(root["minute"]); call.set_minute(root["minute"]);
} }
if (root.containsKey("second")) { if (root["second"].is<uint8_t>()) {
call.set_second(root["second"]); call.set_second(root["second"]);
} }
call.perform(); call.perform();
@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() {
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) { uint8_t second) {
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["year"] = year; root["year"] = year;
root["month"] = month; root["month"] = month;
root["day"] = day; root["day"] = day;

View File

@ -16,7 +16,8 @@ using namespace esphome::event;
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>();
for (const auto &event_type : this->event_->get_event_types()) for (const auto &event_type : this->event_->get_event_types())
event_types.add(event_type); event_types.add(event_type);
@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
} }
bool MQTTEventComponent::publish_event_(const std::string &event_type) { bool MQTTEventComponent::publish_event_(const std::string &event_type) {
return this->publish_json(this->get_state_topic_(), return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_EVENT_TYPE] = event_type;
});
} }
std::string MQTTEventComponent::component_type() const { return "event"; } std::string MQTTEventComponent::component_type() const { return "event"; }

View File

@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() {
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (this->state_->get_traits().supports_direction()) { if (this->state_->get_traits().supports_direction()) {
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();

View File

@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() {
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
bool MQTTJSONLightComponent::publish_state_() { bool MQTTJSONLightComponent::publish_state_() {
return this->publish_json(this->get_state_topic_(), return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
LightJSONSchema::dump_json(*this->state_, root);
});
} }
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["schema"] = "json"; root["schema"] = "json";
auto traits = this->state_->get_traits(); auto traits = this->state_->get_traits();
root[MQTT_COLOR_MODE] = true; root[MQTT_COLOR_MODE] = true;
JsonArray color_modes = root.createNestedArray("supported_color_modes"); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray color_modes = root["supported_color_modes"].to<JsonArray>();
if (traits.supports_color_mode(ColorMode::ON_OFF)) if (traits.supports_color_mode(ColorMode::ON_OFF))
color_modes.add("onoff"); color_modes.add("onoff");
if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
if (this->state_->supports_effects()) { if (this->state_->supports_effects()) {
root["effect"] = true; root["effect"] = true;
JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST); JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
for (auto *effect : this->state_->get_effects()) for (auto *effect : this->state_->get_effects())
effect_list.add(effect->get_name()); effect_list.add(effect->get_name());
effect_list.add("None"); effect_list.add("None");

View File

@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
std::string MQTTLockComponent::component_type() const { return "lock"; } std::string MQTTLockComponent::component_type() const { return "lock"; }
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->lock_->traits.get_assumed_state()) // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (this->lock_->traits.get_assumed_state()) {
root[MQTT_OPTIMISTIC] = true; root[MQTT_OPTIMISTIC] = true;
}
if (this->lock_->traits.get_supports_open()) if (this->lock_->traits.get_supports_open())
root[MQTT_PAYLOAD_OPEN] = "OPEN"; root[MQTT_PAYLOAD_OPEN] = "OPEN";
} }

View File

@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = number_->traits; const auto &traits = number_->traits;
// https://www.home-assistant.io/integrations/number.mqtt/ // https://www.home-assistant.io/integrations/number.mqtt/
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MIN] = traits.get_min_value();
root[MQTT_MAX] = traits.get_max_value(); root[MQTT_MAX] = traits.get_max_value();
root[MQTT_STEP] = traits.get_step(); root[MQTT_STEP] = traits.get_step();

View File

@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = select_->traits; const auto &traits = select_->traits;
// https://www.home-assistant.io/integrations/select.mqtt/ // https://www.home-assistant.io/integrations/select.mqtt/
JsonArray options = root.createNestedArray(MQTT_OPTIONS); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray options = root[MQTT_OPTIONS].to<JsonArray>();
for (const auto &option : traits.get_options()) for (const auto &option : traits.get_options())
options.add(option); options.add(option);

View File

@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_device_class().empty()) // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->sensor_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
}
if (!this->sensor_->get_unit_of_measurement().empty()) if (!this->sensor_->get_unit_of_measurement().empty())
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();

View File

@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() {
std::string MQTTSwitchComponent::component_type() const { return "switch"; } std::string MQTTSwitchComponent::component_type() const { return "switch"; }
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->switch_->assumed_state()) // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (this->switch_->assumed_state()) {
root[MQTT_OPTIMISTIC] = true; root[MQTT_OPTIMISTIC] = true;
}
} }
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }

View File

@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; }
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
switch (this->text_->traits.get_mode()) { switch (this->text_->traits.get_mode()) {
case TEXT_MODE_TEXT: case TEXT_MODE_TEXT:
root[MQTT_MODE] = "text"; root[MQTT_MODE] = "text";

View File

@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_device_class().empty()) // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->sensor_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
}
config.command_topic = false; config.command_topic = false;
} }
void MQTTTextSensor::setup() { void MQTTTextSensor::setup() {

View File

@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
void MQTTTimeComponent::setup() { void MQTTTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->time_->make_call(); auto call = this->time_->make_call();
if (root.containsKey("hour")) { if (root["hour"].is<uint8_t>()) {
call.set_hour(root["hour"]); call.set_hour(root["hour"]);
} }
if (root.containsKey("minute")) { if (root["minute"].is<uint8_t>()) {
call.set_minute(root["minute"]); call.set_minute(root["minute"]);
} }
if (root.containsKey("second")) { if (root["second"].is<uint8_t>()) {
call.set_second(root["second"]); call.set_second(root["second"]);
} }
call.perform(); call.perform();
@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
} }
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["hour"] = hour; root["hour"] = hour;
root["minute"] = minute; root["minute"] = minute;
root["second"] = second; root["second"] = second;

View File

@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() {
} }
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["schema"] = "json"; root["schema"] = "json";
root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
} }

View File

@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
} }
} }
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->valve_->get_device_class().empty()) // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->valve_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
}
auto traits = this->valve_->get_traits(); auto traits = this->valve_->get_traits();
if (traits.get_is_assumed_state()) { if (traits.get_is_assumed_state()) {

View File

@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) {
// map 16 bit humidity value into range [-6%, 118%] // map 16 bit humidity value into range [-6%, 118%]
float const humidity_partial = double(humidity) / (1 << 16); float const humidity_partial = double(humidity) / (1 << 16);
float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial);
float const compensated_humidity_percentage = float const compensated_humidity_percentage =
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);

View File

@ -0,0 +1,218 @@
from __future__ import annotations
from pathlib import Path
import esphome.codegen as cg
from esphome.components.zephyr import (
copy_files as zephyr_copy_files,
zephyr_add_pm_static,
zephyr_set_core_data,
zephyr_to_code,
)
from esphome.components.zephyr.const import (
BOOTLOADER_MCUBOOT,
KEY_BOOTLOADER,
KEY_ZEPHYR,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
CONF_FRAMEWORK,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_NRF52,
)
from esphome.core import CORE, EsphomeError, coroutine_with_priority
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG
from .const import (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
)
# force import gpio to register pin schema
from .gpio import nrf52_pin_to_code # noqa
CODEOWNERS = ["@tomaszduda23"]
AUTO_LOAD = ["zephyr"]
IS_TARGET_PLATFORM = True
def set_core_data(config: ConfigType) -> ConfigType:
zephyr_set_core_data(config)
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1)
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
return config
BOOTLOADERS = [
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_MCUBOOT,
]
def _detect_bootloader(config: ConfigType) -> ConfigType:
"""Detect the bootloader for the given board."""
config = config.copy()
bootloaders: list[str] = []
board = config[CONF_BOARD]
if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]:
# this board have bootloaders config available
bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER]
if KEY_BOOTLOADER not in config:
if bootloaders:
# there is no bootloader in config -> take first one
config[KEY_BOOTLOADER] = bootloaders[0]
else:
# make mcuboot as default if there is no configuration for that board
config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT
elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders:
raise cv.Invalid(
f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
}
),
_detect_bootloader,
set_core_data,
)
@coroutine_with_priority(1000)
async def to_code(config: ConfigType) -> None:
"""Convert the configuration to code."""
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_NRF52")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "NRF52")
cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK])
cg.add_platformio_option(
"platform",
"https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip",
)
cg.add_platformio_option(
"platform_packages",
[
"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip",
"platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip",
],
)
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
# make sure that firmware.zip is created
# for Adafruit_nRF52_Bootloader
cg.add_platformio_option("board_upload.protocol", "nrfutil")
cg.add_platformio_option("board_upload.use_1200bps_touch", "true")
cg.add_platformio_option("board_upload.require_upload_port", "true")
cg.add_platformio_option("board_upload.wait_for_upload_port", "true")
zephyr_to_code(config)
def copy_files() -> None:
"""Copy files to the build directory."""
zephyr_copy_files()
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
"""Get the download types for the firmware."""
types = []
UF2_PATH = "zephyr/zephyr.uf2"
DFU_PATH = "firmware.zip"
HEX_PATH = "zephyr/zephyr.hex"
HEX_MERGED_PATH = "zephyr/merged.hex"
APP_IMAGE_PATH = "zephyr/app_update.bin"
build_dir = Path(storage_json.firmware_bin_path).parent
if (build_dir / UF2_PATH).is_file():
types = [
{
"title": "UF2 package (recommended)",
"description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.",
"file": UF2_PATH,
"download": f"{storage_json.name}.uf2",
},
{
"title": "DFU package",
"description": "For flashing via adafruit-nrfutil using USB CDC.",
"file": DFU_PATH,
"download": f"dfu-{storage_json.name}.zip",
},
]
else:
types = [
{
"title": "HEX package",
"description": "For flashing via pyocd using SWD.",
"file": (
HEX_MERGED_PATH
if (build_dir / HEX_MERGED_PATH).is_file()
else HEX_PATH
),
"download": f"{storage_json.name}.hex",
},
]
if (build_dir / APP_IMAGE_PATH).is_file():
types += [
{
"title": "App update package",
"description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.",
"file": APP_IMAGE_PATH,
"download": f"app-{storage_json.name}.img",
},
]
return types
def _upload_using_platformio(
config: ConfigType, port: str, upload_args: list[str]
) -> int | str:
from esphome import platformio_api
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def upload_program(config: ConfigType, args, host: str) -> bool:
from esphome.__main__ import check_permissions, get_port_type
result = 0
handled = False
if get_port_type(host) == "SERIAL":
check_permissions(host)
result = _upload_using_platformio(config, host, ["-t", "upload"])
handled = True
if host == "PYOCD":
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
handled = True
if result != 0:
raise EsphomeError(f"Upload failed with result: {result}")
return handled

View File

@ -0,0 +1,34 @@
from esphome.components.zephyr import Section
from esphome.components.zephyr.const import KEY_BOOTLOADER
from .const import (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
)
BOARDS_ZEPHYR = {
"adafruit_itsybitsy_nrf52840": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
]
},
}
# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go
# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map
BOOTLOADER_CONFIG = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: [
Section("empty_app_offset", 0x0, 0x26000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [
Section("empty_app_offset", 0x0, 0x26000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [
Section("empty_app_offset", 0x0, 0x27000, "flash_primary"),
],
}

View File

@ -0,0 +1,4 @@
BOOTLOADER_ADAFRUIT = "adafruit"
BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132"
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"

View File

@ -0,0 +1,53 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.zephyr.const import zephyr_ns
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52
ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin)
def _translate_pin(value):
if isinstance(value, dict) or value is None:
raise cv.Invalid(
"This variable only supports pin numbers, not full pin schemas "
"(with inverted and mode)."
)
if isinstance(value, int):
return value
try:
return int(value)
except ValueError:
pass
# e.g. P0.27
if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".":
return cv.int_(value[len("P")].strip()) * 32 + cv.int_(
value[len("P0.") :].strip()
)
raise cv.Invalid(f"Invalid pin: {value}")
def validate_gpio_pin(value):
value = _translate_pin(value)
if value < 0 or value > (32 + 16):
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
return value
NRF52_PIN_SCHEMA = cv.All(
pins.gpio_base_schema(
ZephyrGPIOPin,
validate_gpio_pin,
modes=pins.GPIO_STANDARD_MODES,
),
)
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA)
async def nrf52_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@ -2,7 +2,7 @@ import logging
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import ( from esphome.components.image import (
CONF_INVERT_ALPHA, CONF_INVERT_ALPHA,
@ -11,6 +11,7 @@ from esphome.components.image import (
Image_, Image_,
get_image_type_enum, get_image_type_enum,
get_transparency_enum, get_transparency_enum,
validate_settings,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
rp2040_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0),
), ),
validate_settings,
) )
) )
@ -213,6 +215,7 @@ async def to_code(config):
get_image_type_enum(config[CONF_TYPE]), get_image_type_enum(config[CONF_TYPE]),
transparent, transparent,
config[CONF_BUFFER_SIZE], config[CONF_BUFFER_SIZE],
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
) )
await cg.register_component(var, config) await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])

View File

@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) {
} }
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
image::Transparency transparency, uint32_t download_buffer_size) image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
: Image(nullptr, 0, 0, type, transparency), : Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size), download_buffer_initial_size_(download_buffer_size),
format_(format), format_(format),
fixed_width_(width), fixed_width_(width),
fixed_height_(height) { fixed_height_(height),
is_big_endian_(is_big_endian) {
this->set_url(url); this->set_url(url);
} }
@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
break; break;
} }
case ImageType::IMAGE_TYPE_GRAYSCALE: { case ImageType::IMAGE_TYPE_GRAYSCALE: {
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) { if (gray == 1) {
gray = 0; gray = 0;
@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
case ImageType::IMAGE_TYPE_RGB565: { case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color); this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color); uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); if (this->is_big_endian_) {
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
} else {
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
}
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w; this->buffer_[pos + 2] = color.w;
} }

View File

@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent,
* @param buffer_size Size of the buffer used to download the image. * @param buffer_size Size of the buffer used to download the image.
*/ */
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
image::Transparency transparency, uint32_t buffer_size); image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent,
const int fixed_width_; const int fixed_width_;
/** height requested on configuration, or 0 if non specified. */ /** height requested on configuration, or 0 if non specified. */
const int fixed_height_; const int fixed_height_;
/**
* Whether the image is stored in big-endian format.
* This is used to determine how to store 16 bit colors in the buffer.
*/
bool is_big_endian_;
/** /**
* Actual width of the current image. If fixed_width_ is specified, * Actual width of the current image. If fixed_width_ is specified,
* this will be equal to it; otherwise it will be set once the decoding * this will be equal to it; otherwise it will be set once the decoding

View File

@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) {
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
this->state = state < 0.003 && this->zero_means_zero_ this->state = state < 0.003 && this->zero_means_zero_
? 0.0 ? 0.0
: clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
this->has_state_ = true; this->has_state_ = true;
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
} }

View File

@ -0,0 +1,34 @@
"""
Runtime statistics component for ESPHome.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@bdraco"]
CONF_LOG_INTERVAL = "log_interval"
runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats")
RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(RuntimeStatsCollector),
cv.Optional(
CONF_LOG_INTERVAL, default="60s"
): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
"""Generate code for the runtime statistics component."""
# Define USE_RUNTIME_STATS when this component is used
cg.add_define("USE_RUNTIME_STATS")
# Create the runtime stats instance (constructor sets global_runtime_stats)
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL]))

View File

@ -0,0 +1,102 @@
#include "runtime_stats.h"
#ifdef USE_RUNTIME_STATS
#include "esphome/core/component.h"
#include <algorithm>
namespace esphome {
namespace runtime_stats {
RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) {
global_runtime_stats = this;
}
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
if (component == nullptr)
return;
// Check if we have cached the name for this component
auto name_it = this->component_names_cache_.find(component);
if (name_it == this->component_names_cache_.end()) {
// First time seeing this component, cache its name
const char *source = component->get_component_source();
this->component_names_cache_[component] = source;
this->component_stats_[source].record_time(duration_ms);
} else {
this->component_stats_[name_it->second].record_time(duration_ms);
}
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
}
void RuntimeStatsCollector::log_stats_() {
ESP_LOGI(TAG, "Component Runtime Statistics");
ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
// First collect stats we want to display
std::vector<ComponentStatPair> stats_to_display;
for (const auto &it : this->component_stats_) {
const ComponentRuntimeStats &stats = it.second;
if (stats.get_period_count() > 0) {
ComponentStatPair pair = {it.first, &stats};
stats_to_display.push_back(pair);
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const char *source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source,
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
stats->get_period_time_ms());
}
// Log total stats since boot
ESP_LOGI(TAG, "Total stats (since boot):");
// Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(),
[](const ComponentStatPair &a, const ComponentStatPair &b) {
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
});
for (const auto &it : stats_to_display) {
const char *source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source,
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
}
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if (this->next_log_time_ == 0)
return;
if (current_time >= this->next_log_time_) {
this->log_stats_();
this->reset_stats_();
this->next_log_time_ = current_time + this->log_interval_;
}
}
} // namespace runtime_stats
runtime_stats::RuntimeStatsCollector *global_runtime_stats =
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_RUNTIME_STATS

View File

@ -0,0 +1,132 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_RUNTIME_STATS
#include <map>
#include <vector>
#include <cstdint>
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
class Component; // Forward declaration
namespace runtime_stats {
static const char *const TAG = "runtime_stats";
class ComponentRuntimeStats {
public:
ComponentRuntimeStats()
: period_count_(0),
period_time_ms_(0),
period_max_time_ms_(0),
total_count_(0),
total_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Period stats (reset each logging interval)
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
}
// Total stats (persistent until reboot)
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
}
protected:
// Period stats (reset each logging interval)
uint32_t period_count_;
uint32_t period_time_ms_;
uint32_t period_max_time_ms_;
// Total stats (persistent until reboot)
uint32_t total_count_;
uint32_t total_time_ms_;
uint32_t total_max_time_ms_;
};
// For sorting components by run time
struct ComponentStatPair {
const char *name;
const ComponentRuntimeStats *stats;
bool operator>(const ComponentStatPair &other) const {
// Sort by period time as that's what we're displaying in the logs
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
}
};
class RuntimeStatsCollector {
public:
RuntimeStatsCollector();
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
uint32_t get_log_interval() const { return this->log_interval_; }
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
protected:
void log_stats_();
void reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
// Use const char* keys for efficiency
// Custom comparator for const char* keys in map
// Without this, std::map would compare pointer addresses instead of string contents,
// causing identical component names at different addresses to be treated as different keys
struct CStrCompare {
bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; }
};
std::map<const char *, ComponentRuntimeStats, CStrCompare> component_stats_;
std::map<Component *, const char *> component_names_cache_;
uint32_t log_interval_;
uint32_t next_log_time_;
};
} // namespace runtime_stats
extern runtime_stats::RuntimeStatsCollector
*global_runtime_stats; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_RUNTIME_STATS

View File

@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
value = clamp(value, -1.0f, 1.0f); value = clamp(value, -1.0f, 1.0f);
float level; float level;
if (value < 0.0) { if (value < 0.0) {
level = lerp(-value, this->idle_level_, this->min_level_); level = std::lerp(this->idle_level_, this->min_level_, -value);
} else { } else {
level = lerp(value, this->idle_level_, this->max_level_); level = std::lerp(this->idle_level_, this->max_level_, value);
} }
this->output_->set_level(level); this->output_->set_level(level);
this->current_value_ = value; this->current_value_ = value;

View File

@ -1,4 +1,5 @@
import re import re
from typing import Any
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
@ -139,6 +140,27 @@ def get_hw_interface_list():
return [] return []
def one_of_interface_validator(additional_values: list[str] | None = None) -> Any:
"""Helper to create a one_of validator for SPI interfaces.
This delays evaluation of get_hw_interface_list() until validation time,
avoiding access to CORE.data during module import.
Args:
additional_values: List of additional valid values to include
"""
if additional_values is None:
additional_values = []
def validator(value: str) -> str:
return cv.one_of(
*sum(get_hw_interface_list(), additional_values),
lower=True,
)(value)
return cv.All(cv.string, validator)
# Given an SPI name, return the index of it in the available list # Given an SPI name, return the index of it in the available list
def get_spi_index(name): def get_spi_index(name):
for i, ilist in enumerate(get_hw_interface_list()): for i, ilist in enumerate(get_hw_interface_list()):
@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All(
cv.Optional(CONF_FORCE_SW): cv.invalid( cv.Optional(CONF_FORCE_SW): cv.invalid(
"force_sw is deprecated - use interface: software" "force_sw is deprecated - use interface: software"
), ),
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator(
*sum(get_hw_interface_list(), ["software", "hardware", "any"]), ["software", "hardware", "any"]
lower=True,
), ),
cv.Optional(CONF_DATA_PINS): cv.invalid( cv.Optional(CONF_DATA_PINS): cv.invalid(
"'data_pins' should be used with 'type: quad or octal' only" "'data_pins' should be used with 'type: quad or octal' only"
@ -309,10 +330,9 @@ def spi_mode_schema(mode):
cv.ensure_list(pins.internal_gpio_output_pin_number), cv.ensure_list(pins.internal_gpio_output_pin_number),
cv.Length(min=pin_count, max=pin_count), cv.Length(min=pin_count, max=pin_count),
), ),
cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( cv.Optional(
*sum(get_hw_interface_list(), ["hardware"]), CONF_INTERFACE, default="hardware"
lower=True, ): one_of_interface_validator(["hardware"]),
),
cv.Optional(CONF_MISO_PIN): cv.invalid( cv.Optional(CONF_MISO_PIN): cv.invalid(
f"'miso_pin' should not be used with {mode} SPI" f"'miso_pin' should not be used with {mode} SPI"
), ),

View File

@ -146,8 +146,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
if sub is not None: if sub is not None:
item[k] = sub item[k] = sub
for old, new in replace_keys: for old, new in replace_keys:
item[new] = merge_config(item.get(old), item.get(new)) if str(new) == str(old):
del item[old] item[new] = item[old]
else:
item[new] = merge_config(item.get(old), item.get(new))
del item[old]
elif isinstance(item, str): elif isinstance(item, str):
sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing)
if isinstance(sub, JinjaStr) or sub != item: if isinstance(sub, JinjaStr) or sub != item:

View File

@ -6,6 +6,7 @@ import tzlocal
from esphome import automation from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AT, CONF_AT,
@ -25,7 +26,7 @@ from esphome.const import (
CONF_TIMEZONE, CONF_TIMEZONE,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
) )
from esphome.core import coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -341,6 +342,8 @@ async def register_time(time_var, config):
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
if CORE.using_zephyr:
zephyr_add_prj_conf("POSIX_CLOCK", True)
cg.add_define("USE_TIME") cg.add_define("USE_TIME")
cg.add_global(time_ns.using) cg.add_global(time_ns.using)

View File

@ -2,13 +2,15 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_HOST #ifdef USE_HOST
#include <sys/time.h> #include <sys/time.h>
#elif defined(USE_ZEPHYR)
#include <zephyr/posix/time.h>
#else #else
#include "lwip/opt.h" #include "lwip/opt.h"
#endif #endif
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include "sys/time.h" #include "sys/time.h"
#endif #endif
#ifdef USE_RP2040 #if defined(USE_RP2040) || defined(USE_ZEPHYR)
#include <sys/time.h> #include <sys/time.h>
#endif #endif
#include <cerrno> #include <cerrno>
@ -22,11 +24,22 @@ static const char *const TAG = "time";
RealTimeClock::RealTimeClock() = default; RealTimeClock::RealTimeClock() = default;
void RealTimeClock::synchronize_epoch_(uint32_t epoch) { void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
// Update UTC epoch time. // Update UTC epoch time.
#ifdef USE_ZEPHYR
struct timespec ts;
ts.tv_nsec = 0;
ts.tv_sec = static_cast<time_t>(epoch);
int ret = clock_settime(CLOCK_REALTIME, &ts);
if (ret != 0) {
ESP_LOGW(TAG, "clock_settime() failed with code %d", ret);
}
#else
struct timeval timev { struct timeval timev {
.tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
}; };
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
struct timezone tz = {0, 0}; struct timezone tz = {0, 0};
int ret = settimeofday(&timev, &tz); int ret = settimeofday(&timev, &tz);
if (ret == EINVAL) { if (ret == EINVAL) {
@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
if (ret != 0) { if (ret != 0) {
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
} }
#endif
auto time = this->now(); auto time = this->now();
ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
time.minute, time.second); time.minute, time.second);

View File

@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
light::LightJSONSchema::dump_json(*obj, root); light::LightJSONSchema::dump_json(*obj, root);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("effects"); JsonArray opt = root["effects"].to<JsonArray>();
opt.add("None"); opt.add("None");
for (auto const &option : obj->get_effects()) { for (auto const &option : obj->get_effects()) {
opt.add(option->get_name()); opt.add(option->get_name());
@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
return json::build_json([this, obj, value, start_config](JsonObject root) { return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("option"); JsonArray opt = root["option"].to<JsonArray>();
for (auto &option : obj->traits.get_options()) { for (auto &option : obj->traits.get_options()) {
opt.add(option); opt.add(option);
} }
@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
} }
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return json::build_json([this, obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
const auto traits = obj->get_traits(); const auto traits = obj->get_traits();
@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
char buf[16]; char buf[16];
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("modes"); JsonArray opt = root["modes"].to<JsonArray>();
for (climate::ClimateMode m : traits.get_supported_modes()) for (climate::ClimateMode m : traits.get_supported_modes())
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
if (!traits.get_supported_custom_fan_modes().empty()) { if (!traits.get_supported_custom_fan_modes().empty()) {
JsonArray opt = root.createNestedArray("fan_modes"); JsonArray opt = root["fan_modes"].to<JsonArray>();
for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
} }
if (!traits.get_supported_custom_fan_modes().empty()) { if (!traits.get_supported_custom_fan_modes().empty()) {
JsonArray opt = root.createNestedArray("custom_fan_modes"); JsonArray opt = root["custom_fan_modes"].to<JsonArray>();
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
opt.add(custom_fan_mode); opt.add(custom_fan_mode);
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
JsonArray opt = root.createNestedArray("swing_modes"); JsonArray opt = root["swing_modes"].to<JsonArray>();
for (auto swing_mode : traits.get_supported_swing_modes()) for (auto swing_mode : traits.get_supported_swing_modes())
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
} }
if (traits.get_supports_presets() && obj->preset.has_value()) { if (traits.get_supports_presets() && obj->preset.has_value()) {
JsonArray opt = root.createNestedArray("presets"); JsonArray opt = root["presets"].to<JsonArray>();
for (climate::ClimatePreset m : traits.get_supported_presets()) for (climate::ClimatePreset m : traits.get_supported_presets())
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
} }
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
JsonArray opt = root.createNestedArray("custom_presets"); JsonArray opt = root["custom_presets"].to<JsonArray>();
for (auto const &custom_preset : traits.get_supported_custom_presets()) for (auto const &custom_preset : traits.get_supported_custom_presets())
opt.add(custom_preset); opt.add(custom_preset);
} }
@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
root["state"] = root["target_temperature"]; root["state"] = root["target_temperature"];
} }
}); });
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
#endif #endif
@ -1635,7 +1637,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
root["event_type"] = event_type; root["event_type"] = event_type;
} }
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray event_types = root.createNestedArray("event_types"); JsonArray event_types = root["event_types"].to<JsonArray>();
for (auto const &event_type : obj->get_event_types()) { for (auto const &event_type : obj->get_event_types()) {
event_types.add(event_type); event_types.add(event_type);
} }
@ -1682,6 +1684,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return json::build_json([this, obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
root["value"] = obj->update_info.latest_version; root["value"] = obj->update_info.latest_version;
@ -1707,166 +1710,166 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
}); });
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
#endif #endif
bool WebServer::canHandle(AsyncWebServerRequest *request) const { bool WebServer::canHandle(AsyncWebServerRequest *request) const {
if (request->url() == "/") const auto &url = request->url();
const auto method = request->method();
// Simple URL checks
if (url == "/")
return true; return true;
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
if (request->url() == "/events") { if (url == "/events")
return true; return true;
}
#endif #endif
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
if (request->url() == "/0.css") if (url == "/0.css")
return true; return true;
#endif #endif
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
if (request->url() == "/0.js") if (url == "/0.js")
return true; return true;
#endif #endif
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA))
return true; return true;
}
#endif #endif
// Store the URL to prevent temporary string destruction // Parse URL for component checks
// request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF)
// UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url()
const auto &url = request->url();
UrlMatch match = match_url(url.c_str(), url.length(), true); UrlMatch match = match_url(url.c_str(), url.length(), true);
if (!match.valid) if (!match.valid)
return false; return false;
// Common pattern check
bool is_get = method == HTTP_GET;
bool is_post = method == HTTP_POST;
bool is_get_or_post = is_get || is_post;
if (!is_get_or_post)
return false;
// GET-only components
if (is_get) {
#ifdef USE_SENSOR #ifdef USE_SENSOR
if (request->method() == HTTP_GET && match.domain_equals("sensor")) if (match.domain_equals("sensor"))
return true; return true;
#endif #endif
#ifdef USE_SWITCH
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch"))
return true;
#endif
#ifdef USE_BUTTON
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button"))
return true;
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) if (match.domain_equals("binary_sensor"))
return true; return true;
#endif #endif
#ifdef USE_FAN
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan"))
return true;
#endif
#ifdef USE_LIGHT
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light"))
return true;
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) if (match.domain_equals("text_sensor"))
return true; return true;
#endif #endif
#ifdef USE_COVER
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover"))
return true;
#endif
#ifdef USE_NUMBER
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number"))
return true;
#endif
#ifdef USE_DATETIME_DATE
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date"))
return true;
#endif
#ifdef USE_DATETIME_TIME
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time"))
return true;
#endif
#ifdef USE_DATETIME_DATETIME
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime"))
return true;
#endif
#ifdef USE_TEXT
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text"))
return true;
#endif
#ifdef USE_SELECT
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select"))
return true;
#endif
#ifdef USE_CLIMATE
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate"))
return true;
#endif
#ifdef USE_LOCK
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock"))
return true;
#endif
#ifdef USE_VALVE
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve"))
return true;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel"))
return true;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
if (request->method() == HTTP_GET && match.domain_equals("event")) if (match.domain_equals("event"))
return true; return true;
#endif #endif
}
#ifdef USE_UPDATE // GET+POST components
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) if (is_get_or_post) {
return true; #ifdef USE_SWITCH
if (match.domain_equals("switch"))
return true;
#endif #endif
#ifdef USE_BUTTON
if (match.domain_equals("button"))
return true;
#endif
#ifdef USE_FAN
if (match.domain_equals("fan"))
return true;
#endif
#ifdef USE_LIGHT
if (match.domain_equals("light"))
return true;
#endif
#ifdef USE_COVER
if (match.domain_equals("cover"))
return true;
#endif
#ifdef USE_NUMBER
if (match.domain_equals("number"))
return true;
#endif
#ifdef USE_DATETIME_DATE
if (match.domain_equals("date"))
return true;
#endif
#ifdef USE_DATETIME_TIME
if (match.domain_equals("time"))
return true;
#endif
#ifdef USE_DATETIME_DATETIME
if (match.domain_equals("datetime"))
return true;
#endif
#ifdef USE_TEXT
if (match.domain_equals("text"))
return true;
#endif
#ifdef USE_SELECT
if (match.domain_equals("select"))
return true;
#endif
#ifdef USE_CLIMATE
if (match.domain_equals("climate"))
return true;
#endif
#ifdef USE_LOCK
if (match.domain_equals("lock"))
return true;
#endif
#ifdef USE_VALVE
if (match.domain_equals("valve"))
return true;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
if (match.domain_equals("alarm_control_panel"))
return true;
#endif
#ifdef USE_UPDATE
if (match.domain_equals("update"))
return true;
#endif
}
return false; return false;
} }
void WebServer::handleRequest(AsyncWebServerRequest *request) { void WebServer::handleRequest(AsyncWebServerRequest *request) {
if (request->url() == "/") { const auto &url = request->url();
// Handle static routes first
if (url == "/") {
this->handle_index_request(request); this->handle_index_request(request);
return; return;
} }
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
if (request->url() == "/events") { if (url == "/events") {
this->events_.add_new_client(this, request); this->events_.add_new_client(this, request);
return; return;
} }
#endif #endif
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
if (request->url() == "/0.css") { if (url == "/0.css") {
this->handle_css_request(request); this->handle_css_request(request);
return; return;
} }
#endif #endif
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
if (request->url() == "/0.js") { if (url == "/0.js") {
this->handle_js_request(request); this->handle_js_request(request);
return; return;
} }
@ -1879,147 +1882,85 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
} }
#endif #endif
// See comment in canHandle() for why we store the URL reference // Parse URL for component routing
const auto &url = request->url();
UrlMatch match = match_url(url.c_str(), url.length(), false); UrlMatch match = match_url(url.c_str(), url.length(), false);
// Component routing using minimal code repetition
struct ComponentRoute {
const char *domain;
void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &);
};
static const ComponentRoute ROUTES[] = {
#ifdef USE_SENSOR #ifdef USE_SENSOR
if (match.domain_equals("sensor")) { {"sensor", &WebServer::handle_sensor_request},
this->handle_sensor_request(request, match);
return;
}
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (match.domain_equals("switch")) { {"switch", &WebServer::handle_switch_request},
this->handle_switch_request(request, match);
return;
}
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
if (match.domain_equals("button")) { {"button", &WebServer::handle_button_request},
this->handle_button_request(request, match);
return;
}
#endif #endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
if (match.domain_equals("binary_sensor")) { {"binary_sensor", &WebServer::handle_binary_sensor_request},
this->handle_binary_sensor_request(request, match);
return;
}
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
if (match.domain_equals("fan")) { {"fan", &WebServer::handle_fan_request},
this->handle_fan_request(request, match);
return;
}
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
if (match.domain_equals("light")) { {"light", &WebServer::handle_light_request},
this->handle_light_request(request, match);
return;
}
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (match.domain_equals("text_sensor")) { {"text_sensor", &WebServer::handle_text_sensor_request},
this->handle_text_sensor_request(request, match);
return;
}
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
if (match.domain_equals("cover")) { {"cover", &WebServer::handle_cover_request},
this->handle_cover_request(request, match);
return;
}
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
if (match.domain_equals("number")) { {"number", &WebServer::handle_number_request},
this->handle_number_request(request, match);
return;
}
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
if (match.domain_equals("date")) { {"date", &WebServer::handle_date_request},
this->handle_date_request(request, match);
return;
}
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
if (match.domain_equals("time")) { {"time", &WebServer::handle_time_request},
this->handle_time_request(request, match);
return;
}
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
if (match.domain_equals("datetime")) { {"datetime", &WebServer::handle_datetime_request},
this->handle_datetime_request(request, match);
return;
}
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
if (match.domain_equals("text")) { {"text", &WebServer::handle_text_request},
this->handle_text_request(request, match);
return;
}
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
if (match.domain_equals("select")) { {"select", &WebServer::handle_select_request},
this->handle_select_request(request, match);
return;
}
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
if (match.domain_equals("climate")) { {"climate", &WebServer::handle_climate_request},
this->handle_climate_request(request, match);
return;
}
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
if (match.domain_equals("lock")) { {"lock", &WebServer::handle_lock_request},
this->handle_lock_request(request, match);
return;
}
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
if (match.domain_equals("valve")) { {"valve", &WebServer::handle_valve_request},
this->handle_valve_request(request, match);
return;
}
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
if (match.domain_equals("alarm_control_panel")) { {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request},
this->handle_alarm_control_panel_request(request, match);
return;
}
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
if (match.domain_equals("update")) { {"update", &WebServer::handle_update_request},
this->handle_update_request(request, match);
return;
}
#endif #endif
};
// Check each route
for (const auto &route : ROUTES) {
if (match.domain_equals(route.domain)) {
(this->*route.handler)(request, match);
return;
}
}
// No matching handler found - send 404 // No matching handler found - send 404
ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str()); ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str());
request->send(404, "text/plain", "Not Found"); request->send(404, "text/plain", "Not Found");
} }

View File

@ -40,4 +40,4 @@ async def to_code(config):
if CORE.is_esp8266: if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None) cg.add_library("ESP8266WiFi", None)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")

View File

@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
#ifdef USE_WEBSERVER_SORTING #ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) { for (auto &group : ws->sorting_groups_) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
message = json::build_json([group](JsonObject root) { message = json::build_json([group](JsonObject root) {
root["name"] = group.second.name; root["name"] = group.second.name;
root["sorting_weight"] = group.second.weight; root["sorting_weight"] = group.second.weight;
}); });
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
// a (very) large number of these should be able to be queued initially without defer // a (very) large number of these should be able to be queued initially without defer
// since the only thing in the send buffer at this point is the initial ping/config // since the only thing in the send buffer at this point is the initial ping/config

View File

@ -0,0 +1,231 @@
import os
from typing import Final, TypedDict
import esphome.codegen as cg
from esphome.const import CONF_BOARD
from esphome.core import CORE
from esphome.helpers import copy_file_if_changed, write_file_if_changed
from .const import (
BOOTLOADER_MCUBOOT,
KEY_BOOTLOADER,
KEY_EXTRA_BUILD_FILES,
KEY_OVERLAY,
KEY_PM_STATIC,
KEY_PRJ_CONF,
KEY_ZEPHYR,
zephyr_ns,
)
CODEOWNERS = ["@tomaszduda23"]
AUTO_LOAD = ["preferences"]
KEY_BOARD: Final = "board"
PrjConfValueType = bool | str | int
class Section:
def __init__(self, name, address, size, region):
self.name = name
self.address = address
self.size = size
self.region = region
self.end_address = self.address + self.size
def __str__(self):
return (
f"{self.name}:\n"
f" address: 0x{self.address:X}\n"
f" end_address: 0x{self.end_address:X}\n"
f" region: {self.region}\n"
f" size: 0x{self.size:X}"
)
class ZephyrData(TypedDict):
board: str
bootloader: str
prj_conf: dict[str, tuple[PrjConfValueType, bool]]
overlay: str
extra_build_files: dict[str, str]
pm_static: list[Section]
def zephyr_set_core_data(config):
CORE.data[KEY_ZEPHYR] = ZephyrData(
board=config[CONF_BOARD],
bootloader=config[KEY_BOOTLOADER],
prj_conf={},
overlay="",
extra_build_files={},
pm_static=[],
)
return config
def zephyr_data() -> ZephyrData:
return CORE.data[KEY_ZEPHYR]
def zephyr_add_prj_conf(
name: str, value: PrjConfValueType, required: bool = True
) -> None:
"""Set an zephyr prj conf value."""
if not name.startswith("CONFIG_"):
name = "CONFIG_" + name
prj_conf = zephyr_data()[KEY_PRJ_CONF]
if name not in prj_conf:
prj_conf[name] = (value, required)
return
old_value, old_required = prj_conf[name]
if old_value != value and old_required:
raise ValueError(
f"{name} already set with value '{old_value}', cannot set again to '{value}'"
)
if required:
prj_conf[name] = (value, required)
def zephyr_add_overlay(content):
zephyr_data()[KEY_OVERLAY] += content
def add_extra_build_file(filename: str, path: str) -> bool:
"""Add an extra build file to the project."""
extra_build_files = zephyr_data()[KEY_EXTRA_BUILD_FILES]
if filename not in extra_build_files:
extra_build_files[filename] = path
return True
return False
def add_extra_script(stage: str, filename: str, path: str):
"""Add an extra script to the project."""
key = f"{stage}:{filename}"
if add_extra_build_file(filename, path):
cg.add_platformio_option("extra_scripts", [key])
def zephyr_to_code(config):
cg.add(zephyr_ns.setup_preferences())
cg.add_build_flag("-DUSE_ZEPHYR")
cg.set_cpp_standard("gnu++20")
# build is done by west so bypass board checking in platformio
cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards"))
# c++ support
zephyr_add_prj_conf("NEWLIB_LIBC", True)
zephyr_add_prj_conf("CONFIG_FPU", True)
zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True)
zephyr_add_prj_conf("CPLUSPLUS", True)
zephyr_add_prj_conf("CONFIG_STD_CPP20", True)
zephyr_add_prj_conf("LIB_CPLUSPLUS", True)
# preferences
zephyr_add_prj_conf("SETTINGS", True)
zephyr_add_prj_conf("NVS", True)
zephyr_add_prj_conf("FLASH_MAP", True)
zephyr_add_prj_conf("CONFIG_FLASH", True)
# watchdog
zephyr_add_prj_conf("WATCHDOG", True)
zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False)
# disable console
zephyr_add_prj_conf("UART_CONSOLE", False)
zephyr_add_prj_conf("CONSOLE", False, False)
# use NFC pins as GPIO
zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True)
# <err> os: ***** USAGE FAULT *****
# <err> os: Illegal load of EXC_RETURN into PC
zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048)
add_extra_script(
"pre",
"pre_build.py",
os.path.join(os.path.dirname(__file__), "pre_build.py.script"),
)
def _format_prj_conf_val(value: PrjConfValueType) -> str:
if isinstance(value, bool):
return "y" if value else "n"
if isinstance(value, int):
return str(value)
if isinstance(value, str):
return f'"{value}"'
raise ValueError
def zephyr_add_cdc_acm(config, id):
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
zephyr_add_prj_conf("USB_CDC_ACM", True)
# prevent device to go to susspend, without this communication stop working in python
# there should be a way to solve it
zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False)
# prevent logging when buffer is full
zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True)
zephyr_add_overlay(
f"""
&zephyr_udc0 {{
cdc_acm_uart{id}: cdc_acm_uart{id} {{
compatible = "zephyr,cdc-acm-uart";
}};
}};
"""
)
def zephyr_add_pm_static(section: Section):
CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section)
def copy_files():
want_opts = zephyr_data()[KEY_PRJ_CONF]
prj_conf = (
"\n".join(
f"{name}={_format_prj_conf_val(value[0])}"
for name, value in sorted(want_opts.items())
)
+ "\n"
)
write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf)
write_file_if_changed(
CORE.relative_build_path("zephyr/app.overlay"),
zephyr_data()[KEY_OVERLAY],
)
if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[
KEY_BOARD
] in ["xiao_ble"]:
fake_board_manifest = """
{
"frameworks": [
"zephyr"
],
"name": "esphome nrf52",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104
},
"url": "https://esphome.io/",
"vendor": "esphome"
}
"""
write_file_if_changed(
CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"),
fake_board_manifest,
)
for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items():
copy_file_if_changed(
path,
CORE.relative_build_path(filename),
)
pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC])
if pm_static:
write_file_if_changed(
CORE.relative_build_path("zephyr/pm_static.yml"), pm_static
)

View File

@ -0,0 +1,14 @@
from typing import Final
import esphome.codegen as cg
BOOTLOADER_MCUBOOT = "mcuboot"
KEY_BOOTLOADER: Final = "bootloader"
KEY_EXTRA_BUILD_FILES: Final = "extra_build_files"
KEY_OVERLAY: Final = "overlay"
KEY_PM_STATIC: Final = "pm_static"
KEY_PRJ_CONF: Final = "prj_conf"
KEY_ZEPHYR = "zephyr"
zephyr_ns = cg.esphome_ns.namespace("zephyr")

View File

@ -0,0 +1,86 @@
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#include <zephyr/drivers/watchdog.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/random/rand32.h>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome {
static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0));
void yield() { ::k_yield(); }
uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); }
uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); }
void delayMicroseconds(uint32_t us) { ::k_usleep(us); }
void delay(uint32_t ms) { ::k_msleep(ms); }
void arch_init() {
if (device_is_ready(WDT)) {
static wdt_timeout_cfg wdt_config{};
wdt_config.flags = WDT_FLAG_RESET_SOC;
wdt_config.window.max = 2000;
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
if (wdt_channel_id >= 0) {
wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP);
}
}
}
void arch_feed_wdt() {
if (wdt_channel_id >= 0) {
wdt_feed(WDT, wdt_channel_id);
}
}
void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
Mutex::Mutex() {
auto *mutex = new k_mutex();
this->handle_ = mutex;
k_mutex_init(mutex);
}
Mutex::~Mutex() { delete static_cast<k_mutex *>(this->handle_); }
void Mutex::lock() { k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_FOREVER); }
bool Mutex::try_lock() { return k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_NO_WAIT) == 0; }
void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); }
IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); }
IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); }
uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp)
bool random_bytes(uint8_t *data, size_t len) {
sys_rand_get(data, len);
return true;
}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0;
mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF;
mac[2] = NRF_FICR->DEVICEADDR[0] >> 24;
mac[3] = NRF_FICR->DEVICEADDR[0] >> 16;
mac[4] = NRF_FICR->DEVICEADDR[0] >> 8;
mac[5] = NRF_FICR->DEVICEADDR[0];
}
} // namespace esphome
void setup();
void loop();
int main() {
setup();
while (true) {
loop();
esphome::yield();
}
return 0;
}
#endif

View File

@ -0,0 +1,120 @@
#ifdef USE_ZEPHYR
#include "gpio.h"
#include <zephyr/drivers/gpio.h>
#include "esphome/core/log.h"
namespace esphome {
namespace zephyr {
static const char *const TAG = "zephyr";
static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) {
int ret = 0;
if (flags & gpio::FLAG_INPUT) {
ret |= GPIO_INPUT;
}
if (flags & gpio::FLAG_OUTPUT) {
ret |= GPIO_OUTPUT;
if (value != inverted) {
ret |= GPIO_OUTPUT_INIT_HIGH;
} else {
ret |= GPIO_OUTPUT_INIT_LOW;
}
}
if (flags & gpio::FLAG_PULLUP) {
ret |= GPIO_PULL_UP;
}
if (flags & gpio::FLAG_PULLDOWN) {
ret |= GPIO_PULL_DOWN;
}
if (flags & gpio::FLAG_OPEN_DRAIN) {
ret |= GPIO_OPEN_DRAIN;
}
return ret;
}
struct ISRPinArg {
uint8_t pin;
bool inverted;
};
ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = this->pin_;
arg->inverted = this->inverted_;
return ISRInternalGPIOPin((void *) arg);
}
void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
// TODO
}
void ZephyrGPIOPin::setup() {
const struct device *gpio = nullptr;
if (this->pin_ < 32) {
#define GPIO0 DT_NODELABEL(gpio0)
#if DT_NODE_HAS_STATUS(GPIO0, okay)
gpio = DEVICE_DT_GET(GPIO0);
#else
#error "gpio0 is disabled"
#endif
} else {
#define GPIO1 DT_NODELABEL(gpio1)
#if DT_NODE_HAS_STATUS(GPIO1, okay)
gpio = DEVICE_DT_GET(GPIO1);
#else
#error "gpio1 is disabled"
#endif
}
if (device_is_ready(gpio)) {
this->gpio_ = gpio;
} else {
ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_);
return;
}
this->pin_mode(this->flags_);
}
void ZephyrGPIOPin::pin_mode(gpio::Flags flags) {
if (nullptr == this->gpio_) {
return;
}
gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_));
}
std::string ZephyrGPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32);
return buffer;
}
bool ZephyrGPIOPin::digital_read() {
if (nullptr == this->gpio_) {
return false;
}
return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_);
}
void ZephyrGPIOPin::digital_write(bool value) {
// make sure that value is not ignored since it can be inverted e.g. on switch side
// that way init state should be correct
this->value_ = value;
if (nullptr == this->gpio_) {
return;
}
gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0);
}
void ZephyrGPIOPin::detach_interrupt() const {
// TODO
}
} // namespace zephyr
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
// TODO
return false;
}
} // namespace esphome
#endif

View File

@ -0,0 +1,38 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/hal.h"
struct device;
namespace esphome {
namespace zephyr {
class ZephyrGPIOPin : public InternalGPIOPin {
public:
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return this->pin_; }
bool is_inverted() const override { return this->inverted_; }
gpio::Flags get_flags() const override { return flags_; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
const device *gpio_ = nullptr;
bool value_ = false;
};
} // namespace zephyr
} // namespace esphome
#endif // USE_ZEPHYR

View File

@ -0,0 +1,4 @@
Import("env")
board_config = env.BoardConfig()
board_config.update("frameworks", ["arduino", "zephyr"])

View File

@ -0,0 +1,156 @@
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#include "esphome/core/preferences.h"
#include "esphome/core/log.h"
#include <zephyr/settings/settings.h>
namespace esphome {
namespace zephyr {
static const char *const TAG = "zephyr.preferences";
#define ESPHOME_SETTINGS_KEY "esphome"
class ZephyrPreferenceBackend : public ESPPreferenceBackend {
public:
ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; }
ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)) { this->type_ = type; }
bool save(const uint8_t *data, size_t len) override {
this->data.resize(len);
std::memcpy(this->data.data(), data, len);
ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len);
return true;
}
bool load(uint8_t *data, size_t len) override {
if (len != this->data.size()) {
ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len);
return false;
}
std::memcpy(data, this->data.data(), len);
ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len);
return true;
}
uint32_t get_type() const { return this->type_; }
std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); }
std::vector<uint8_t> data;
protected:
uint32_t type_ = 0;
};
class ZephyrPreferences : public ESPPreferences {
public:
void open() {
int err = settings_subsys_init();
if (err) {
ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err);
return;
}
static struct settings_handler settings_cb = {
.name = ESPHOME_SETTINGS_KEY,
.h_set = load_setting,
.h_export = export_settings,
};
err = settings_register(&settings_cb);
if (err) {
ESP_LOGE(TAG, "setting_register failed, err, %d", err);
return;
}
err = settings_load_subtree(ESPHOME_SETTINGS_KEY);
if (err) {
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
return;
}
ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size());
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
for (auto *backend : this->backends_) {
if (backend->get_type() == type) {
return ESPPreferenceObject(backend);
}
}
printf("type %u size %u\n", type, this->backends_.size());
auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory)
ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str());
this->backends_.push_back(pref);
return ESPPreferenceObject(pref);
}
bool sync() override {
ESP_LOGD(TAG, "Save settings");
int err = settings_save();
if (err) {
ESP_LOGE(TAG, "Cannot save settings, err: %d", err);
return false;
}
return true;
}
bool reset() override {
ESP_LOGD(TAG, "Reset settings");
for (auto *backend : this->backends_) {
// save empty delete data
backend->data.clear();
}
sync();
return true;
}
protected:
std::vector<ZephyrPreferenceBackend *> backends_;
static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) {
auto type = parse_hex<uint32_t>(name);
if (!type.has_value()) {
std::string full_name(ESPHOME_SETTINGS_KEY);
full_name += "/";
full_name += name;
// Delete unusable keys. Otherwise it will stay in flash forever.
settings_delete(full_name.c_str());
return 1;
}
std::vector<uint8_t> data(len);
int err = read_cb(cb_arg, data.data(), len);
ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err);
auto *pref = new ZephyrPreferenceBackend(*type, std::move(data)); // NOLINT(cppcoreguidelines-owning-memory)
static_cast<ZephyrPreferences *>(global_preferences)->backends_.push_back(pref);
return 0;
}
static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) {
for (auto *backend : static_cast<ZephyrPreferences *>(global_preferences)->backends_) {
auto name = backend->get_key();
int err = cb(name.c_str(), backend->data.data(), backend->data.size());
ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err);
}
return 0;
}
};
void setup_preferences() {
auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
global_preferences = prefs;
prefs->open();
}
} // namespace zephyr
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif

View File

@ -0,0 +1,13 @@
#pragma once
#ifdef USE_ZEPHYR
namespace esphome {
namespace zephyr {
void setup_preferences();
} // namespace zephyr
} // namespace esphome
#endif

View File

@ -21,6 +21,7 @@ class Platform(StrEnum):
HOST = "host" HOST = "host"
LIBRETINY_OLDSTYLE = "libretiny" LIBRETINY_OLDSTYLE = "libretiny"
LN882X = "ln882x" LN882X = "ln882x"
NRF52 = "nrf52"
RP2040 = "rp2040" RP2040 = "rp2040"
RTL87XX = "rtl87xx" RTL87XX = "rtl87xx"
@ -31,6 +32,7 @@ class Framework(StrEnum):
ARDUINO = "arduino" ARDUINO = "arduino"
ESP_IDF = "esp-idf" ESP_IDF = "esp-idf"
NATIVE = "host" NATIVE = "host"
ZEPHYR = "zephyr"
class PlatformFramework(Enum): class PlatformFramework(Enum):
@ -47,6 +49,9 @@ class PlatformFramework(Enum):
RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
# Zephyr framework platforms
NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR)
# Host platform (native) # Host platform (native)
HOST_NATIVE = (Platform.HOST, Framework.NATIVE) HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266
PLATFORM_HOST = Platform.HOST PLATFORM_HOST = Platform.HOST
PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
PLATFORM_LN882X = Platform.LN882X PLATFORM_LN882X = Platform.LN882X
PLATFORM_NRF52 = Platform.NRF52
PLATFORM_RP2040 = Platform.RP2040 PLATFORM_RP2040 = Platform.RP2040
PLATFORM_RTL87XX = Platform.RTL87XX PLATFORM_RTL87XX = Platform.RTL87XX

View File

@ -21,6 +21,7 @@ from esphome.const import (
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_HOST, PLATFORM_HOST,
PLATFORM_LN882X, PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2040, PLATFORM_RP2040,
PLATFORM_RTL87XX, PLATFORM_RTL87XX,
) )
@ -670,6 +671,10 @@ class EsphomeCore:
def is_libretiny(self): def is_libretiny(self):
return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x
@property
def is_nrf52(self):
return self.target_platform == PLATFORM_NRF52
@property @property
def is_host(self): def is_host(self):
return self.target_platform == PLATFORM_HOST return self.target_platform == PLATFORM_HOST
@ -686,6 +691,10 @@ class EsphomeCore:
def using_esp_idf(self): def using_esp_idf(self):
return self.target_framework == "esp-idf" return self.target_framework == "esp-idf"
@property
def using_zephyr(self):
return self.target_framework == "zephyr"
def add_job(self, func, *args, **kwargs) -> None: def add_job(self, func, *args, **kwargs) -> None:
self.event_loop.add_job(func, *args, **kwargs) self.event_loop.add_job(func, *args, **kwargs)

View File

@ -4,6 +4,9 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include <algorithm> #include <algorithm>
#include <ranges> #include <ranges>
#ifdef USE_RUNTIME_STATS
#include "esphome/components/runtime_stats/runtime_stats.h"
#endif
#ifdef USE_STATUS_LED #ifdef USE_STATUS_LED
#include "esphome/components/status_led/status_led.h" #include "esphome/components/status_led/status_led.h"
@ -141,6 +144,14 @@ void Application::loop() {
this->in_loop_ = false; this->in_loop_ = false;
this->app_state_ = new_app_state; this->app_state_ = new_app_state;
#ifdef USE_RUNTIME_STATS
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
if (global_runtime_stats != nullptr) {
global_runtime_stats->process_pending_stats(last_op_end_time);
}
#endif
// Use the last component's end time instead of calling millis() again // Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_; auto elapsed = last_op_end_time - this->last_loop_;
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
@ -309,6 +320,12 @@ void Application::disable_component_loop_(Component *component) {
if (this->in_loop_ && i == this->current_loop_index_) { if (this->in_loop_ && i == this->current_loop_index_) {
// Decrement so we'll process the swapped component next // Decrement so we'll process the swapped component next
this->current_loop_index_--; this->current_loop_index_--;
// Update the loop start time to current time so the swapped component
// gets correct timing instead of inheriting stale timing.
// This prevents integer underflow in timing calculations by ensuring
// the swapped component starts with a fresh timing reference, avoiding
// errors caused by stale or wrapped timing values.
this->loop_component_start_time_ = millis();
} }
} }
return; return;

View File

@ -9,6 +9,9 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_RUNTIME_STATS
#include "esphome/components/runtime_stats/runtime_stats.h"
#endif
namespace esphome { namespace esphome {
@ -138,7 +141,7 @@ void Component::call_dump_config() {
} }
} }
} }
ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg); ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg);
} }
} }
@ -191,7 +194,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
return false; return false;
} }
void Component::mark_failed() { void Component::mark_failed() {
ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_FAILED; this->component_state_ |= COMPONENT_STATE_FAILED;
this->status_set_error(); this->status_set_error();
@ -229,7 +232,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
} }
void Component::reset_to_construction_state() { void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
// Clear error status when resetting // Clear error status when resetting
@ -264,6 +267,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() const { bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
} }
bool Component::can_proceed() { return true; } bool Component::can_proceed() { return true; }
@ -275,14 +279,14 @@ void Component::status_set_warning(const char *message) {
return; return;
this->component_state_ |= STATUS_LED_WARNING; this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message); ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message);
} }
void Component::status_set_error(const char *message) { void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0) if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return; return;
this->component_state_ |= STATUS_LED_ERROR; this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message);
if (strcmp(message, "unspecified") != 0) { if (strcmp(message, "unspecified") != 0) {
// Lazy allocate the error messages vector if needed // Lazy allocate the error messages vector if needed
if (!component_error_messages) { if (!component_error_messages) {
@ -303,13 +307,13 @@ void Component::status_clear_warning() {
if ((this->component_state_ & STATUS_LED_WARNING) == 0) if ((this->component_state_ & STATUS_LED_WARNING) == 0)
return; return;
this->component_state_ &= ~STATUS_LED_WARNING; this->component_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source()); ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source());
} }
void Component::status_clear_error() { void Component::status_clear_error() {
if ((this->component_state_ & STATUS_LED_ERROR) == 0) if ((this->component_state_ & STATUS_LED_ERROR) == 0)
return; return;
this->component_state_ &= ~STATUS_LED_ERROR; this->component_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source()); ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source());
} }
void Component::status_momentary_warning(const std::string &name, uint32_t length) { void Component::status_momentary_warning(const std::string &name, uint32_t length) {
this->status_set_warning(); this->status_set_warning();
@ -395,6 +399,13 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis(); uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_; uint32_t blocking_time = curr_time - this->started_;
#ifdef USE_RUNTIME_STATS
// Record component runtime stats
if (global_runtime_stats != nullptr) {
global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time);
}
#endif
bool should_warn; bool should_warn;
if (this->component_ != nullptr) { if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time); should_warn = this->component_->should_warn_of_blocking(blocking_time);
@ -403,7 +414,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
} }
if (should_warn) { if (should_warn) {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source(); const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms"); ESP_LOGW(TAG, "Components should block for at most 30 ms");
} }

View File

@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) {
this->at_ = 0; this->at_ = 0;
this->include_internal_ = include_internal; this->include_internal_ = include_internal;
} }
template<typename PlatformItem>
void ComponentIterator::process_platform_item_(const std::vector<PlatformItem *> &items,
bool (ComponentIterator::*on_item)(PlatformItem *)) {
if (this->at_ >= items.size()) {
this->advance_platform_();
} else {
PlatformItem *item = items[this->at_];
if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) {
this->at_++;
}
}
}
void ComponentIterator::advance_platform_() {
this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1);
this->at_ = 0;
}
void ComponentIterator::advance() { void ComponentIterator::advance() {
bool advance_platform = false;
bool success = true;
switch (this->state_) { switch (this->state_) {
case IteratorState::NONE: case IteratorState::NONE:
// not started // not started
return; return;
case IteratorState::BEGIN: case IteratorState::BEGIN:
if (this->on_begin()) { if (this->on_begin()) {
advance_platform = true; advance_platform_();
} else {
return;
} }
break; break;
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
case IteratorState::BINARY_SENSOR: case IteratorState::BINARY_SENSOR:
if (this->at_ >= App.get_binary_sensors().size()) { this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor);
advance_platform = true;
} else {
auto *binary_sensor = App.get_binary_sensors()[this->at_];
if (binary_sensor->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_binary_sensor(binary_sensor);
}
}
break; break;
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
case IteratorState::COVER: case IteratorState::COVER:
if (this->at_ >= App.get_covers().size()) { this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover);
advance_platform = true;
} else {
auto *cover = App.get_covers()[this->at_];
if (cover->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_cover(cover);
}
}
break; break;
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
case IteratorState::FAN: case IteratorState::FAN:
if (this->at_ >= App.get_fans().size()) { this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan);
advance_platform = true;
} else {
auto *fan = App.get_fans()[this->at_];
if (fan->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_fan(fan);
}
}
break; break;
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
case IteratorState::LIGHT: case IteratorState::LIGHT:
if (this->at_ >= App.get_lights().size()) { this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light);
advance_platform = true;
} else {
auto *light = App.get_lights()[this->at_];
if (light->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_light(light);
}
}
break; break;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
case IteratorState::SENSOR: case IteratorState::SENSOR:
if (this->at_ >= App.get_sensors().size()) { this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor);
advance_platform = true;
} else {
auto *sensor = App.get_sensors()[this->at_];
if (sensor->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_sensor(sensor);
}
}
break; break;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
case IteratorState::SWITCH: case IteratorState::SWITCH:
if (this->at_ >= App.get_switches().size()) { this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch);
advance_platform = true;
} else {
auto *a_switch = App.get_switches()[this->at_];
if (a_switch->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_switch(a_switch);
}
}
break; break;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
case IteratorState::BUTTON: case IteratorState::BUTTON:
if (this->at_ >= App.get_buttons().size()) { this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button);
advance_platform = true;
} else {
auto *button = App.get_buttons()[this->at_];
if (button->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_button(button);
}
}
break; break;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
case IteratorState::TEXT_SENSOR: case IteratorState::TEXT_SENSOR:
if (this->at_ >= App.get_text_sensors().size()) { this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor);
advance_platform = true;
} else {
auto *text_sensor = App.get_text_sensors()[this->at_];
if (text_sensor->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_text_sensor(text_sensor);
}
}
break; break;
#endif #endif
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
case IteratorState ::SERVICE: case IteratorState::SERVICE:
if (this->at_ >= api::global_api_server->get_user_services().size()) { this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service);
advance_platform = true;
} else {
auto *service = api::global_api_server->get_user_services()[this->at_];
success = this->on_service(service);
}
break; break;
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
case IteratorState::CAMERA: case IteratorState::CAMERA: {
if (camera::Camera::instance() == nullptr) { camera::Camera *camera_instance = camera::Camera::instance();
advance_platform = true; if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) {
} else { this->on_camera(camera_instance);
if (camera::Camera::instance()->is_internal() && !this->include_internal_) {
advance_platform = success = true;
break;
} else {
advance_platform = success = this->on_camera(camera::Camera::instance());
}
} }
break; advance_platform_();
} break;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
case IteratorState::CLIMATE: case IteratorState::CLIMATE:
if (this->at_ >= App.get_climates().size()) { this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate);
advance_platform = true;
} else {
auto *climate = App.get_climates()[this->at_];
if (climate->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_climate(climate);
}
}
break; break;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
case IteratorState::NUMBER: case IteratorState::NUMBER:
if (this->at_ >= App.get_numbers().size()) { this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number);
advance_platform = true;
} else {
auto *number = App.get_numbers()[this->at_];
if (number->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_number(number);
}
}
break; break;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
case IteratorState::DATETIME_DATE: case IteratorState::DATETIME_DATE:
if (this->at_ >= App.get_dates().size()) { this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date);
advance_platform = true;
} else {
auto *date = App.get_dates()[this->at_];
if (date->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_date(date);
}
}
break; break;
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
case IteratorState::DATETIME_TIME: case IteratorState::DATETIME_TIME:
if (this->at_ >= App.get_times().size()) { this->process_platform_item_(App.get_times(), &ComponentIterator::on_time);
advance_platform = true;
} else {
auto *time = App.get_times()[this->at_];
if (time->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_time(time);
}
}
break; break;
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
case IteratorState::DATETIME_DATETIME: case IteratorState::DATETIME_DATETIME:
if (this->at_ >= App.get_datetimes().size()) { this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime);
advance_platform = true;
} else {
auto *datetime = App.get_datetimes()[this->at_];
if (datetime->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_datetime(datetime);
}
}
break; break;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
case IteratorState::TEXT: case IteratorState::TEXT:
if (this->at_ >= App.get_texts().size()) { this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text);
advance_platform = true;
} else {
auto *text = App.get_texts()[this->at_];
if (text->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_text(text);
}
}
break; break;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
case IteratorState::SELECT: case IteratorState::SELECT:
if (this->at_ >= App.get_selects().size()) { this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select);
advance_platform = true;
} else {
auto *select = App.get_selects()[this->at_];
if (select->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_select(select);
}
}
break; break;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
case IteratorState::LOCK: case IteratorState::LOCK:
if (this->at_ >= App.get_locks().size()) { this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock);
advance_platform = true;
} else {
auto *a_lock = App.get_locks()[this->at_];
if (a_lock->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_lock(a_lock);
}
}
break; break;
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
case IteratorState::VALVE: case IteratorState::VALVE:
if (this->at_ >= App.get_valves().size()) { this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve);
advance_platform = true;
} else {
auto *valve = App.get_valves()[this->at_];
if (valve->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_valve(valve);
}
}
break; break;
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
case IteratorState::MEDIA_PLAYER: case IteratorState::MEDIA_PLAYER:
if (this->at_ >= App.get_media_players().size()) { this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player);
advance_platform = true;
} else {
auto *media_player = App.get_media_players()[this->at_];
if (media_player->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_media_player(media_player);
}
}
break; break;
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
case IteratorState::ALARM_CONTROL_PANEL: case IteratorState::ALARM_CONTROL_PANEL:
if (this->at_ >= App.get_alarm_control_panels().size()) { this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel);
advance_platform = true;
} else {
auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_];
if (a_alarm_control_panel->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_alarm_control_panel(a_alarm_control_panel);
}
}
break; break;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
case IteratorState::EVENT: case IteratorState::EVENT:
if (this->at_ >= App.get_events().size()) { this->process_platform_item_(App.get_events(), &ComponentIterator::on_event);
advance_platform = true;
} else {
auto *event = App.get_events()[this->at_];
if (event->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_event(event);
}
}
break; break;
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
case IteratorState::UPDATE: case IteratorState::UPDATE:
if (this->at_ >= App.get_updates().size()) { this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update);
advance_platform = true;
} else {
auto *update = App.get_updates()[this->at_];
if (update->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_update(update);
}
}
break; break;
#endif #endif
case IteratorState::MAX: case IteratorState::MAX:
if (this->on_end()) { if (this->on_end()) {
this->state_ = IteratorState::NONE; this->state_ = IteratorState::NONE;
} }
return; return;
} }
if (advance_platform) {
this->state_ = static_cast<IteratorState>(static_cast<uint8_t>(this->state_) + 1);
this->at_ = 0;
} else if (success) {
this->at_++;
}
} }
bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_end() { return true; }
bool ComponentIterator::on_begin() { return true; } bool ComponentIterator::on_begin() { return true; }
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES

View File

@ -171,6 +171,11 @@ class ComponentIterator {
} state_{IteratorState::NONE}; } state_{IteratorState::NONE};
uint16_t at_{0}; // Supports up to 65,535 entities per type uint16_t at_{0}; // Supports up to 65,535 entities per type
bool include_internal_{false}; bool include_internal_{false};
template<typename PlatformItem>
void process_platform_item_(const std::vector<PlatformItem *> &items,
bool (ComponentIterator::*on_item)(PlatformItem *));
void advance_platform_();
}; };
} // namespace esphome } // namespace esphome

View File

@ -258,7 +258,9 @@ std::string format_hex(const uint8_t *data, size_t length) {
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); } std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
// Shared implementation for uint8_t and string hex formatting
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0) if (data == nullptr || length == 0)
return ""; return "";
std::string ret; std::string ret;
@ -274,6 +276,10 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
return ret + " (" + std::to_string(length) + ")"; return ret + " (" + std::to_string(length) + ")";
return ret; return ret;
} }
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
return format_hex_pretty_uint8(data, length, separator, show_length);
}
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) { std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length); return format_hex_pretty(data.data(), data.size(), separator, show_length);
} }
@ -300,20 +306,7 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator,
return format_hex_pretty(data.data(), data.size(), separator, show_length); return format_hex_pretty(data.data(), data.size(), separator, show_length);
} }
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
if (data.empty()) return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
return "";
std::string ret;
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
ret.resize(multiple * data.length() - (separator ? 1 : 0));
for (size_t i = 0; i < data.length(); i++) {
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
if (separator && i != data.length() - 1)
ret[multiple * i + 2] = separator;
}
if (show_length && data.length() > 4)
return ret + " (" + std::to_string(data.length()) + ")";
return ret;
} }
std::string format_bin(const uint8_t *data, size_t length) { std::string format_bin(const uint8_t *data, size_t length) {

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <array>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
@ -678,7 +679,7 @@ class InterruptLock {
~InterruptLock(); ~InterruptLock();
protected: protected:
#if defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR)
uint32_t state_; uint32_t state_;
#endif #endif
}; };
@ -783,7 +784,7 @@ template<class T> class RAMAllocator {
T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); } T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
T *reallocate(T *p, size_t n, size_t manual_size) { T *reallocate(T *p, size_t n, size_t manual_size) {
size_t size = n * sizeof(T); size_t size = n * manual_size;
T *ptr = nullptr; T *ptr = nullptr;
#ifdef USE_ESP32 #ifdef USE_ESP32
if (this->flags_ & Flags::ALLOC_EXTERNAL) { if (this->flags_ & Flags::ALLOC_EXTERNAL) {

View File

@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
os.environ.setdefault( os.environ.setdefault(
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
) )
# Suppress Python syntax warnings from third-party scripts during compilation
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
cmd = ["platformio"] + list(args) cmd = ["platformio"] + list(args)
if not CORE.verbose: if not CORE.verbose:

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