mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 17:46:23 -07:00
Compare commits
3 Commits
wifi_timeo
...
scheduler_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d91cad1636 | ||
|
|
54d0328002 | ||
|
|
865312ff60 |
@@ -276,12 +276,12 @@ This document provides essential context for AI models interacting with this pro
|
||||
## 7. Specific Instructions for AI Collaboration
|
||||
|
||||
* **Contribution Workflow (Pull Request Process):**
|
||||
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
|
||||
1. **Fork & Branch:** Create a new branch in your fork.
|
||||
2. **Make Changes:** Adhere to all coding conventions and patterns.
|
||||
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
|
||||
4. **Lint:** Run `pre-commit` to ensure code is compliant.
|
||||
5. **Commit:** Commit your changes. There is no strict format for commit messages.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly.
|
||||
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
|
||||
|
||||
@@ -1 +1 @@
|
||||
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07
|
||||
29270eecb86ffa07b2b1d2a4ca56dd7f84762ddc89c6248dbf3f012eca8780b6
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
2
.github/workflows/auto-label-pr.yml
vendored
2
.github/workflows/auto-label-pr.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -152,12 +152,12 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -334,14 +334,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -413,14 +413,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -502,14 +502,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -735,7 +735,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -759,7 +759,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -800,7 +800,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -821,7 +821,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -847,7 +847,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -885,7 +885,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -915,13 +915,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
@@ -959,13 +959,13 @@ jobs:
|
||||
- memory-impact-comment
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check job results
|
||||
- name: Success
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
run: exit 0
|
||||
- name: Failure
|
||||
if: ${{ contains(needs.*.result, 'failure') }}
|
||||
env:
|
||||
NEEDS_JSON: ${{ toJSON(needs) }}
|
||||
JSON_DOC: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
# memory-impact-target-branch is allowed to fail without blocking CI.
|
||||
# This job builds the target branch (dev/beta/release) which may fail because:
|
||||
# 1. The target branch has a build issue independent of this PR
|
||||
# 2. This PR fixes a build issue on the target branch
|
||||
# In either case, we only care that the PR branch builds successfully.
|
||||
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'
|
||||
echo $JSON_DOC | jq
|
||||
exit 1
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.9
|
||||
rev: v0.14.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -212,7 +212,6 @@ esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/hlk_fm22x/* @OnFreund
|
||||
esphome/components/hlw8032/* @rici4kubicek
|
||||
esphome/components/hm3301/* @freekode
|
||||
esphome/components/hmac_md5/* @dwmw2
|
||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
||||
@@ -228,7 +227,6 @@ esphome/components/hte501/* @Stock-M
|
||||
esphome/components/http_request/ota/* @oarcher
|
||||
esphome/components/http_request/update/* @jesserockz
|
||||
esphome/components/htu31d/* @betterengineering
|
||||
esphome/components/hub75/* @stuartparmenter
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
@@ -308,7 +306,7 @@ esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
|
||||
esphome/components/micronova/* @edenhaus @jorre05
|
||||
esphome/components/micronova/* @jorre05
|
||||
esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
@@ -524,7 +522,6 @@ esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
esphome/components/update/* @jesserockz
|
||||
esphome/components/uponor_smatrix/* @kroimon
|
||||
esphome/components/usb_cdc_acm/* @kbx81
|
||||
esphome/components/usb_host/* @clydebarrow
|
||||
esphome/components/usb_uart/* @clydebarrow
|
||||
esphome/components/valve/* @esphome/core
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
We welcome contributions to the ESPHome suite of code and documentation!
|
||||
|
||||
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the
|
||||
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
|
||||
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
|
||||
|
||||
**See also:**
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.1.0-dev
|
||||
PROJECT_NUMBER = 2025.12.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://media.esphome.io/logo/logo-text-on-dark.svg">
|
||||
<img src="https://media.esphome.io/logo/logo-text-on-light.svg" alt="ESPHome Logo">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
|
||||
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
|
||||
}
|
||||
|
||||
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
|
||||
// H/T to https://esphome.io/cookbook/bme280_environment/
|
||||
// H/T to https://esphome.io/cookbook/bme280_environment.html
|
||||
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
||||
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
|
||||
// es = saturated vapor pressure (kPa)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
@@ -101,13 +99,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
5: adc_channel_t.ADC_CHANNEL_5,
|
||||
6: adc_channel_t.ADC_CHANNEL_6,
|
||||
},
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html
|
||||
VARIANT_ESP32C61: {
|
||||
1: adc_channel_t.ADC_CHANNEL_0,
|
||||
3: adc_channel_t.ADC_CHANNEL_1,
|
||||
4: adc_channel_t.ADC_CHANNEL_2,
|
||||
5: adc_channel_t.ADC_CHANNEL_3,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32H2: {
|
||||
1: adc_channel_t.ADC_CHANNEL_0,
|
||||
@@ -183,8 +174,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
VARIANT_ESP32C5: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C6: {}, # no ADC2
|
||||
# ESP32-C61 has no ADC2
|
||||
VARIANT_ESP32C61: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32H2: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
|
||||
|
||||
@@ -42,11 +42,10 @@ void ADCSensor::setup() {
|
||||
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
||||
init_config.unit_id = this->adc_unit_;
|
||||
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
|
||||
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
|
||||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
// USE_ESP32_VARIANT_ESP32H2
|
||||
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
|
||||
@@ -75,7 +74,7 @@ void ADCSensor::setup() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
// 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)
|
||||
@@ -112,7 +111,7 @@ void ADCSensor::setup() {
|
||||
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 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
}
|
||||
|
||||
this->setup_flags_.init_complete = true;
|
||||
@@ -187,11 +186,11 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
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 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +219,7 @@ float ADCSensor::sample_autorange_() {
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@@ -232,7 +231,7 @@ float ADCSensor::sample_autorange_() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -267,7 +266,7 @@ float ADCSensor::sample_autorange_() {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
@@ -289,7 +288,7 @@ float ADCSensor::sample_autorange_() {
|
||||
}
|
||||
// Clean up calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
|
||||
@@ -27,13 +27,12 @@ from esphome.const import (
|
||||
CONF_SERVICE,
|
||||
CONF_SERVICES,
|
||||
CONF_TAG,
|
||||
CONF_THEN,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,21 +63,17 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
|
||||
"HomeAssistantActionResponseTrigger", automation.Trigger
|
||||
)
|
||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
||||
APIRespondAction = api_ns.class_("APIRespondAction", automation.Action)
|
||||
APIUnregisterServiceCallAction = api_ns.class_(
|
||||
"APIUnregisterServiceCallAction", automation.Action
|
||||
)
|
||||
|
||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
||||
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
|
||||
SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
|
||||
"bool": cg.bool_,
|
||||
SERVICE_ARG_NATIVE_TYPES = {
|
||||
"bool": bool,
|
||||
"int": cg.int32,
|
||||
"float": cg.float_,
|
||||
"float": float,
|
||||
"string": cg.std_string,
|
||||
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
||||
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
|
||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
|
||||
"string[]": cg.FixedVector.template(cg.std_string)
|
||||
.operator("const")
|
||||
.operator("ref"),
|
||||
@@ -107,85 +102,6 @@ def validate_encryption_key(value):
|
||||
return value
|
||||
|
||||
|
||||
CONF_SUPPORTS_RESPONSE = "supports_response"
|
||||
|
||||
# Enum values in api::enums namespace
|
||||
enums_ns = api_ns.namespace("enums")
|
||||
SUPPORTS_RESPONSE_OPTIONS = {
|
||||
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
|
||||
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
|
||||
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
|
||||
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
|
||||
}
|
||||
|
||||
|
||||
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
|
||||
"""Auto-detect supports_response based on api.respond usage in the action's then block.
|
||||
|
||||
- If api.respond with data found: set to "optional" (unless user explicitly set)
|
||||
- If api.respond without data found: set to "status" (unless user explicitly set)
|
||||
- If no api.respond found: set to "none" (unless user explicitly set)
|
||||
"""
|
||||
|
||||
def scan_actions(items: ConfigFragmentType) -> tuple[bool, bool]:
|
||||
"""Recursively scan actions for api.respond.
|
||||
|
||||
Returns: (found, has_data) tuple - has_data is True if ANY api.respond has data
|
||||
"""
|
||||
found_any = False
|
||||
has_data_any = False
|
||||
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
found, has_data = scan_actions(item)
|
||||
if found:
|
||||
found_any = True
|
||||
has_data_any = has_data_any or has_data
|
||||
elif isinstance(items, dict):
|
||||
# Check if this is an api.respond action
|
||||
if "api.respond" in items:
|
||||
respond_config = items["api.respond"]
|
||||
has_data = isinstance(respond_config, dict) and "data" in respond_config
|
||||
return True, has_data
|
||||
# Recursively check all values
|
||||
for value in items.values():
|
||||
found, has_data = scan_actions(value)
|
||||
if found:
|
||||
found_any = True
|
||||
has_data_any = has_data_any or has_data
|
||||
|
||||
return found_any, has_data_any
|
||||
|
||||
then = config.get(CONF_THEN, [])
|
||||
action_name = config.get(CONF_ACTION)
|
||||
found, has_data = scan_actions(then)
|
||||
|
||||
# If user explicitly set supports_response, validate and use that
|
||||
if CONF_SUPPORTS_RESPONSE in config:
|
||||
user_value = config[CONF_SUPPORTS_RESPONSE]
|
||||
# Validate: "only" requires api.respond with data
|
||||
if user_value == "only" and not has_data:
|
||||
raise cv.Invalid(
|
||||
f"Action '{action_name}' has supports_response=only but no api.respond "
|
||||
"action with 'data:' was found. Use 'status' for responses without data, "
|
||||
"or add 'data:' to your api.respond action."
|
||||
)
|
||||
return config
|
||||
|
||||
# Auto-detect based on api.respond usage
|
||||
if found:
|
||||
config[CONF_SUPPORTS_RESPONSE] = "optional" if has_data else "status"
|
||||
else:
|
||||
config[CONF_SUPPORTS_RESPONSE] = "none"
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _validate_supports_response(value):
|
||||
"""Validate supports_response after auto-detection has set the value."""
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
|
||||
ACTIONS_SCHEMA = automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
|
||||
@@ -196,20 +112,10 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
}
|
||||
),
|
||||
# No default - auto-detected by _auto_detect_supports_response
|
||||
cv.Optional(CONF_SUPPORTS_RESPONSE): cv.enum(
|
||||
SUPPORTS_RESPONSE_OPTIONS, lower=True
|
||||
),
|
||||
},
|
||||
cv.All(
|
||||
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
||||
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
||||
_auto_detect_supports_response,
|
||||
# Re-validate supports_response after auto-detection sets it
|
||||
cv.Schema(
|
||||
{cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -246,7 +152,7 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
|
||||
_LOGGER.warning(
|
||||
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
|
||||
"Please migrate to the 'encryption' configuration. "
|
||||
"See https://esphome.io/components/api/#configuration-variables"
|
||||
"See https://esphome.io/components/api.html#configuration-variables"
|
||||
)
|
||||
|
||||
return config
|
||||
@@ -336,7 +242,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WEB)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -373,61 +279,20 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
for conf in actions:
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
|
||||
# Determine supports_response mode
|
||||
# cv.enum returns the key with enum_value attribute containing the MockObj
|
||||
supports_response_key = conf[CONF_SUPPORTS_RESPONSE]
|
||||
supports_response = supports_response_key.enum_value
|
||||
is_none = supports_response_key == "none"
|
||||
is_optional = supports_response_key == "optional"
|
||||
|
||||
# Add call_id and return_response based on supports_response mode
|
||||
# These must match the C++ Trigger template arguments
|
||||
# - none: no extra args
|
||||
# - status: call_id only (for reporting success/error without data)
|
||||
# - only: call_id only (response always expected with data)
|
||||
# - optional: call_id + return_response (client decides)
|
||||
if not is_none:
|
||||
# call_id is present for "optional", "only", and "status"
|
||||
func_args.append((cg.uint32, "call_id"))
|
||||
# return_response only present for "optional"
|
||||
if is_optional:
|
||||
func_args.append((cg.bool_, "return_response"))
|
||||
|
||||
service_arg_names: list[str] = []
|
||||
template_args = []
|
||||
func_args = []
|
||||
service_arg_names = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
service_template_args.append(native)
|
||||
template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
templ = cg.TemplateArguments(*template_args)
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID],
|
||||
templ,
|
||||
conf[CONF_ACTION],
|
||||
service_arg_names,
|
||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
||||
)
|
||||
triggers.append(trigger)
|
||||
auto = await automation.build_automation(trigger, func_args, conf)
|
||||
|
||||
# For non-none response modes, automatically append unregister action
|
||||
# This ensures the call is unregistered after all actions complete (including async ones)
|
||||
if not is_none:
|
||||
arg_types = [arg[0] for arg in func_args]
|
||||
action_templ = cg.TemplateArguments(*arg_types)
|
||||
unregister_id = ID(
|
||||
f"{conf[CONF_TRIGGER_ID]}__unregister",
|
||||
is_declaration=True,
|
||||
type=APIUnregisterServiceCallAction.template(action_templ),
|
||||
)
|
||||
unregister_action = cg.new_Pvariable(
|
||||
unregister_id,
|
||||
var,
|
||||
)
|
||||
cg.add(auto.add_actions([unregister_action]))
|
||||
await automation.build_automation(trigger, func_args, conf)
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
|
||||
@@ -673,80 +538,6 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
return var
|
||||
|
||||
|
||||
CONF_SUCCESS = "success"
|
||||
CONF_ERROR_MESSAGE = "error_message"
|
||||
|
||||
|
||||
def _validate_api_respond_data(config):
|
||||
"""Set flag during validation so AUTO_LOAD can include json component."""
|
||||
if CONF_DATA in config:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||
return config
|
||||
|
||||
|
||||
API_RESPOND_ACTION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APIServer),
|
||||
cv.Optional(CONF_SUCCESS, default=True): cv.templatable(cv.boolean),
|
||||
cv.Optional(CONF_ERROR_MESSAGE, default=""): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_DATA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
_validate_api_respond_data,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"api.respond",
|
||||
APIRespondAction,
|
||||
API_RESPOND_ACTION_SCHEMA,
|
||||
)
|
||||
async def api_respond_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
# Validate that api.respond is used inside an API action context.
|
||||
# We can't easily validate this at config time since the schema validation
|
||||
# doesn't have access to the parent action context. Validating here in to_code
|
||||
# is still much better than a cryptic C++ compile error.
|
||||
has_call_id = any(name == "call_id" for _, name in args)
|
||||
if not has_call_id:
|
||||
raise EsphomeError(
|
||||
"api.respond can only be used inside an API action's 'then:' block. "
|
||||
"The 'call_id' variable is required to send a response."
|
||||
)
|
||||
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv)
|
||||
|
||||
# Check if we're in optional mode (has return_response arg)
|
||||
is_optional = any(name == "return_response" for _, name in args)
|
||||
if is_optional:
|
||||
cg.add(var.set_is_optional_mode(True))
|
||||
|
||||
templ = await cg.templatable(config[CONF_SUCCESS], args, cg.bool_)
|
||||
cg.add(var.set_success(templ))
|
||||
|
||||
templ = await cg.templatable(config[CONF_ERROR_MESSAGE], args, cg.std_string)
|
||||
cg.add(var.set_error_message(templ))
|
||||
|
||||
if CONF_DATA in config:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES_JSON")
|
||||
# Lambda populates the JsonObject root - no return value needed
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_DATA],
|
||||
args + [(cg.JsonObject, "root")],
|
||||
return_type=cg.void,
|
||||
)
|
||||
cg.add(var.set_data(lambda_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APIServer),
|
||||
|
||||
@@ -579,7 +579,7 @@ message LightCommandRequest {
|
||||
bool has_flash_length = 16;
|
||||
uint32 flash_length = 17;
|
||||
bool has_effect = 18;
|
||||
string effect = 19 [(pointer_to_buffer) = true];
|
||||
string effect = 19;
|
||||
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -855,14 +855,6 @@ enum ServiceArgType {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
|
||||
}
|
||||
enum SupportsResponseType {
|
||||
SUPPORTS_RESPONSE_NONE = 0;
|
||||
SUPPORTS_RESPONSE_OPTIONAL = 1;
|
||||
SUPPORTS_RESPONSE_ONLY = 2;
|
||||
// Status-only response - reports success/error without data payload
|
||||
// Value is higher to avoid conflicts with future Home Assistant values
|
||||
SUPPORTS_RESPONSE_STATUS = 100;
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1;
|
||||
@@ -876,7 +868,6 @@ message ListEntitiesServicesResponse {
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
@@ -899,21 +890,6 @@ message ExecuteServiceRequest {
|
||||
|
||||
fixed32 key = 1;
|
||||
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
||||
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
}
|
||||
|
||||
// Message sent by ESPHome to Home Assistant with service execution response data
|
||||
message ExecuteServiceResponse {
|
||||
option (id) = 131;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";
|
||||
|
||||
uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
|
||||
bool success = 2; // Whether the service execution succeeded
|
||||
string error_message = 3; // Error message if success = false
|
||||
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
|
||||
}
|
||||
|
||||
// ==================== CAMERA ====================
|
||||
@@ -1195,7 +1171,7 @@ message SelectCommandRequest {
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
string state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
#include <cerrno>
|
||||
#include <cinttypes>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
@@ -533,7 +527,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
if (msg.has_flash_length)
|
||||
call.set_flash_length(msg.flash_length);
|
||||
if (msg.has_effect)
|
||||
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
|
||||
call.set_effect(msg.effect);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -905,7 +899,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||
call.set_option(reinterpret_cast<const char *>(msg.state), msg.state_len);
|
||||
call.set_option(msg.state);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1474,64 +1468,35 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
|
||||
resp.set_compilation_time(App.get_compilation_time_ref());
|
||||
|
||||
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
|
||||
// Compile-time StringRef constants for manufacturers
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
#define ESPHOME_MANUFACTURER "Espressif"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif");
|
||||
#elif defined(USE_RP2040)
|
||||
#define ESPHOME_MANUFACTURER "Raspberry Pi"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi");
|
||||
#elif defined(USE_BK72XX)
|
||||
#define ESPHOME_MANUFACTURER "Beken"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
||||
#elif defined(USE_LN882X)
|
||||
#define ESPHOME_MANUFACTURER "Lightning"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
||||
#elif defined(USE_NRF52)
|
||||
#define ESPHOME_MANUFACTURER "Nordic Semiconductor"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
|
||||
#elif defined(USE_RTL87XX)
|
||||
#define ESPHOME_MANUFACTURER "Realtek"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
||||
#elif defined(USE_HOST)
|
||||
#define ESPHOME_MANUFACTURER "Host"
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Host");
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility
|
||||
static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
|
||||
char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
|
||||
memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
|
||||
resp.set_manufacturer(StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1));
|
||||
#else
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
|
||||
resp.set_manufacturer(MANUFACTURER);
|
||||
#endif
|
||||
#undef ESPHOME_MANUFACTURER
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
|
||||
char model_buf[sizeof(MODEL_PROGMEM)];
|
||||
memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
|
||||
resp.set_model(StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1));
|
||||
#else
|
||||
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
|
||||
resp.set_model(MODEL);
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
#ifdef USE_ESP8266
|
||||
static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME;
|
||||
static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION;
|
||||
char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)];
|
||||
char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
|
||||
memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
|
||||
memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
|
||||
resp.set_project_name(StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1));
|
||||
resp.set_project_version(StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1));
|
||||
#else
|
||||
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
|
||||
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
|
||||
resp.set_project_name(PROJECT_NAME);
|
||||
resp.set_project_version(PROJECT_VERSION);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
resp.webserver_port = USE_WEBSERVER_PORT;
|
||||
#endif
|
||||
@@ -1580,12 +1545,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
// Compare entity_id and attribute with message fields
|
||||
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
|
||||
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
|
||||
(it.attribute == nullptr && msg.attribute.empty());
|
||||
|
||||
if (entity_match && attribute_match) {
|
||||
if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
|
||||
it.callback(msg.state);
|
||||
}
|
||||
}
|
||||
@@ -1594,54 +1554,15 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Register the call and get a unique server-generated action_call_id
|
||||
// This avoids collisions when multiple clients use the same call_id
|
||||
uint32_t action_call_id = 0;
|
||||
if (msg.call_id != 0) {
|
||||
action_call_id = this->parent_->register_active_action_call(msg.call_id, this);
|
||||
}
|
||||
// Use the overload that passes action_call_id separately (avoids copying msg)
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
if (service->execute_service(msg, action_call_id)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
if (service->execute_service(msg)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!found) {
|
||||
ESP_LOGV(TAG, "Could not find service");
|
||||
}
|
||||
// Note: For services with supports_response != none, the call is unregistered
|
||||
// by an automatically appended APIUnregisterServiceCallAction at the end of
|
||||
// the action list. This ensures async actions (delays, waits) complete first.
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
resp.response_data = response_data;
|
||||
resp.response_data_len = response_data_len;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -1669,7 +1590,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to clear encryption key");
|
||||
}
|
||||
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
|
||||
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
@@ -1741,13 +1662,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
||||
for (auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type) {
|
||||
// Replace with new creator
|
||||
item.creator = creator;
|
||||
item.creator = std::move(creator);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No existing item found, add new one
|
||||
items.emplace_back(entity, creator, message_type, estimated_size);
|
||||
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
|
||||
}
|
||||
|
||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
|
||||
@@ -1756,7 +1677,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre
|
||||
// This avoids expensive vector::insert which shifts all elements
|
||||
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
||||
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
||||
items.emplace_back(entity, creator, message_type, estimated_size);
|
||||
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
|
||||
if (items.size() > 1) {
|
||||
// Swap the new high-priority item to the front
|
||||
std::swap(items.front(), items.back());
|
||||
@@ -1964,8 +1885,8 @@ void APIConnection::process_state_subscriptions_() {
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
|
||||
// Avoid string copy by using the const char* pointer if it exists
|
||||
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
|
||||
// Avoid string copy by directly using the optional's value if it exists
|
||||
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
|
||||
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
|
||||
@@ -223,13 +223,6 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
@@ -512,9 +505,28 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
class MessageCreator {
|
||||
public:
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
|
||||
// Constructor for const char * (Event types - no allocation needed)
|
||||
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
||||
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
MessageCreator &operator=(const MessageCreator &other) = delete;
|
||||
|
||||
// Move constructor
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
|
||||
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
data_ = other.data_;
|
||||
other.data_.function_ptr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Call operator - uses message_type to determine union type
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint8_t message_type) const;
|
||||
@@ -523,7 +535,7 @@ class APIConnection final : public APIServerConnection {
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
const char *const_char_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
};
|
||||
|
||||
// Generic batching mechanism for both state updates and entity info
|
||||
@@ -536,7 +548,7 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
// Constructor for creating BatchItem
|
||||
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
|
||||
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {}
|
||||
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
|
||||
};
|
||||
|
||||
std::vector<BatchItem> items;
|
||||
@@ -704,12 +716,12 @@ class APIConnection final : public APIServerConnection {
|
||||
}
|
||||
|
||||
// Fall back to scheduled batching
|
||||
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
||||
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size);
|
||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
|
||||
@@ -539,8 +539,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
||||
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
|
||||
std::vector<uint8_t>().swap(prologue_);
|
||||
prologue_ = {};
|
||||
|
||||
err = noise_handshakestate_start(handshake_);
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
|
||||
@@ -611,12 +611,9 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 19: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->effect = value.data();
|
||||
this->effect_len = value.size();
|
||||
case 19:
|
||||
this->effect = value.as_string();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1013,13 +1010,11 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (auto &it : this->args) {
|
||||
buffer.encode_message(3, it, true);
|
||||
}
|
||||
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->name_ref_.size());
|
||||
size.add_fixed32(1, this->key);
|
||||
size.add_repeated_message(1, this->args);
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
@@ -1080,23 +1075,6 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||
this->string_array.init(count_string_array);
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
case 3:
|
||||
this->call_id = value.as_uint32();
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
case 4:
|
||||
this->return_response = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
@@ -1124,24 +1102,6 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, this->call_id);
|
||||
buffer.encode_bool(2, this->success);
|
||||
buffer.encode_string(3, this->error_message_ref_);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
buffer.encode_bytes(4, this->response_data, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
void ExecuteServiceResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, this->call_id);
|
||||
size.add_bool(1, this->success);
|
||||
size.add_length(1, this->error_message_ref_.size());
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
size.add_length(4, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id_ref_);
|
||||
@@ -1572,12 +1532,9 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->state = value.data();
|
||||
this->state_len = value.size();
|
||||
case 2:
|
||||
this->state = value.as_string();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -75,12 +75,6 @@ enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
|
||||
};
|
||||
enum SupportsResponseType : uint32_t {
|
||||
SUPPORTS_RESPONSE_NONE = 0,
|
||||
SUPPORTS_RESPONSE_OPTIONAL = 1,
|
||||
SUPPORTS_RESPONSE_ONLY = 2,
|
||||
SUPPORTS_RESPONSE_STATUS = 100,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
enum ClimateMode : uint32_t {
|
||||
@@ -840,7 +834,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
|
||||
class LightCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 32;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 122;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 112;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "light_command_request"; }
|
||||
#endif
|
||||
@@ -869,8 +863,7 @@ class LightCommandRequest final : public CommandProtoMessage {
|
||||
bool has_flash_length{false};
|
||||
uint32_t flash_length{0};
|
||||
bool has_effect{false};
|
||||
const uint8_t *effect{nullptr};
|
||||
uint16_t effect_len{0};
|
||||
std::string effect{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1264,7 +1257,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 41;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 50;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 48;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_services_response"; }
|
||||
#endif
|
||||
@@ -1272,7 +1265,6 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1305,18 +1297,12 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
|
||||
class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 42;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 45;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 39;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "execute_service_request"; }
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
FixedVector<ExecuteServiceArgument> args{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
uint32_t call_id{0};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
bool return_response{false};
|
||||
#endif
|
||||
void decode(const uint8_t *buffer, size_t length) override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
@@ -1325,32 +1311,6 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
class ExecuteServiceResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 131;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "execute_service_response"; }
|
||||
#endif
|
||||
uint32_t call_id{0};
|
||||
bool success{false};
|
||||
StringRef error_message_ref_{};
|
||||
void set_error_message(const StringRef &ref) { this->error_message_ref_ = ref; }
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
const uint8_t *response_data{nullptr};
|
||||
uint16_t response_data_len{0};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
@@ -1605,12 +1565,11 @@ class SelectStateResponse final : public StateResponseProtoMessage {
|
||||
class SelectCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 54;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 28;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 18;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "select_command_request"; }
|
||||
#endif
|
||||
const uint8_t *state{nullptr};
|
||||
uint16_t state_len{0};
|
||||
std::string state{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -231,20 +231,6 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::SupportsResponseType value) {
|
||||
switch (value) {
|
||||
case enums::SUPPORTS_RESPONSE_NONE:
|
||||
return "SUPPORTS_RESPONSE_NONE";
|
||||
case enums::SUPPORTS_RESPONSE_OPTIONAL:
|
||||
return "SUPPORTS_RESPONSE_OPTIONAL";
|
||||
case enums::SUPPORTS_RESPONSE_ONLY:
|
||||
return "SUPPORTS_RESPONSE_ONLY";
|
||||
case enums::SUPPORTS_RESPONSE_STATUS:
|
||||
return "SUPPORTS_RESPONSE_STATUS";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||
@@ -999,9 +985,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "has_flash_length", this->has_flash_length);
|
||||
dump_field(out, "flash_length", this->flash_length);
|
||||
dump_field(out, "has_effect", this->has_effect);
|
||||
out.append(" effect: ");
|
||||
out.append(format_hex_pretty(this->effect, this->effect_len));
|
||||
out.append("\n");
|
||||
dump_field(out, "effect", this->effect);
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
@@ -1210,7 +1194,6 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response));
|
||||
}
|
||||
void ExecuteServiceArgument::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ExecuteServiceArgument");
|
||||
@@ -1240,25 +1223,6 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
dump_field(out, "return_response", this->return_response);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void ExecuteServiceResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ExecuteServiceResponse");
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
dump_field(out, "success", this->success);
|
||||
dump_field(out, "error_message", this->error_message_ref_);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
out.append(" response_data: ");
|
||||
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
||||
out.append("\n");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
@@ -1455,9 +1419,7 @@ void SelectStateResponse::dump_to(std::string &out) const {
|
||||
void SelectCommandRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "SelectCommandRequest");
|
||||
dump_field(out, "key", this->key);
|
||||
out.append(" state: ");
|
||||
out.append(format_hex_pretty(this->state, this->state_len));
|
||||
out.append("\n");
|
||||
dump_field(out, "state", this->state);
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include "api_connection.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
@@ -186,9 +186,6 @@ void APIServer::loop() {
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
|
||||
|
||||
@@ -419,56 +416,25 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper to add subscription (reduces duplication)
|
||||
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
|
||||
std::function<void(std::string)> f, bool once) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
|
||||
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to add subscription with heap-allocated strings (reduces duplication)
|
||||
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f, bool once) {
|
||||
HomeAssistantStateSubscription sub;
|
||||
// Allocate heap storage for the strings
|
||||
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
|
||||
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
|
||||
|
||||
if (attribute.has_value()) {
|
||||
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
|
||||
sub.attribute = sub.attribute_dynamic_storage->c_str();
|
||||
} else {
|
||||
sub.attribute = nullptr;
|
||||
}
|
||||
|
||||
sub.callback = std::move(f);
|
||||
sub.once = once;
|
||||
this->state_subs_.push_back(std::move(sub));
|
||||
}
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
|
||||
}
|
||||
|
||||
// Existing std::string overload (for custom_api_device.h - heap allocation)
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = false,
|
||||
});
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = true,
|
||||
});
|
||||
};
|
||||
|
||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
||||
return this->state_subs_;
|
||||
@@ -619,84 +585,5 @@ bool APIServer::teardown() {
|
||||
return this->clients_.empty();
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
|
||||
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
|
||||
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
|
||||
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
|
||||
#endif
|
||||
|
||||
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
|
||||
uint32_t action_call_id = this->next_action_call_id_++;
|
||||
// Handle wraparound (skip 0 as it means "no call")
|
||||
if (this->next_action_call_id_ == 0) {
|
||||
this->next_action_call_id_ = 1;
|
||||
}
|
||||
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
|
||||
|
||||
// Schedule automatic cleanup after timeout (client will have given up by then)
|
||||
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
|
||||
[this, action_call_id]() {
|
||||
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
|
||||
this->unregister_active_action_call(action_call_id);
|
||||
});
|
||||
|
||||
return action_call_id;
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
|
||||
|
||||
// Swap-and-pop is more efficient than remove_if for unordered vectors
|
||||
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
|
||||
if (this->active_action_calls_[i].action_call_id == action_call_id) {
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
|
||||
// Remove all active action calls for disconnected connection using swap-and-pop
|
||||
for (size_t i = 0; i < this->active_action_calls_.size();) {
|
||||
if (this->active_action_calls_[i].connection == conn) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
|
||||
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
// Don't increment i - need to check the swapped element
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
|
||||
response_data_len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
@@ -19,15 +22,11 @@
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
// Forward declaration - full definition in user_services.h
|
||||
class UserServiceDescriptor;
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
psk_t psk;
|
||||
@@ -155,19 +154,6 @@ class APIServer : public Component,
|
||||
// Only compile push_back method when custom_services: true (external components)
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Action call context management - supports concurrent calls from multiple clients
|
||||
// Returns server-generated action_call_id to avoid collisions when clients use same call_id
|
||||
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn);
|
||||
void unregister_active_action_call(uint32_t action_call_id);
|
||||
void unregister_active_action_calls_for_connection(APIConnection *conn);
|
||||
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void request_time();
|
||||
@@ -190,27 +176,16 @@ class APIServer : public Component,
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
const char *entity_id; // Pointer to flash (internal) or heap (external)
|
||||
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
|
||||
std::string entity_id;
|
||||
optional<std::string> attribute;
|
||||
std::function<void(std::string)> callback;
|
||||
bool once;
|
||||
|
||||
// Dynamic storage for external components using std::string API (custom_api_device.h)
|
||||
// These are only allocated when using the std::string overload (nullptr for const char* overload)
|
||||
std::unique_ptr<std::string> entity_id_dynamic_storage;
|
||||
std::unique_ptr<std::string> attribute_dynamic_storage;
|
||||
};
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
|
||||
|
||||
// Existing std::string overload (for custom_api_device.h - heap allocation)
|
||||
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
@@ -231,13 +206,6 @@ class APIServer : public Component,
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
#endif // USE_API_NOISE
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper methods to reduce code duplication
|
||||
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(std::string)> f,
|
||||
bool once);
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f, bool once);
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
@@ -262,17 +230,6 @@ class APIServer : public Component,
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
std::vector<UserServiceDescriptor *> user_services_;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Active action calls - supports concurrent calls from multiple clients
|
||||
// Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id
|
||||
struct ActiveActionCall {
|
||||
uint32_t action_call_id; // Server-generated unique ID (passed to actions)
|
||||
uint32_t client_call_id; // Client's original call_id (used in response)
|
||||
APIConnection *connection;
|
||||
};
|
||||
std::vector<ActiveActionCall> active_action_calls_;
|
||||
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
struct PendingActionResponse {
|
||||
|
||||
@@ -16,10 +16,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
|
||||
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
|
||||
protected:
|
||||
// CustomAPIDevice services don't support action responses - ignore call_id and return_response
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
|
||||
(this->obj_->*this->callback_)(x...); // NOLINT
|
||||
}
|
||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||
|
||||
T *obj_;
|
||||
void (T::*callback_)(Ts...);
|
||||
|
||||
@@ -12,17 +12,10 @@
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||
// Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation)
|
||||
// rather than being wrapped in a lambda. The base class constructor for const char* is more
|
||||
// specialized than the templated constructor here, so it should be selected.
|
||||
static_assert(std::is_constructible_v<TemplatableValue<std::string, X...>, const char *>,
|
||||
"Base class must have const char* constructor for STATIC_STRING optimization");
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
@@ -53,25 +46,23 @@ template<typename... Ts> class TemplatableKeyValuePair {
|
||||
|
||||
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
|
||||
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
|
||||
// Using const char* avoids std::string heap allocation - keys remain in flash.
|
||||
template<typename T> TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {}
|
||||
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
||||
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
||||
|
||||
const char *key{nullptr};
|
||||
std::string key;
|
||||
TemplatableStringValue<Ts...> value;
|
||||
};
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
// Represents the response data from a Home Assistant action
|
||||
// Note: This class holds a StringRef to the error_message from the protobuf message.
|
||||
// The protobuf message must outlive the ActionResponse (which is guaranteed since
|
||||
// the callback is invoked synchronously while the message is on the stack).
|
||||
class ActionResponse {
|
||||
public:
|
||||
ActionResponse(bool success, const std::string &error_message) : success_(success), error_message_(error_message) {}
|
||||
ActionResponse(bool success, std::string error_message = "")
|
||||
: success_(success), error_message_(std::move(error_message)) {}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
ActionResponse(bool success, const std::string &error_message, const uint8_t *data, size_t data_len)
|
||||
: success_(success), error_message_(error_message) {
|
||||
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
|
||||
: success_(success), error_message_(std::move(error_message)) {
|
||||
if (data == nullptr || data_len == 0)
|
||||
return;
|
||||
this->json_document_ = json::parse_json(data, data_len);
|
||||
@@ -79,8 +70,7 @@ class ActionResponse {
|
||||
#endif
|
||||
|
||||
bool is_success() const { return this->success_; }
|
||||
// Returns reference to error message - can be implicitly converted to std::string if needed
|
||||
const StringRef &get_error_message() const { return this->error_message_; }
|
||||
const std::string &get_error_message() const { return this->error_message_; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
// Get data as parsed JSON object (const version returns read-only view)
|
||||
@@ -89,7 +79,7 @@ class ActionResponse {
|
||||
|
||||
protected:
|
||||
bool success_;
|
||||
StringRef error_message_;
|
||||
std::string error_message_;
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
JsonDocument json_document_;
|
||||
#endif
|
||||
@@ -115,15 +105,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
|
||||
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
|
||||
// The value parameter can be a lambda/template, but keys are never templatable.
|
||||
// Using const char* for keys avoids std::string heap allocation - keys remain in flash.
|
||||
template<typename V> void add_data(const char *key, V &&value) {
|
||||
this->add_kv_(this->data_, key, std::forward<V>(value));
|
||||
template<typename K, typename V> void add_data(K &&key, V &&value) {
|
||||
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_data_template(const char *key, V &&value) {
|
||||
this->add_kv_(this->data_template_, key, std::forward<V>(value));
|
||||
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
|
||||
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_variable(const char *key, V &&value) {
|
||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||
template<typename K, typename V> void add_variable(K &&key, V &&value) {
|
||||
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -196,11 +185,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
protected:
|
||||
// Helper to add key-value pairs to FixedVectors
|
||||
// Keys are always string literals (const char*), values can be lambdas/templates
|
||||
template<typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, const char *key, V &&value) {
|
||||
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
|
||||
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
|
||||
auto &kv = vec.emplace_back();
|
||||
kv.key = key;
|
||||
kv.key = std::forward<K>(key);
|
||||
kv.value = std::forward<V>(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
#include "api_pb2.h"
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
namespace esphome::api {
|
||||
|
||||
// Forward declaration - full definition in api_server.h
|
||||
class APIServer;
|
||||
|
||||
class UserServiceDescriptor {
|
||||
public:
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Overload that accepts server-generated action_call_id (avoids client call_id collisions)
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0;
|
||||
#endif
|
||||
|
||||
bool is_internal() { return false; }
|
||||
};
|
||||
@@ -38,9 +27,8 @@ template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
// Stores only pointers to string literals in flash - no heap allocation
|
||||
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
public:
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
|
||||
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
|
||||
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: name_(name), arg_names_(arg_names) {
|
||||
this->key_ = fnv1_hash(name);
|
||||
}
|
||||
|
||||
@@ -48,7 +36,6 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
@@ -64,37 +51,21 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#else
|
||||
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#endif
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
|
||||
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
// Pointers to string literals in flash - no heap allocation
|
||||
const char *name_;
|
||||
std::array<const char *, sizeof...(Ts)> arg_names_;
|
||||
uint32_t key_{0};
|
||||
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
|
||||
};
|
||||
|
||||
// Separate class for custom_api_device services (rare case)
|
||||
@@ -110,7 +81,6 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
@@ -126,31 +96,15 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#else
|
||||
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#endif
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Dynamic services don't support responses yet, but need to implement the interface
|
||||
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
|
||||
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
// Heap-allocated strings for runtime-generated names
|
||||
@@ -159,149 +113,15 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
uint32_t key_{0};
|
||||
};
|
||||
|
||||
// Primary template declaration
|
||||
template<enums::SupportsResponseType Mode, typename... Ts> class UserServiceTrigger;
|
||||
|
||||
// Specialization for NONE - no extra trigger arguments
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||
public:
|
||||
// Constructor for static names (YAML-defined services - used by code generator)
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
|
||||
: UserServiceBase<Ts...>(name, arg_names) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
|
||||
};
|
||||
|
||||
// Specialization for OPTIONAL - call_id and return_response trigger arguments
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, bool, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool return_response, Ts... x) override {
|
||||
this->trigger(call_id, return_response, x...);
|
||||
}
|
||||
};
|
||||
|
||||
// Specialization for ONLY - just call_id trigger argument
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
};
|
||||
|
||||
// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
void execute(Ts... x) override { this->trigger(x...); } // NOLINT
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Include full definition of APIServer for template implementation
|
||||
// Must be outside namespace to avoid including STL headers inside namespace
|
||||
#include "api_server.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit APIRespondAction(APIServer *parent) : parent_(parent) {}
|
||||
|
||||
template<typename V> void set_success(V success) { this->success_ = success; }
|
||||
template<typename V> void set_error_message(V error) { this->error_message_ = error; }
|
||||
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void set_data(std::function<void(Ts..., JsonObject)> func) {
|
||||
this->json_builder_ = std::move(func);
|
||||
this->has_data_ = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - it's always first for optional/only/status modes
|
||||
auto args = std::make_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
|
||||
bool success = this->success_.value(x...);
|
||||
std::string error_message = this->error_message_.value(x...);
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
if (this->has_data_) {
|
||||
// For optional mode, check return_response (second arg) to decide if client wants data
|
||||
// Use nested if constexpr to avoid compile error when tuple doesn't have enough elements
|
||||
// (std::tuple_element_t is evaluated before the && short-circuit, so we must nest)
|
||||
if constexpr (sizeof...(Ts) >= 2) {
|
||||
if constexpr (std::is_same_v<std::tuple_element_t<1, std::tuple<Ts...>>, bool>) {
|
||||
if (this->is_optional_mode_) {
|
||||
bool return_response = std::get<1>(args);
|
||||
if (!return_response) {
|
||||
// Client doesn't want response data, just send success/error
|
||||
this->parent_->send_action_response(call_id, success, error_message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
std::string json_str = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, error_message,
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->parent_->send_action_response(call_id, success, error_message);
|
||||
}
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
TemplatableValue<bool, Ts...> success_{true};
|
||||
TemplatableValue<std::string, Ts...> error_message_{""};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
std::function<void(Ts..., JsonObject)> json_builder_;
|
||||
bool has_data_{false};
|
||||
#endif
|
||||
bool is_optional_mode_{false};
|
||||
};
|
||||
|
||||
// Action to unregister a service call after execution completes
|
||||
// Automatically appended to the end of action lists for non-none response modes
|
||||
template<typename... Ts> class APIUnregisterServiceCallAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - same convention as APIRespondAction
|
||||
auto args = std::make_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
if (call_id != 0) {
|
||||
this->parent_->unregister_active_action_call(call_id);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
@@ -44,7 +44,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
|
||||
"The 'ble_client_id' option has been removed. Please migrate "
|
||||
"to the new `bedjet_id` option in the `bedjet` component.\n"
|
||||
"See https://esphome.io/components/climate/bedjet/"
|
||||
"See https://esphome.io/components/climate/bedjet.html"
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.invalid(
|
||||
"The 'time_id' option has been moved to the `bedjet` component."
|
||||
|
||||
@@ -34,20 +34,13 @@ void BinarySensor::publish_initial_state(bool new_state) {
|
||||
void BinarySensor::send_state_internal(bool new_state) {
|
||||
// copy the new state to the visible property for backwards compatibility, before any callbacks
|
||||
this->state = new_state;
|
||||
// Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed
|
||||
this->set_new_state(new_state);
|
||||
}
|
||||
|
||||
bool BinarySensor::set_new_state(const optional<bool> &new_state) {
|
||||
if (StatefulEntityBase::set_new_state(new_state)) {
|
||||
// weirdly, this file could be compiled even without USE_BINARY_SENSOR defined
|
||||
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
|
||||
if (this->set_state_(new_state)) {
|
||||
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
|
||||
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_binary_sensor_update(this);
|
||||
#endif
|
||||
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BinarySensor::add_filter(Filter *filter) {
|
||||
|
||||
@@ -61,8 +61,6 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
|
||||
|
||||
protected:
|
||||
Filter *filter_list_{nullptr};
|
||||
|
||||
bool set_new_state(const optional<bool> &new_state) override;
|
||||
};
|
||||
|
||||
class BinarySensorInitiallyOff : public BinarySensor {
|
||||
|
||||
@@ -69,7 +69,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on_esp8266,
|
||||
cv.All(
|
||||
cv.only_on_esp32,
|
||||
esp32.only_on_variant(supported=[esp32.VARIANT_ESP32]),
|
||||
esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
from esphome import automation, pins
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_DATA,
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_WAIT_TIME,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.const import CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, CONF_WAIT_TIME
|
||||
|
||||
CODEOWNERS = ["@lygris", "@gabest11"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -37,6 +29,7 @@ CONF_MANCHESTER = "manchester"
|
||||
CONF_NUM_PREAMBLE = "num_preamble"
|
||||
CONF_SYNC1 = "sync1"
|
||||
CONF_SYNC0 = "sync0"
|
||||
CONF_PKTLEN = "pktlen"
|
||||
CONF_MAGN_TARGET = "magn_target"
|
||||
CONF_MAX_LNA_GAIN = "max_lna_gain"
|
||||
CONF_MAX_DVGA_GAIN = "max_dvga_gain"
|
||||
@@ -48,12 +41,6 @@ CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook"
|
||||
CONF_FREEZE = "freeze"
|
||||
CONF_HYST_LEVEL = "hyst_level"
|
||||
|
||||
# Packet mode config keys
|
||||
CONF_PACKET_MODE = "packet_mode"
|
||||
CONF_PACKET_LENGTH = "packet_length"
|
||||
CONF_WHITENING = "whitening"
|
||||
CONF_GDO0_PIN = "gdo0_pin"
|
||||
|
||||
# Enums
|
||||
SyncMode = ns.enum("SyncMode", True)
|
||||
SYNC_MODE = {
|
||||
@@ -165,12 +152,12 @@ CONFIG_MAP = {
|
||||
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
|
||||
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
|
||||
CONF_DC_BLOCKING_FILTER: cv.boolean,
|
||||
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)),
|
||||
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
|
||||
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
|
||||
CONF_FREQUENCY: cv.float_range(min=300000.0, max=928000.0),
|
||||
CONF_IF_FREQUENCY: cv.float_range(min=25, max=788),
|
||||
CONF_FILTER_BANDWIDTH: cv.float_range(min=58.0, max=812.0),
|
||||
CONF_CHANNEL: cv.uint8_t,
|
||||
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
|
||||
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
|
||||
CONF_CHANNEL_SPACING: cv.float_range(min=25, max=405),
|
||||
CONF_FSK_DEVIATION: cv.float_range(min=1.5, max=381),
|
||||
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
|
||||
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
|
||||
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
|
||||
@@ -180,6 +167,7 @@ CONFIG_MAP = {
|
||||
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
|
||||
CONF_SYNC1: cv.hex_uint8_t,
|
||||
CONF_SYNC0: cv.hex_uint8_t,
|
||||
CONF_PKTLEN: cv.uint8_t,
|
||||
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
|
||||
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
|
||||
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
@@ -191,36 +179,13 @@ CONFIG_MAP = {
|
||||
CONF_FREEZE: cv.enum(FREEZE, upper=False),
|
||||
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
|
||||
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
|
||||
CONF_PACKET_MODE: cv.boolean,
|
||||
CONF_PACKET_LENGTH: cv.uint8_t,
|
||||
CONF_CRC_ENABLE: cv.boolean,
|
||||
CONF_WHITENING: cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
def _validate_packet_mode(config):
|
||||
if config.get(CONF_PACKET_MODE, False):
|
||||
if CONF_GDO0_PIN not in config:
|
||||
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
|
||||
if CONF_PACKET_LENGTH not in config:
|
||||
raise cv.Invalid("packet_length is required when packet_mode is enabled")
|
||||
if config[CONF_PACKET_LENGTH] > 64:
|
||||
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CC1101Component),
|
||||
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({cv.GenerateID(): cv.declare_id(CC1101Component)})
|
||||
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True)),
|
||||
_validate_packet_mode,
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True))
|
||||
)
|
||||
|
||||
|
||||
@@ -233,29 +198,12 @@ async def to_code(config):
|
||||
if key in config:
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
if CONF_GDO0_PIN in config:
|
||||
gdo0_pin = await cg.gpio_pin_expression(config[CONF_GDO0_PIN])
|
||||
cg.add(var.set_gdo0_pin(gdo0_pin))
|
||||
if CONF_ON_PACKET in config:
|
||||
await automation.build_automation(
|
||||
var.get_packet_trigger(),
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.float_, "rssi"),
|
||||
(cg.uint8, "lqi"),
|
||||
],
|
||||
config[CONF_ON_PACKET],
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
BeginTxAction = ns.class_("BeginTxAction", automation.Action)
|
||||
BeginRxAction = ns.class_("BeginRxAction", automation.Action)
|
||||
ResetAction = ns.class_("ResetAction", automation.Action)
|
||||
SetIdleAction = ns.class_("SetIdleAction", automation.Action)
|
||||
SendPacketAction = ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
|
||||
)
|
||||
|
||||
CC1101_ACTION_SCHEMA = cv.Schema(
|
||||
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
|
||||
@@ -270,42 +218,3 @@ async def cc1101_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
def validate_raw_data(value):
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
if isinstance(value, list):
|
||||
return cv.Schema([cv.hex_uint8_t])(value)
|
||||
raise cv.Invalid(
|
||||
"data must either be a string wrapped in quotes or a list of bytes"
|
||||
)
|
||||
|
||||
|
||||
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(CC1101Component),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
|
||||
)
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
data = config[CONF_DATA]
|
||||
if isinstance(data, bytes):
|
||||
data = list(data)
|
||||
if cg.is_template(data):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
@@ -143,11 +143,6 @@ void CC1101Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
@@ -156,69 +151,8 @@ void CC1101Component::setup() {
|
||||
}
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->write_(Register::PATABLE, this->pa_table_, sizeof(this->pa_table_));
|
||||
this->strobe_(Command::RX);
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read state
|
||||
this->read_(Register::RXBYTES);
|
||||
uint8_t rx_bytes = this->state_.NUM_RXBYTES;
|
||||
bool overflow = this->state_.RXFIFO_OVERFLOW;
|
||||
if (overflow || rx_bytes == 0) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, flushing");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read packet
|
||||
uint8_t payload_length;
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->read_(Register::FIFO, &payload_length, 1);
|
||||
} else {
|
||||
payload_length = this->state_.PKTLEN;
|
||||
}
|
||||
if (payload_length == 0 || payload_length > 64) {
|
||||
ESP_LOGW(TAG, "Invalid payload length: %u", payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return;
|
||||
}
|
||||
this->packet_.resize(payload_length);
|
||||
this->read_(Register::FIFO, this->packet_.data(), payload_length);
|
||||
|
||||
// Read status and trigger
|
||||
uint8_t status[2];
|
||||
this->read_(Register::FIFO, status, 2);
|
||||
int8_t rssi_raw = static_cast<int8_t>(status[0]);
|
||||
float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = status[1] & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
}
|
||||
|
||||
void CC1101Component::dump_config() {
|
||||
@@ -226,29 +160,27 @@ void CC1101Component::dump_config() {
|
||||
"4-FSK", "UNUSED", "UNUSED", "MSK"};
|
||||
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
float symbol_rate =
|
||||
(((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY * 1000.0f;
|
||||
float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E));
|
||||
ESP_LOGCONFIG(TAG, "CC1101:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Chip ID: 0x%04X\n"
|
||||
" Frequency: %" PRId32 " Hz\n"
|
||||
" Frequency: %" PRId32 " kHz\n"
|
||||
" Channel: %u\n"
|
||||
" Modulation: %s\n"
|
||||
" Symbol Rate: %.0f baud\n"
|
||||
" Filter Bandwidth: %.1f Hz\n"
|
||||
" Filter Bandwidth: %.1f kHz\n"
|
||||
" Output Power: %.1f dBm",
|
||||
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
|
||||
symbol_rate, bw, this->output_power_effective_);
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
// Ensure Packet Format is 3 (Async Serial)
|
||||
// Ensure Packet Format is 3 (Async Serial), use GDO0 as input during TX
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::TX, 50)) {
|
||||
ESP_LOGW(TAG, "Timed out waiting for TX state!");
|
||||
@@ -257,9 +189,6 @@ void CC1101Component::begin_tx() {
|
||||
|
||||
void CC1101Component::begin_rx() {
|
||||
ESP_LOGV(TAG, "Beginning RX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
this->strobe_(Command::RX);
|
||||
}
|
||||
|
||||
@@ -273,6 +202,20 @@ void CC1101Component::set_idle() {
|
||||
this->enter_idle_();
|
||||
}
|
||||
|
||||
void CC1101Component::set_gdo0_config(uint8_t value) {
|
||||
this->state_.GDO0_CFG = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::IOCFG0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_gdo2_config(uint8_t value) {
|
||||
this->state_.GDO2_CFG = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::IOCFG2);
|
||||
}
|
||||
}
|
||||
|
||||
bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < timeout_ms) {
|
||||
@@ -340,46 +283,19 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet) {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
|
||||
// Write packet
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FTX);
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
|
||||
}
|
||||
this->write_(Register::FIFO, packet.data(), packet.size());
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::IDLE, 1000)) {
|
||||
ESP_LOGW(TAG, "TX timeout");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return CC1101Error::TIMEOUT;
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return CC1101Error::NONE;
|
||||
}
|
||||
|
||||
// Setters
|
||||
void CC1101Component::set_output_power(float value) {
|
||||
this->output_power_requested_ = value;
|
||||
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
uint8_t a = 0xC0;
|
||||
if (freq >= 300000000 && freq <= 348000000) {
|
||||
if (freq >= 300000 && freq <= 348000) {
|
||||
a = PowerTableItem::find(PA_TABLE_315, sizeof(PA_TABLE_315) / sizeof(PA_TABLE_315[0]), value);
|
||||
} else if (freq >= 378000000 && freq <= 464000000) {
|
||||
} else if (freq >= 378000 && freq <= 464000) {
|
||||
a = PowerTableItem::find(PA_TABLE_433, sizeof(PA_TABLE_433) / sizeof(PA_TABLE_433[0]), value);
|
||||
} else if (freq >= 779000000 && freq < 900000000) {
|
||||
} else if (freq >= 779000 && freq < 900000) {
|
||||
a = PowerTableItem::find(PA_TABLE_868, sizeof(PA_TABLE_868) / sizeof(PA_TABLE_868[0]), value);
|
||||
} else if (freq >= 900000000 && freq <= 928000000) {
|
||||
} else if (freq >= 900000 && freq <= 928000) {
|
||||
a = PowerTableItem::find(PA_TABLE_915, sizeof(PA_TABLE_915) / sizeof(PA_TABLE_915[0]), value);
|
||||
}
|
||||
|
||||
@@ -485,7 +401,7 @@ void CC1101Component::set_msk_deviation(uint8_t value) {
|
||||
void CC1101Component::set_symbol_rate(float value) {
|
||||
uint8_t e;
|
||||
uint32_t m;
|
||||
split_float(value * (1 << 28) / XTAL_FREQUENCY, 8, e, m);
|
||||
split_float(value * (1 << 28) / (XTAL_FREQUENCY * 1000), 8, e, m);
|
||||
this->state_.DRATE_E = e;
|
||||
this->state_.DRATE_M = static_cast<uint8_t>(m);
|
||||
if (this->initialized_) {
|
||||
@@ -513,7 +429,6 @@ void CC1101Component::set_modulation_type(Modulation value) {
|
||||
this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->write_(Register::MDMCFG2);
|
||||
this->write_(Register::FREND0);
|
||||
this->strobe_(Command::RX);
|
||||
@@ -548,6 +463,13 @@ void CC1101Component::set_sync0(uint8_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_pktlen(uint8_t value) {
|
||||
this->state_.PKTLEN = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_magn_target(MagnTarget value) {
|
||||
this->state_.MAGN_TARGET = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
@@ -625,50 +547,4 @@ void CC1101Component::set_hyst_level(HystLevel value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.PKT_FORMAT =
|
||||
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
if (value) {
|
||||
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
|
||||
this->state_.FIFO_THR = 15;
|
||||
} else {
|
||||
// Configure GDO0 for serial data (async serial mode)
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_length(uint8_t value) {
|
||||
if (value == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = value;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_crc_enable(bool value) {
|
||||
this->state_.CRC_EN = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_whitening(bool value) {
|
||||
this->state_.WHITE_DATA = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cc1101defs.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW };
|
||||
|
||||
class CC1101Component : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||
@@ -18,7 +15,6 @@ class CC1101Component : public Component,
|
||||
CC1101Component();
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
// Actions
|
||||
@@ -28,7 +24,8 @@ class CC1101Component : public Component,
|
||||
void set_idle();
|
||||
|
||||
// GDO Pin Configuration
|
||||
void set_gdo0_pin(InternalGPIOPin *pin) { this->gdo0_pin_ = pin; }
|
||||
void set_gdo0_config(uint8_t value);
|
||||
void set_gdo2_config(uint8_t value);
|
||||
|
||||
// Configuration Setters
|
||||
void set_output_power(float value);
|
||||
@@ -51,6 +48,7 @@ class CC1101Component : public Component,
|
||||
void set_num_preamble(uint8_t value);
|
||||
void set_sync1(uint8_t value);
|
||||
void set_sync0(uint8_t value);
|
||||
void set_pktlen(uint8_t value);
|
||||
|
||||
// AGC settings
|
||||
void set_magn_target(MagnTarget value);
|
||||
@@ -65,16 +63,6 @@ class CC1101Component : public Component,
|
||||
void set_wait_time(WaitTime value);
|
||||
void set_hyst_level(HystLevel value);
|
||||
|
||||
// Packet mode settings
|
||||
void set_packet_mode(bool value);
|
||||
void set_packet_length(uint8_t value);
|
||||
void set_crc_enable(bool value);
|
||||
void set_whitening(bool value);
|
||||
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
bool initialized_{false};
|
||||
@@ -85,13 +73,6 @@ class CC1101Component : public Component,
|
||||
|
||||
CC1101State state_;
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
|
||||
// Packet handling
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, uint8_t>()};
|
||||
std::vector<uint8_t> packet_;
|
||||
|
||||
// Low-level Helpers
|
||||
uint8_t strobe_(Command cmd);
|
||||
void write_(Register reg);
|
||||
@@ -126,28 +107,4 @@ template<typename... Ts> class SetIdleAction : public Action<Ts...>, public Pare
|
||||
void play(const Ts &...x) override { this->parent_->set_idle(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { this->data_func_ = func; }
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_static_ = data;
|
||||
this->data_static_len_ = len;
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->data_func_) {
|
||||
auto data = this->data_func_(x...);
|
||||
this->parent_->transmit_packet(data);
|
||||
} else if (this->data_static_ != nullptr) {
|
||||
std::vector<uint8_t> data(this->data_static_, this->data_static_ + this->data_static_len_);
|
||||
this->parent_->transmit_packet(data);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
const uint8_t *data_static_{nullptr};
|
||||
size_t data_static_len_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
|
||||
@@ -4,13 +4,7 @@
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
static constexpr float XTAL_FREQUENCY = 26000000;
|
||||
|
||||
static constexpr float RSSI_OFFSET = 74.0f;
|
||||
static constexpr float RSSI_STEP = 0.5f;
|
||||
|
||||
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
|
||||
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
|
||||
static constexpr float XTAL_FREQUENCY = 26000;
|
||||
|
||||
static constexpr uint8_t BUS_BURST = 0x40;
|
||||
static constexpr uint8_t BUS_READ = 0x80;
|
||||
@@ -140,10 +134,6 @@ enum class SyncMode : uint8_t {
|
||||
SYNC_MODE_15_16,
|
||||
SYNC_MODE_16_16,
|
||||
SYNC_MODE_30_32,
|
||||
SYNC_MODE_NONE_CS,
|
||||
SYNC_MODE_15_16_CS,
|
||||
SYNC_MODE_16_16_CS,
|
||||
SYNC_MODE_30_32_CS,
|
||||
};
|
||||
|
||||
enum class Modulation : uint8_t {
|
||||
@@ -228,19 +218,6 @@ enum class HystLevel : uint8_t {
|
||||
HYST_LEVEL_HIGH,
|
||||
};
|
||||
|
||||
enum class PacketFormat : uint8_t {
|
||||
PACKET_FORMAT_FIFO,
|
||||
PACKET_FORMAT_SYNC_SERIAL,
|
||||
PACKET_FORMAT_RANDOM_TX,
|
||||
PACKET_FORMAT_ASYNC_SERIAL,
|
||||
};
|
||||
|
||||
enum class LengthConfig : uint8_t {
|
||||
LENGTH_CONFIG_FIXED,
|
||||
LENGTH_CONFIG_VARIABLE,
|
||||
LENGTH_CONFIG_INFINITE,
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) CC1101State {
|
||||
// Byte array accessors for bulk SPI transfers
|
||||
uint8_t *regs() { return reinterpret_cast<uint8_t *>(this); }
|
||||
|
||||
@@ -275,13 +275,10 @@ async def setup_climate_core_(var, config):
|
||||
|
||||
visual = config[CONF_VISUAL]
|
||||
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
cg.add(var.set_visual_min_temperature_override(min_temp))
|
||||
if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
cg.add(var.set_visual_max_temperature_override(max_temp))
|
||||
if (temp_step := visual.get(CONF_TEMPERATURE_STEP)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
cg.add(
|
||||
var.set_visual_temperature_step_override(
|
||||
temp_step[CONF_TARGET_TEMPERATURE],
|
||||
@@ -289,10 +286,8 @@ async def setup_climate_core_(var, config):
|
||||
)
|
||||
)
|
||||
if (min_humidity := visual.get(CONF_MIN_HUMIDITY)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
cg.add(var.set_visual_min_humidity_override(min_humidity))
|
||||
if (max_humidity := visual.get(CONF_MAX_HUMIDITY)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
cg.add(var.set_visual_max_humidity_override(max_humidity))
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
|
||||
@@ -473,28 +473,26 @@ void Climate::publish_state() {
|
||||
|
||||
ClimateTraits Climate::get_traits() {
|
||||
auto traits = this->traits();
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
if (!std::isnan(this->visual_min_temperature_override_)) {
|
||||
traits.set_visual_min_temperature(this->visual_min_temperature_override_);
|
||||
if (this->visual_min_temperature_override_.has_value()) {
|
||||
traits.set_visual_min_temperature(*this->visual_min_temperature_override_);
|
||||
}
|
||||
if (!std::isnan(this->visual_max_temperature_override_)) {
|
||||
traits.set_visual_max_temperature(this->visual_max_temperature_override_);
|
||||
if (this->visual_max_temperature_override_.has_value()) {
|
||||
traits.set_visual_max_temperature(*this->visual_max_temperature_override_);
|
||||
}
|
||||
if (!std::isnan(this->visual_target_temperature_step_override_)) {
|
||||
traits.set_visual_target_temperature_step(this->visual_target_temperature_step_override_);
|
||||
traits.set_visual_current_temperature_step(this->visual_current_temperature_step_override_);
|
||||
if (this->visual_target_temperature_step_override_.has_value()) {
|
||||
traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_);
|
||||
traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_);
|
||||
}
|
||||
if (!std::isnan(this->visual_min_humidity_override_)) {
|
||||
traits.set_visual_min_humidity(this->visual_min_humidity_override_);
|
||||
if (this->visual_min_humidity_override_.has_value()) {
|
||||
traits.set_visual_min_humidity(*this->visual_min_humidity_override_);
|
||||
}
|
||||
if (!std::isnan(this->visual_max_humidity_override_)) {
|
||||
traits.set_visual_max_humidity(this->visual_max_humidity_override_);
|
||||
if (this->visual_max_humidity_override_.has_value()) {
|
||||
traits.set_visual_max_humidity(*this->visual_max_humidity_override_);
|
||||
}
|
||||
#endif
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
@@ -515,7 +513,6 @@ void Climate::set_visual_min_humidity_override(float visual_min_humidity_overrid
|
||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
ClimateCall Climate::make_call() { return ClimateCall(this); }
|
||||
|
||||
|
||||
@@ -213,13 +213,11 @@ class Climate : public EntityBase {
|
||||
*/
|
||||
ClimateTraits get_traits();
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override);
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override);
|
||||
void set_visual_temperature_step_override(float target, float current);
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
#endif
|
||||
|
||||
/// Check if a custom fan mode is currently active.
|
||||
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
|
||||
@@ -323,14 +321,12 @@ class Climate : public EntityBase {
|
||||
CallbackManager<void(Climate &)> state_callback_{};
|
||||
CallbackManager<void(ClimateCall &)> control_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
float visual_min_temperature_override_{NAN};
|
||||
float visual_max_temperature_override_{NAN};
|
||||
float visual_target_temperature_step_override_{NAN};
|
||||
float visual_current_temperature_step_override_{NAN};
|
||||
float visual_min_humidity_override_{NAN};
|
||||
float visual_max_humidity_override_{NAN};
|
||||
#endif
|
||||
optional<float> visual_min_temperature_override_{};
|
||||
optional<float> visual_max_temperature_override_{};
|
||||
optional<float> visual_target_temperature_step_override_{};
|
||||
optional<float> visual_current_temperature_step_override_{};
|
||||
optional<float> visual_min_humidity_override_{};
|
||||
optional<float> visual_max_humidity_override_{};
|
||||
|
||||
private:
|
||||
/** The active custom fan mode (private - enforces use of safe setters).
|
||||
|
||||
@@ -7,11 +7,9 @@ BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, time
|
||||
from esphome.components.esp32 import (
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
@@ -57,11 +54,8 @@ WAKEUP_PINS = {
|
||||
],
|
||||
VARIANT_ESP32C2: [0, 1, 2, 3, 4, 5],
|
||||
VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
|
||||
VARIANT_ESP32C5: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
VARIANT_ESP32C6: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
VARIANT_ESP32C61: [0, 1, 2, 3, 4, 5, 6],
|
||||
VARIANT_ESP32H2: [7, 8, 9, 10, 11, 12, 13, 14],
|
||||
VARIANT_ESP32P4: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
VARIANT_ESP32S2: [
|
||||
0,
|
||||
1,
|
||||
@@ -128,11 +122,8 @@ def _validate_ex1_wakeup_mode(value):
|
||||
if value == "ANY_LOW":
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
],
|
||||
@@ -227,9 +218,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
unsupported=[
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
],
|
||||
msg_prefix="Wakeup from touch",
|
||||
|
||||
@@ -81,7 +81,7 @@ class DeepSleepComponent : public Component {
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace deep_sleep {
|
||||
// | ESP32-C3 | | | | ✓ |
|
||||
// | ESP32-C5 | | (✓) | | (✓) |
|
||||
// | ESP32-C6 | | ✓ | | ✓ |
|
||||
// | ESP32-C61 | | ✓ | | ✓ |
|
||||
// | ESP32-H2 | | ✓ | | |
|
||||
//
|
||||
// Notes:
|
||||
@@ -56,7 +55,7 @@ void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wa
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
@@ -122,9 +121,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// GPIO wakeup - C2, C3, C6, C61 only
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61)
|
||||
// GPIO wakeup - C2, C3, C6 only
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
|
||||
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
|
||||
@@ -157,7 +155,7 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
|
||||
// Touch wakeup - ESP32, S2, S3 only
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
|
||||
esp_sleep_enable_touchpad_wakeup();
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
@@ -41,7 +41,6 @@ AUTO_LOAD = ["split_buffer"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
|
||||
CONF_MINIMUM_UPDATE_INTERVAL = "minimum_update_interval"
|
||||
|
||||
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
|
||||
EPaperBase = epaper_spi_ns.class_(
|
||||
@@ -72,9 +71,6 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
|
||||
minimum_update_interval = update_interval(
|
||||
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
|
||||
)
|
||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||
return (
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
@@ -94,9 +90,9 @@ def model_schema(config):
|
||||
{
|
||||
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
|
||||
update_interval, cv.Range(min=minimum_update_interval)
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_UPDATE_INTERVAL, default=cv.UNDEFINED
|
||||
): update_interval,
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
@@ -157,8 +153,9 @@ def _final_validate(config):
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
config[CONF_UPDATE_INTERVAL] = core.TimePeriod(
|
||||
seconds=60
|
||||
).total_milliseconds
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ void EPaperBase::initialise_() {
|
||||
* @param y
|
||||
* @return false if the coordinates are out of bounds
|
||||
*/
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return false;
|
||||
if (this->transform_ & SWAP_XY)
|
||||
@@ -297,10 +297,6 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
y = this->height_ - y - 1;
|
||||
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
|
||||
return false;
|
||||
this->x_low_ = clamp_at_most(this->x_low_, x);
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -323,6 +319,10 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
|
||||
} else {
|
||||
this->buffer_[byte_position] = original | pixel_bit;
|
||||
}
|
||||
this->x_low_ = clamp_at_most(this->x_low_, x);
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
|
||||
}
|
||||
|
||||
void EPaperBase::dump_config() {
|
||||
|
||||
@@ -106,7 +106,7 @@ class EPaperBase : public Display,
|
||||
void initialise_();
|
||||
void wait_for_idle_(bool should_wait);
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
bool rotate_coordinates_(int &x, int &y) const;
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
|
||||
@@ -4,8 +4,8 @@ from . import EpaperModel
|
||||
|
||||
|
||||
class SpectraE6(EpaperModel):
|
||||
def __init__(self, name, class_name="EPaperSpectraE6", **defaults):
|
||||
super().__init__(name, class_name, **defaults)
|
||||
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
|
||||
super().__init__(name, class_name, **kwargs)
|
||||
|
||||
# fmt: off
|
||||
def get_init_sequence(self, config: dict):
|
||||
@@ -30,7 +30,7 @@ class SpectraE6(EpaperModel):
|
||||
return self.defaults.get(key, fallback)
|
||||
|
||||
|
||||
spectra_e6 = SpectraE6("spectra-e6", minimum_update_interval="30s")
|
||||
spectra_e6 = SpectraE6("spectra-e6")
|
||||
|
||||
spectra_e6_7p3 = spectra_e6.extend(
|
||||
"7.3in-Spectra-E6",
|
||||
|
||||
@@ -59,7 +59,6 @@ from .const import ( # noqa
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
@@ -127,7 +126,6 @@ CPU_FREQUENCIES = {
|
||||
VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
|
||||
VARIANT_ESP32C5: get_cpu_frequencies(80, 160, 240),
|
||||
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
|
||||
VARIANT_ESP32C61: get_cpu_frequencies(80, 120, 160),
|
||||
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
|
||||
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
|
||||
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
|
||||
@@ -764,7 +762,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
|
||||
+ "Need help? Check out the migration guide:\n"
|
||||
+ color(
|
||||
AnsiFore.BLUE,
|
||||
"https://esphome.io/guides/esp32_arduino_to_idf/",
|
||||
"https://esphome.io/guides/esp32_arduino_to_idf.html",
|
||||
)
|
||||
)
|
||||
_LOGGER.warning(message)
|
||||
|
||||
@@ -4,7 +4,6 @@ from .const import (
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
@@ -18,7 +17,6 @@ STANDARD_BOARDS = {
|
||||
VARIANT_ESP32C3: "esp32-c3-devkitm-1",
|
||||
VARIANT_ESP32C5: "esp32-c5-devkitc-1",
|
||||
VARIANT_ESP32C6: "esp32-c6-devkitm-1",
|
||||
VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2",
|
||||
VARIANT_ESP32H2: "esp32-h2-devkitm-1",
|
||||
VARIANT_ESP32P4: "esp32-p4-evboard",
|
||||
VARIANT_ESP32S2: "esp32-s2-kaluga-1",
|
||||
@@ -1218,28 +1216,6 @@ ESP32_BOARD_PINS = {
|
||||
"LED_BUILTINB": 4,
|
||||
},
|
||||
"sensesiot_weizen": {},
|
||||
"seeed_xiao_esp32c6": {
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 2,
|
||||
"D3": 21,
|
||||
"D4": 22,
|
||||
"D5": 23,
|
||||
"D6": 16,
|
||||
"D7": 17,
|
||||
"D8": 19,
|
||||
"D9": 20,
|
||||
"D10": 18,
|
||||
"MTDO": 7,
|
||||
"MTCK": 6,
|
||||
"MTDI": 5,
|
||||
"MTMS": 4,
|
||||
"BOOT": 9,
|
||||
"LED": 8,
|
||||
"LED_BUILTIN": 8,
|
||||
"RF_SWITCH_EN": 3,
|
||||
"RF_ANT_SELECT": 14,
|
||||
},
|
||||
"sg-o_airMon": {},
|
||||
"sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16},
|
||||
"tinypico": {},
|
||||
|
||||
@@ -17,7 +17,6 @@ VARIANT_ESP32C2 = "ESP32C2"
|
||||
VARIANT_ESP32C3 = "ESP32C3"
|
||||
VARIANT_ESP32C5 = "ESP32C5"
|
||||
VARIANT_ESP32C6 = "ESP32C6"
|
||||
VARIANT_ESP32C61 = "ESP32C61"
|
||||
VARIANT_ESP32H2 = "ESP32H2"
|
||||
VARIANT_ESP32P4 = "ESP32P4"
|
||||
VARIANT_ESP32S2 = "ESP32S2"
|
||||
@@ -28,7 +27,6 @@ VARIANTS = [
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
@@ -41,7 +39,6 @@ VARIANT_FRIENDLY = {
|
||||
VARIANT_ESP32C3: "ESP32-C3",
|
||||
VARIANT_ESP32C5: "ESP32-C5",
|
||||
VARIANT_ESP32C6: "ESP32-C6",
|
||||
VARIANT_ESP32C61: "ESP32-C61",
|
||||
VARIANT_ESP32H2: "ESP32-H2",
|
||||
VARIANT_ESP32P4: "ESP32-P4",
|
||||
VARIANT_ESP32S2: "ESP32-S2",
|
||||
|
||||
@@ -29,7 +29,6 @@ from .const import (
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
@@ -41,7 +40,6 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support
|
||||
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
|
||||
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
|
||||
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
|
||||
from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
|
||||
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
|
||||
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
|
||||
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
|
||||
@@ -112,10 +110,6 @@ _esp32_validations = {
|
||||
pin_validation=esp32_c6_validate_gpio_pin,
|
||||
usage_validation=esp32_c6_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C61: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c61_validate_gpio_pin,
|
||||
usage_validation=esp32_c61_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32H2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_h2_validate_gpio_pin,
|
||||
usage_validation=esp32_h2_validate_supports,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
|
||||
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
|
||||
|
||||
_ESP32C5_SPI_PSRAM_PINS = {
|
||||
16: "SPICS0",
|
||||
17: "SPIQ",
|
||||
@@ -46,13 +43,3 @@ def esp32_c5_validate_supports(value):
|
||||
|
||||
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c5_validate_lp_i2c(value):
|
||||
lp_sda_pin = _ESP32C5_I2C_LP_PINS["SDA"]
|
||||
lp_scl_pin = _ESP32C5_I2C_LP_PINS["SCL"]
|
||||
if int(value[CONF_SDA]) != lp_sda_pin or int(value[CONF_SCL]) != lp_scl_pin:
|
||||
raise cv.Invalid(
|
||||
f"Low power i2c interface is only supported on GPIO{lp_sda_pin} SDA and GPIO{lp_scl_pin} SCL for ESP32-C5"
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
|
||||
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
|
||||
|
||||
_ESP32C6_SPI_PSRAM_PINS = {
|
||||
24: "SPICS0",
|
||||
25: "SPIQ",
|
||||
@@ -46,13 +43,3 @@ def esp32_c6_validate_supports(value):
|
||||
|
||||
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c6_validate_lp_i2c(value):
|
||||
lp_sda_pin = _ESP32C6_I2C_LP_PINS["SDA"]
|
||||
lp_scl_pin = _ESP32C6_I2C_LP_PINS["SCL"]
|
||||
if int(value[CONF_SDA]) != lp_sda_pin or int(value[CONF_SCL]) != lp_scl_pin:
|
||||
raise cv.Invalid(
|
||||
f"Low power i2c interface is only supported on GPIO{lp_sda_pin} SDA and GPIO{lp_scl_pin} SCL for ESP32-C6"
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
|
||||
_ESP32C61_SPI_PSRAM_PINS = {
|
||||
14: "SPICS0",
|
||||
15: "SPICLK",
|
||||
16: "SPID",
|
||||
17: "SPIQ",
|
||||
19: "SPIWP",
|
||||
20: "SPIHD",
|
||||
21: "VDD_SPI",
|
||||
}
|
||||
|
||||
_ESP32C61_STRAPPING_PINS = {8, 9}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_c61_validate_gpio_pin(value):
|
||||
if value < 0 or value > 29:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)")
|
||||
if value in _ESP32C61_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c61_validate_supports(value):
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 29:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-29)")
|
||||
if is_input:
|
||||
# All ESP32-C61 pins support input mode
|
||||
pass
|
||||
|
||||
check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
@@ -1,12 +1,9 @@
|
||||
import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf
|
||||
_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||
|
||||
_ESP32P4_USB_JTAG_PINS = {24, 25}
|
||||
|
||||
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
|
||||
@@ -39,14 +36,3 @@ def esp32_p4_validate_supports(value):
|
||||
pass
|
||||
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_p4_validate_lp_i2c(value):
|
||||
if (
|
||||
int(value[CONF_SDA]) not in _ESP32P4_LP_PINS
|
||||
or int(value[CONF_SCL]) not in _ESP32P4_LP_PINS
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Low power i2c interface for ESP32-P4 is only supported on low power interface GPIO{min(_ESP32P4_LP_PINS)} - GPIO{max(_ESP32P4_LP_PINS)}"
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -524,9 +524,10 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
|
||||
return;
|
||||
char addr_str[MAC_ADDR_STR_LEN];
|
||||
format_mac_addr_upper(param->ble_security.auth_cmpl.bd_addr, addr_str);
|
||||
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, addr_str);
|
||||
esp_bd_addr_t bd_addr;
|
||||
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
|
||||
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_,
|
||||
format_hex(bd_addr, 6).c_str());
|
||||
if (!param->ble_security.auth_cmpl.success) {
|
||||
this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
|
||||
} else {
|
||||
|
||||
@@ -4,17 +4,15 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import canbus
|
||||
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
|
||||
from esphome.components.esp32 import (
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -60,18 +58,14 @@ CAN_SPEEDS_ESP32_S2 = {
|
||||
|
||||
CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2}
|
||||
CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2}
|
||||
CAN_SPEEDS_ESP32_C5 = {**CAN_SPEEDS_ESP32_S2}
|
||||
CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2}
|
||||
CAN_SPEEDS_ESP32_C61 = {**CAN_SPEEDS_ESP32_S2}
|
||||
CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2}
|
||||
CAN_SPEEDS_ESP32_P4 = {**CAN_SPEEDS_ESP32_S2}
|
||||
|
||||
CAN_SPEEDS = {
|
||||
VARIANT_ESP32: CAN_SPEEDS_ESP32,
|
||||
VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3,
|
||||
VARIANT_ESP32C5: CAN_SPEEDS_ESP32_C5,
|
||||
VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6,
|
||||
VARIANT_ESP32C61: CAN_SPEEDS_ESP32_C61,
|
||||
VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2,
|
||||
VARIANT_ESP32P4: CAN_SPEEDS_ESP32_P4,
|
||||
VARIANT_ESP32S2: CAN_SPEEDS_ESP32_S2,
|
||||
|
||||
@@ -16,9 +16,8 @@ static const char *const TAG = "esp32_can";
|
||||
|
||||
static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) {
|
||||
switch (bitrate) {
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case canbus::CAN_1KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS();
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_ESP32S2
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
esp32.VARIANT_ESP32H2,
|
||||
esp32.VARIANT_ESP32P4,
|
||||
esp32.const.VARIANT_ESP32H2,
|
||||
esp32.const.VARIANT_ESP32P4,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ def validate_clock_resolution():
|
||||
cv.only_on_esp32(value)
|
||||
value = cv.int_(value)
|
||||
variant = esp32.get_esp32_variant()
|
||||
if variant == esp32.VARIANT_ESP32H2 and value > 32000000:
|
||||
if variant == esp32.const.VARIANT_ESP32H2 and value > 32000000:
|
||||
raise cv.Invalid(
|
||||
f"ESP32 variant {variant} has a max clock_resolution of 32000000."
|
||||
)
|
||||
|
||||
@@ -91,7 +91,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
|
||||
cv.Optional(CONF_USE_DMA): cv.All(
|
||||
esp32.only_on_variant(
|
||||
supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
|
||||
supported=[esp32.const.VARIANT_ESP32P4, esp32.const.VARIANT_ESP32S3]
|
||||
),
|
||||
cv.boolean,
|
||||
),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
from esphome.components.esp32 import get_esp32_variant, gpio
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
gpio,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -256,9 +255,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.has_none_or_all_keys(CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER),
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
esp32.VARIANT_ESP32,
|
||||
esp32.VARIANT_ESP32S2,
|
||||
esp32.VARIANT_ESP32S3,
|
||||
esp32.const.VARIANT_ESP32,
|
||||
esp32.const.VARIANT_ESP32S2,
|
||||
esp32.const.VARIANT_ESP32S3,
|
||||
]
|
||||
),
|
||||
validate_variant_vars,
|
||||
|
||||
@@ -13,7 +13,7 @@ static const char *const TAG = "espnow.transport";
|
||||
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
|
||||
|
||||
void ESPNowTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
packet_transport::PacketTransport::setup();
|
||||
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "ESPNow component not set");
|
||||
@@ -26,10 +26,15 @@ void ESPNowTransport::setup() {
|
||||
this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
|
||||
|
||||
// Register received handler
|
||||
this->parent_->register_received_handler(this);
|
||||
this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
|
||||
|
||||
// Register broadcasted handler
|
||||
this->parent_->register_broadcasted_handler(this);
|
||||
this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
|
||||
}
|
||||
|
||||
void ESPNowTransport::update() {
|
||||
packet_transport::PacketTransport::update();
|
||||
this->updated_ = true;
|
||||
}
|
||||
|
||||
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
|
||||
@@ -18,6 +18,7 @@ class ESPNowTransport : public packet_transport::PacketTransport,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void set_peer_address(peer_address_t address) {
|
||||
|
||||
@@ -3,18 +3,17 @@ import logging
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
import esphome.config_validation as cv
|
||||
@@ -304,14 +303,7 @@ def _final_validate_spi(config):
|
||||
return
|
||||
if spi_configs := fv.full_config.get().get(CONF_SPI):
|
||||
variant = get_esp32_variant()
|
||||
if variant in (
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
):
|
||||
if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3):
|
||||
spi_host = "SPI2_HOST"
|
||||
else:
|
||||
spi_host = "SPI3_HOST"
|
||||
|
||||
@@ -87,8 +87,8 @@ void EthernetComponent::setup() {
|
||||
.intr_flags = 0,
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
auto host = SPI2_HOST;
|
||||
#else
|
||||
auto host = SPI3_HOST;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@rici4kubicek"]
|
||||
@@ -1,194 +0,0 @@
|
||||
#include "hlw8032.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::hlw8032 {
|
||||
|
||||
static const char *const TAG = "hlw8032";
|
||||
|
||||
static constexpr uint8_t STATE_REG_OFFSET = 0;
|
||||
static constexpr uint8_t VOLTAGE_PARAM_OFFSET = 2;
|
||||
static constexpr uint8_t VOLTAGE_REG_OFFSET = 5;
|
||||
static constexpr uint8_t CURRENT_PARAM_OFFSET = 8;
|
||||
static constexpr uint8_t CURRENT_REG_OFFSET = 11;
|
||||
static constexpr uint8_t POWER_PARAM_OFFSET = 14;
|
||||
static constexpr uint8_t POWER_REG_OFFSET = 17;
|
||||
static constexpr uint8_t DATA_UPDATE_REG_OFFSET = 20;
|
||||
static constexpr uint8_t CHECKSUM_REG_OFFSET = 23;
|
||||
static constexpr uint8_t PARAM_REG_USABLE_BIT = (1 << 0);
|
||||
static constexpr uint8_t POWER_OVERFLOW_BIT = (1 << 1);
|
||||
static constexpr uint8_t CURRENT_OVERFLOW_BIT = (1 << 2);
|
||||
static constexpr uint8_t VOLTAGE_OVERFLOW_BIT = (1 << 3);
|
||||
static constexpr uint8_t HAVE_POWER_BIT = (1 << 4);
|
||||
static constexpr uint8_t HAVE_CURRENT_BIT = (1 << 5);
|
||||
static constexpr uint8_t HAVE_VOLTAGE_BIT = (1 << 6);
|
||||
static constexpr uint8_t CHECK_REG = 0x5A;
|
||||
static constexpr uint8_t STATE_REG_CORRECTION_FUNC_NORMAL = 0x55;
|
||||
static constexpr uint8_t STATE_REG_CORRECTION_FUNC_FAIL = 0xAA;
|
||||
static constexpr uint8_t STATE_REG_CORRECTION_MASK = 0xF0;
|
||||
static constexpr uint8_t STATE_REG_OVERFLOW_MASK = 0xF;
|
||||
static constexpr uint8_t PACKET_LENGTH = 24;
|
||||
|
||||
void HLW8032Component::loop() {
|
||||
while (this->available()) {
|
||||
uint8_t data = this->read();
|
||||
if (!this->header_found_) {
|
||||
if ((data == STATE_REG_CORRECTION_FUNC_NORMAL) || (data == STATE_REG_CORRECTION_FUNC_FAIL) ||
|
||||
(data & STATE_REG_CORRECTION_MASK) == STATE_REG_CORRECTION_MASK) {
|
||||
this->header_found_ = true;
|
||||
this->raw_data_[0] = data;
|
||||
}
|
||||
} else if (data == CHECK_REG) {
|
||||
this->raw_data_[1] = data;
|
||||
this->raw_data_index_ = 2;
|
||||
this->check_ = 0;
|
||||
} else if (this->raw_data_index_ >= 2 && this->raw_data_index_ < PACKET_LENGTH) {
|
||||
this->raw_data_[this->raw_data_index_++] = data;
|
||||
if (this->raw_data_index_ < PACKET_LENGTH) {
|
||||
this->check_ += data;
|
||||
} else if (this->raw_data_index_ == PACKET_LENGTH) {
|
||||
if (this->check_ == this->raw_data_[CHECKSUM_REG_OFFSET]) {
|
||||
this->parse_data_();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid checksum: 0x%02X != 0x%02X", this->check_, this->raw_data_[CHECKSUM_REG_OFFSET]);
|
||||
}
|
||||
this->raw_data_index_ = 0;
|
||||
this->header_found_ = false;
|
||||
memset(this->raw_data_, 0, PACKET_LENGTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t HLW8032Component::read_uint24_(uint8_t offset) {
|
||||
return encode_uint24(this->raw_data_[offset], this->raw_data_[offset + 1], this->raw_data_[offset + 2]);
|
||||
}
|
||||
|
||||
void HLW8032Component::parse_data_() {
|
||||
// Parse header
|
||||
uint8_t state_reg = this->raw_data_[STATE_REG_OFFSET];
|
||||
|
||||
if (state_reg == STATE_REG_CORRECTION_FUNC_FAIL) {
|
||||
ESP_LOGE(TAG, "The chip's function of error correction fails.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse data frame
|
||||
uint32_t voltage_parameter = this->read_uint24_(VOLTAGE_PARAM_OFFSET);
|
||||
uint32_t voltage_reg = this->read_uint24_(VOLTAGE_REG_OFFSET);
|
||||
uint32_t current_parameter = this->read_uint24_(CURRENT_PARAM_OFFSET);
|
||||
uint32_t current_reg = this->read_uint24_(CURRENT_REG_OFFSET);
|
||||
uint32_t power_parameter = this->read_uint24_(POWER_PARAM_OFFSET);
|
||||
uint32_t power_reg = this->read_uint24_(POWER_REG_OFFSET);
|
||||
uint8_t data_update_register = this->raw_data_[DATA_UPDATE_REG_OFFSET];
|
||||
|
||||
bool have_power = data_update_register & HAVE_POWER_BIT;
|
||||
bool have_current = data_update_register & HAVE_CURRENT_BIT;
|
||||
bool have_voltage = data_update_register & HAVE_VOLTAGE_BIT;
|
||||
|
||||
bool power_cycle_exceeds_range = false;
|
||||
bool parameter_regs_usable = true;
|
||||
|
||||
if ((state_reg & STATE_REG_CORRECTION_MASK) == STATE_REG_CORRECTION_MASK) {
|
||||
if (state_reg & STATE_REG_OVERFLOW_MASK) {
|
||||
if (state_reg & VOLTAGE_OVERFLOW_BIT) {
|
||||
have_voltage = false;
|
||||
}
|
||||
if (state_reg & CURRENT_OVERFLOW_BIT) {
|
||||
have_current = false;
|
||||
}
|
||||
if (state_reg & POWER_OVERFLOW_BIT) {
|
||||
have_power = false;
|
||||
}
|
||||
if (state_reg & PARAM_REG_USABLE_BIT) {
|
||||
parameter_regs_usable = false;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG,
|
||||
"Reports: (0x%02X)\n"
|
||||
" Voltage REG overflows: %s\n"
|
||||
" Current REG overflows: %s\n"
|
||||
" Power REG overflows: %s\n"
|
||||
" Voltage/Current/Power Parameter REGs not usable: %s\n",
|
||||
state_reg, YESNO(!have_voltage), YESNO(!have_current), YESNO(!have_power),
|
||||
YESNO(!parameter_regs_usable));
|
||||
|
||||
if (!parameter_regs_usable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
power_cycle_exceeds_range = have_power;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG,
|
||||
"Parsed data:\n"
|
||||
" Voltage: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n"
|
||||
" Current: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n"
|
||||
" Power: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n"
|
||||
" Data Update: REG 0x%02" PRIX8 "\n",
|
||||
voltage_parameter, voltage_reg, current_parameter, current_reg, power_parameter, power_reg,
|
||||
data_update_register);
|
||||
|
||||
const float current_multiplier = 1 / (this->current_resistor_ * 1000);
|
||||
|
||||
float voltage = 0.0f;
|
||||
if (have_voltage && voltage_reg) {
|
||||
voltage = float(voltage_parameter) * this->voltage_divider_ / float(voltage_reg);
|
||||
}
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
}
|
||||
|
||||
float power = 0.0f;
|
||||
if (have_power && power_reg && !power_cycle_exceeds_range) {
|
||||
power = (float(power_parameter) / float(power_reg)) * this->voltage_divider_ * current_multiplier;
|
||||
}
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
|
||||
float current = 0.0f;
|
||||
if (have_current && current_reg) {
|
||||
current = float(current_parameter) * current_multiplier / float(current_reg);
|
||||
}
|
||||
if (this->current_sensor_ != nullptr) {
|
||||
this->current_sensor_->publish_state(current);
|
||||
}
|
||||
|
||||
float pf = NAN;
|
||||
const float apparent_power = voltage * current;
|
||||
if (have_voltage && have_current) {
|
||||
if (have_power || power_cycle_exceeds_range) {
|
||||
if (apparent_power > 0) {
|
||||
pf = power / apparent_power;
|
||||
if (pf < 0 || pf > 1) {
|
||||
ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf);
|
||||
pf = NAN;
|
||||
}
|
||||
} else if (apparent_power == 0 && power == 0) {
|
||||
// No load, report ideal power factor
|
||||
pf = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->apparent_power_sensor_ != nullptr) {
|
||||
this->apparent_power_sensor_->publish_state(apparent_power);
|
||||
}
|
||||
if (this->power_factor_sensor_ != nullptr) {
|
||||
this->power_factor_sensor_->publish_state(pf);
|
||||
}
|
||||
}
|
||||
|
||||
void HLW8032Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Configuration:\n"
|
||||
" Current resistor: %.1f mΩ\n"
|
||||
" Voltage Divider: %.3f",
|
||||
this->current_resistor_ * 1000.0f, this->voltage_divider_);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Power", this->power_sensor_);
|
||||
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
|
||||
}
|
||||
} // namespace esphome::hlw8032
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::hlw8032 {
|
||||
|
||||
class HLW8032Component : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_current_resistor(float current_resistor) { this->current_resistor_ = current_resistor; }
|
||||
void set_voltage_divider(float voltage_divider) { this->voltage_divider_ = voltage_divider; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||
void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) {
|
||||
this->apparent_power_sensor_ = apparent_power_sensor;
|
||||
}
|
||||
void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) {
|
||||
this->power_factor_sensor_ = power_factor_sensor;
|
||||
}
|
||||
|
||||
protected:
|
||||
void parse_data_();
|
||||
uint32_t read_uint24_(uint8_t offset);
|
||||
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *apparent_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
|
||||
float current_resistor_{0.001f};
|
||||
float voltage_divider_{1.720f};
|
||||
uint8_t raw_data_[24]{};
|
||||
uint8_t check_{0};
|
||||
uint8_t raw_data_index_{0};
|
||||
bool header_found_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::hlw8032
|
||||
@@ -1,93 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_APPARENT_POWER,
|
||||
CONF_CURRENT,
|
||||
CONF_CURRENT_RESISTOR,
|
||||
CONF_ID,
|
||||
CONF_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_VOLTAGE,
|
||||
CONF_VOLTAGE_DIVIDER,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_WATT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
hlw8032_ns = cg.esphome_ns.namespace("hlw8032")
|
||||
HLW8032Component = hlw8032_ns.class_("HLW8032Component", cg.Component, uart.UARTDevice)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HLW8032Component),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT_AMPS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_APPARENT_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
|
||||
cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float,
|
||||
}
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"hlw8032", baud_rate=4800, require_rx=True, data_bits=8, parity="EVEN"
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if current_config := config.get(CONF_CURRENT):
|
||||
sens = await sensor.new_sensor(current_config)
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
if power_config := config.get(CONF_POWER):
|
||||
sens = await sensor.new_sensor(power_config)
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if apparent_power_config := config.get(CONF_APPARENT_POWER):
|
||||
sens = await sensor.new_sensor(apparent_power_config)
|
||||
cg.add(var.set_apparent_power_sensor(sens))
|
||||
if power_factor_config := config.get(CONF_POWER_FACTOR):
|
||||
sens = await sensor.new_sensor(power_factor_config)
|
||||
cg.add(var.set_power_factor_sensor(sens))
|
||||
cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
|
||||
cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
|
||||
@@ -1,6 +1,2 @@
|
||||
import esphome.config_validation as cv
|
||||
|
||||
AUTO_LOAD = ["md5"]
|
||||
CODEOWNERS = ["@dwmw2"]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
@@ -19,10 +19,11 @@ void HomeassistantBinarySensor::setup() {
|
||||
case PARSE_ON:
|
||||
case PARSE_OFF:
|
||||
bool new_state = val == PARSE_ON;
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_, this->attribute_, ONOFF(new_state));
|
||||
if (this->attribute_.has_value()) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_.c_str(),
|
||||
this->attribute_.value().c_str(), ONOFF(new_state));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
|
||||
}
|
||||
if (this->initial_) {
|
||||
this->publish_initial_state(new_state);
|
||||
@@ -36,9 +37,9 @@ void HomeassistantBinarySensor::setup() {
|
||||
}
|
||||
void HomeassistantBinarySensor::dump_config() {
|
||||
LOG_BINARY_SENSOR("", "Homeassistant Binary Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
|
||||
if (this->attribute_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str());
|
||||
}
|
||||
}
|
||||
float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
@@ -8,15 +8,15 @@ namespace homeassistant {
|
||||
|
||||
class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component {
|
||||
public:
|
||||
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
|
||||
void set_attribute(const char *attribute) { this->attribute_ = attribute; }
|
||||
void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
|
||||
void set_attribute(const std::string &attribute) { attribute_ = attribute; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
const char *entity_id_{nullptr};
|
||||
const char *attribute_{nullptr};
|
||||
std::string entity_id_;
|
||||
optional<std::string> attribute_;
|
||||
bool initial_{true};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,21 +12,21 @@ static const char *const TAG = "homeassistant.number";
|
||||
void HomeassistantNumber::state_changed_(const std::string &state) {
|
||||
auto number_value = parse_number<float>(state);
|
||||
if (!number_value.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str());
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
if (this->state == number_value.value()) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, state.c_str());
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str());
|
||||
this->publish_state(number_value.value());
|
||||
}
|
||||
|
||||
void HomeassistantNumber::min_retrieved_(const std::string &min) {
|
||||
auto min_value = parse_number<float>(min);
|
||||
if (!min_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_, min.c_str());
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str());
|
||||
@@ -36,7 +36,7 @@ void HomeassistantNumber::min_retrieved_(const std::string &min) {
|
||||
void HomeassistantNumber::max_retrieved_(const std::string &max) {
|
||||
auto max_value = parse_number<float>(max);
|
||||
if (!max_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_, max.c_str());
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str());
|
||||
@@ -46,7 +46,7 @@ void HomeassistantNumber::max_retrieved_(const std::string &max) {
|
||||
void HomeassistantNumber::step_retrieved_(const std::string &step) {
|
||||
auto step_value = parse_number<float>(step);
|
||||
if (!step_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_, step.c_str());
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str());
|
||||
@@ -55,19 +55,22 @@ void HomeassistantNumber::step_retrieved_(const std::string &step) {
|
||||
|
||||
void HomeassistantNumber::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(
|
||||
this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
|
||||
this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
|
||||
|
||||
api::global_api_server->get_home_assistant_state(
|
||||
this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
|
||||
this->entity_id_, optional<std::string>("min"),
|
||||
std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
|
||||
api::global_api_server->get_home_assistant_state(
|
||||
this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
|
||||
this->entity_id_, optional<std::string>("max"),
|
||||
std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
|
||||
api::global_api_server->get_home_assistant_state(
|
||||
this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
|
||||
this->entity_id_, optional<std::string>("step"),
|
||||
std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
|
||||
}
|
||||
|
||||
void HomeassistantNumber::dump_config() {
|
||||
LOG_NUMBER("", "Homeassistant Number", this);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
|
||||
}
|
||||
|
||||
float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace homeassistant {
|
||||
|
||||
class HomeassistantNumber : public number::Number, public Component {
|
||||
public:
|
||||
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
|
||||
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -25,7 +25,7 @@ class HomeassistantNumber : public number::Number, public Component {
|
||||
|
||||
void control(float value) override;
|
||||
|
||||
const char *entity_id_{nullptr};
|
||||
std::string entity_id_;
|
||||
};
|
||||
} // namespace homeassistant
|
||||
} // namespace esphome
|
||||
|
||||
@@ -12,24 +12,25 @@ void HomeassistantSensor::setup() {
|
||||
this->entity_id_, this->attribute_, [this](const std::string &state) {
|
||||
auto val = parse_number<float>(state);
|
||||
if (!val.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str());
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_, this->attribute_, *val);
|
||||
if (this->attribute_.has_value()) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_.c_str(),
|
||||
this->attribute_.value().c_str(), *val);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_, *val);
|
||||
ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val);
|
||||
}
|
||||
this->publish_state(*val);
|
||||
});
|
||||
}
|
||||
void HomeassistantSensor::dump_config() {
|
||||
LOG_SENSOR("", "Homeassistant Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
|
||||
if (this->attribute_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str());
|
||||
}
|
||||
}
|
||||
float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
@@ -8,15 +8,15 @@ namespace homeassistant {
|
||||
|
||||
class HomeassistantSensor : public sensor::Sensor, public Component {
|
||||
public:
|
||||
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
|
||||
void set_attribute(const char *attribute) { this->attribute_ = attribute; }
|
||||
void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
|
||||
void set_attribute(const std::string &attribute) { attribute_ = attribute; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
const char *entity_id_{nullptr};
|
||||
const char *attribute_{nullptr};
|
||||
std::string entity_id_;
|
||||
optional<std::string> attribute_;
|
||||
};
|
||||
|
||||
} // namespace homeassistant
|
||||
|
||||
@@ -10,7 +10,7 @@ static const char *const TAG = "homeassistant.switch";
|
||||
using namespace esphome::switch_;
|
||||
|
||||
void HomeassistantSwitch::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, [this](const std::string &state) {
|
||||
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) {
|
||||
auto val = parse_on_off(state.c_str());
|
||||
switch (val) {
|
||||
case PARSE_NONE:
|
||||
@@ -20,7 +20,7 @@ void HomeassistantSwitch::setup() {
|
||||
case PARSE_ON:
|
||||
case PARSE_OFF:
|
||||
bool new_state = val == PARSE_ON;
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
|
||||
this->publish_state(new_state);
|
||||
break;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ void HomeassistantSwitch::setup() {
|
||||
|
||||
void HomeassistantSwitch::dump_config() {
|
||||
LOG_SWITCH("", "Homeassistant Switch", this);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
|
||||
}
|
||||
|
||||
float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
@@ -8,14 +8,14 @@ namespace homeassistant {
|
||||
|
||||
class HomeassistantSwitch : public switch_::Switch, public Component {
|
||||
public:
|
||||
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
|
||||
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
const char *entity_id_{nullptr};
|
||||
std::string entity_id_;
|
||||
};
|
||||
|
||||
} // namespace homeassistant
|
||||
|
||||
@@ -10,19 +10,20 @@ static const char *const TAG = "homeassistant.text_sensor";
|
||||
void HomeassistantTextSensor::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(
|
||||
this->entity_id_, this->attribute_, [this](const std::string &state) {
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_, this->attribute_, state.c_str());
|
||||
if (this->attribute_.has_value()) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_.c_str(),
|
||||
this->attribute_.value().c_str(), state.c_str());
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str());
|
||||
ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str());
|
||||
}
|
||||
this->publish_state(state);
|
||||
});
|
||||
}
|
||||
void HomeassistantTextSensor::dump_config() {
|
||||
LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_);
|
||||
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
|
||||
if (this->attribute_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str());
|
||||
}
|
||||
}
|
||||
float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
@@ -8,15 +8,15 @@ namespace homeassistant {
|
||||
|
||||
class HomeassistantTextSensor : public text_sensor::TextSensor, public Component {
|
||||
public:
|
||||
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
|
||||
void set_attribute(const char *attribute) { this->attribute_ = attribute; }
|
||||
void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
|
||||
void set_attribute(const std::string &attribute) { attribute_ = attribute; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
const char *entity_id_{nullptr};
|
||||
const char *attribute_{nullptr};
|
||||
std::string entity_id_;
|
||||
optional<std::string> attribute_;
|
||||
};
|
||||
|
||||
} // namespace homeassistant
|
||||
|
||||
@@ -36,10 +36,6 @@ void HttpRequestUpdate::setup() {
|
||||
}
|
||||
|
||||
void HttpRequestUpdate::update() {
|
||||
if (!network::is_connected()) {
|
||||
ESP_LOGD(TAG, "Network not connected, skipping update check");
|
||||
return;
|
||||
}
|
||||
#ifdef USE_ESP32
|
||||
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
|
||||
#else
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
CODEOWNERS = ["@stuartparmenter"]
|
||||
|
||||
# Use fully-qualified namespace to avoid collision with external hub75 library's global ::hub75 namespace
|
||||
hub75_ns = MockObj("::esphome::hub75", "::")
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Board presets for HUB75 displays.
|
||||
|
||||
Each board preset defines standard pin mappings for HUB75 controller boards.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import importlib
|
||||
import pkgutil
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
class BoardRegistry:
|
||||
"""Global registry for board configurations."""
|
||||
|
||||
_boards: ClassVar[dict[str, "BoardConfig"]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, board: "BoardConfig") -> None:
|
||||
"""Register a board configuration."""
|
||||
cls._boards[board.name] = board
|
||||
|
||||
@classmethod
|
||||
def get_boards(cls) -> dict[str, "BoardConfig"]:
|
||||
"""Return all registered boards."""
|
||||
return cls._boards
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoardConfig:
|
||||
"""Board configuration storing HUB75 pin mappings."""
|
||||
|
||||
name: str
|
||||
r1_pin: int
|
||||
g1_pin: int
|
||||
b1_pin: int
|
||||
r2_pin: int
|
||||
g2_pin: int
|
||||
b2_pin: int
|
||||
a_pin: int
|
||||
b_pin: int
|
||||
c_pin: int
|
||||
d_pin: int
|
||||
e_pin: int | None
|
||||
lat_pin: int
|
||||
oe_pin: int
|
||||
clk_pin: int
|
||||
ignore_strapping_pins: tuple[str, ...] = () # e.g., ("a_pin", "clk_pin")
|
||||
|
||||
# Derived field for pin lookup
|
||||
pins: dict[str, int | None] = field(default_factory=dict, init=False, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize derived fields and register board."""
|
||||
self.name = self.name.lower()
|
||||
self.pins = {
|
||||
"r1": self.r1_pin,
|
||||
"g1": self.g1_pin,
|
||||
"b1": self.b1_pin,
|
||||
"r2": self.r2_pin,
|
||||
"g2": self.g2_pin,
|
||||
"b2": self.b2_pin,
|
||||
"a": self.a_pin,
|
||||
"b": self.b_pin,
|
||||
"c": self.c_pin,
|
||||
"d": self.d_pin,
|
||||
"e": self.e_pin,
|
||||
"lat": self.lat_pin,
|
||||
"oe": self.oe_pin,
|
||||
"clk": self.clk_pin,
|
||||
}
|
||||
BoardRegistry.register(self)
|
||||
|
||||
def get_pin(self, pin_name: str) -> int | None:
|
||||
"""Get pin number for a given pin name."""
|
||||
return self.pins.get(pin_name)
|
||||
|
||||
|
||||
# Dynamically import all board definition modules
|
||||
for module_info in pkgutil.iter_modules(__path__):
|
||||
importlib.import_module(f".{module_info.name}", package=__package__)
|
||||
@@ -1,23 +0,0 @@
|
||||
"""Adafruit Matrix Portal board definitions."""
|
||||
|
||||
from . import BoardConfig
|
||||
|
||||
# Adafruit Matrix Portal S3
|
||||
BoardConfig(
|
||||
"adafruit-matrix-portal-s3",
|
||||
r1_pin=42,
|
||||
g1_pin=41,
|
||||
b1_pin=40,
|
||||
r2_pin=38,
|
||||
g2_pin=39,
|
||||
b2_pin=37,
|
||||
a_pin=45,
|
||||
b_pin=36,
|
||||
c_pin=48,
|
||||
d_pin=35,
|
||||
e_pin=21,
|
||||
lat_pin=47,
|
||||
oe_pin=14,
|
||||
clk_pin=2,
|
||||
ignore_strapping_pins=("a_pin",), # GPIO45 is a strapping pin
|
||||
)
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Apollo Automation M1 board definitions."""
|
||||
|
||||
from . import BoardConfig
|
||||
|
||||
# Apollo Automation M1 Rev4
|
||||
BoardConfig(
|
||||
"apollo-automation-m1-rev4",
|
||||
r1_pin=42,
|
||||
g1_pin=41,
|
||||
b1_pin=40,
|
||||
r2_pin=38,
|
||||
g2_pin=39,
|
||||
b2_pin=37,
|
||||
a_pin=45,
|
||||
b_pin=36,
|
||||
c_pin=48,
|
||||
d_pin=35,
|
||||
e_pin=21,
|
||||
lat_pin=47,
|
||||
oe_pin=14,
|
||||
clk_pin=2,
|
||||
)
|
||||
|
||||
# Apollo Automation M1 Rev6
|
||||
BoardConfig(
|
||||
"apollo-automation-m1-rev6",
|
||||
r1_pin=1,
|
||||
g1_pin=5,
|
||||
b1_pin=6,
|
||||
r2_pin=7,
|
||||
g2_pin=13,
|
||||
b2_pin=9,
|
||||
a_pin=16,
|
||||
b_pin=48,
|
||||
c_pin=47,
|
||||
d_pin=21,
|
||||
e_pin=38,
|
||||
lat_pin=8,
|
||||
oe_pin=4,
|
||||
clk_pin=18,
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
"""Huidu board definitions."""
|
||||
|
||||
from . import BoardConfig
|
||||
|
||||
# Huidu HD-WF2
|
||||
BoardConfig(
|
||||
"huidu-hd-wf2",
|
||||
r1_pin=2,
|
||||
g1_pin=6,
|
||||
b1_pin=10,
|
||||
r2_pin=3,
|
||||
g2_pin=7,
|
||||
b2_pin=11,
|
||||
a_pin=39,
|
||||
b_pin=38,
|
||||
c_pin=37,
|
||||
d_pin=36,
|
||||
e_pin=21,
|
||||
lat_pin=33,
|
||||
oe_pin=35,
|
||||
clk_pin=34,
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
"""ESP32 Trinity board definitions."""
|
||||
|
||||
from . import BoardConfig
|
||||
|
||||
# ESP32 Trinity
|
||||
# https://esp32trinity.com/
|
||||
# Pin assignments from: https://github.com/witnessmenow/ESP32-Trinity/blob/master/FAQ.md
|
||||
BoardConfig(
|
||||
"esp32-trinity",
|
||||
r1_pin=25,
|
||||
g1_pin=26,
|
||||
b1_pin=27,
|
||||
r2_pin=14,
|
||||
g2_pin=12,
|
||||
b2_pin=13,
|
||||
a_pin=23,
|
||||
b_pin=19,
|
||||
c_pin=5,
|
||||
d_pin=17,
|
||||
e_pin=18,
|
||||
lat_pin=4,
|
||||
oe_pin=15,
|
||||
clk_pin=16,
|
||||
)
|
||||
@@ -1,578 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BIT_DEPTH,
|
||||
CONF_BOARD,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_CLK_PIN,
|
||||
CONF_GAMMA_CORRECT,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_OE_PIN,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import boards, hub75_ns
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@stuartparmenter"]
|
||||
|
||||
# Load all board presets
|
||||
BOARDS = boards.BoardRegistry.get_boards()
|
||||
|
||||
# Constants
|
||||
CONF_HUB75_ID = "hub75_id"
|
||||
|
||||
# Panel dimensions
|
||||
CONF_PANEL_WIDTH = "panel_width"
|
||||
CONF_PANEL_HEIGHT = "panel_height"
|
||||
|
||||
# Multi-panel layout
|
||||
CONF_LAYOUT_ROWS = "layout_rows"
|
||||
CONF_LAYOUT_COLS = "layout_cols"
|
||||
CONF_LAYOUT = "layout"
|
||||
|
||||
# Panel hardware
|
||||
CONF_SCAN_WIRING = "scan_wiring"
|
||||
CONF_SHIFT_DRIVER = "shift_driver"
|
||||
|
||||
# RGB pins
|
||||
CONF_R1_PIN = "r1_pin"
|
||||
CONF_G1_PIN = "g1_pin"
|
||||
CONF_B1_PIN = "b1_pin"
|
||||
CONF_R2_PIN = "r2_pin"
|
||||
CONF_G2_PIN = "g2_pin"
|
||||
CONF_B2_PIN = "b2_pin"
|
||||
|
||||
# Address pins
|
||||
CONF_A_PIN = "a_pin"
|
||||
CONF_B_PIN = "b_pin"
|
||||
CONF_C_PIN = "c_pin"
|
||||
CONF_D_PIN = "d_pin"
|
||||
CONF_E_PIN = "e_pin"
|
||||
|
||||
# Control pins
|
||||
CONF_LAT_PIN = "lat_pin"
|
||||
|
||||
NEVER = 4294967295 # uint32_t max - value used when update_interval is "never"
|
||||
|
||||
# Pin mapping from config keys to board keys
|
||||
PIN_MAPPING = {
|
||||
CONF_R1_PIN: "r1",
|
||||
CONF_G1_PIN: "g1",
|
||||
CONF_B1_PIN: "b1",
|
||||
CONF_R2_PIN: "r2",
|
||||
CONF_G2_PIN: "g2",
|
||||
CONF_B2_PIN: "b2",
|
||||
CONF_A_PIN: "a",
|
||||
CONF_B_PIN: "b",
|
||||
CONF_C_PIN: "c",
|
||||
CONF_D_PIN: "d",
|
||||
CONF_E_PIN: "e",
|
||||
CONF_LAT_PIN: "lat",
|
||||
CONF_OE_PIN: "oe",
|
||||
CONF_CLK_PIN: "clk",
|
||||
}
|
||||
|
||||
# Required pins (E pin is optional)
|
||||
REQUIRED_PINS = [key for key in PIN_MAPPING if key != CONF_E_PIN]
|
||||
|
||||
# Configuration
|
||||
CONF_CLOCK_SPEED = "clock_speed"
|
||||
CONF_LATCH_BLANKING = "latch_blanking"
|
||||
CONF_CLOCK_PHASE = "clock_phase"
|
||||
CONF_DOUBLE_BUFFER = "double_buffer"
|
||||
CONF_MIN_REFRESH_RATE = "min_refresh_rate"
|
||||
|
||||
# Map to hub75 library enums (in global namespace)
|
||||
ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True)
|
||||
SHIFT_DRIVERS = {
|
||||
"GENERIC": ShiftDriver.GENERIC,
|
||||
"FM6126A": ShiftDriver.FM6126A,
|
||||
"ICN2038S": ShiftDriver.ICN2038S,
|
||||
"FM6124": ShiftDriver.FM6124,
|
||||
"MBI5124": ShiftDriver.MBI5124,
|
||||
"DP3246": ShiftDriver.DP3246,
|
||||
}
|
||||
|
||||
PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True)
|
||||
PANEL_LAYOUTS = {
|
||||
"HORIZONTAL": PanelLayout.HORIZONTAL,
|
||||
"TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN,
|
||||
"TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN,
|
||||
"BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP,
|
||||
"BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP,
|
||||
"TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
|
||||
"TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
|
||||
"BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
|
||||
"BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
|
||||
}
|
||||
|
||||
ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True)
|
||||
SCAN_PATTERNS = {
|
||||
"STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN,
|
||||
"FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH,
|
||||
"FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH,
|
||||
"FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH,
|
||||
}
|
||||
|
||||
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
|
||||
CLOCK_SPEEDS = {
|
||||
"8MHZ": Hub75ClockSpeed.HZ_8M,
|
||||
"10MHZ": Hub75ClockSpeed.HZ_10M,
|
||||
"16MHZ": Hub75ClockSpeed.HZ_16M,
|
||||
"20MHZ": Hub75ClockSpeed.HZ_20M,
|
||||
}
|
||||
|
||||
HUB75Display = hub75_ns.class_("HUB75Display", cg.PollingComponent, display.Display)
|
||||
Hub75Config = cg.global_ns.struct("Hub75Config")
|
||||
Hub75Pins = cg.global_ns.struct("Hub75Pins")
|
||||
|
||||
|
||||
def _merge_board_pins(config: ConfigType) -> ConfigType:
|
||||
"""Merge board preset pins with explicit pin overrides."""
|
||||
board_name = config.get(CONF_BOARD)
|
||||
|
||||
if board_name is None:
|
||||
# No board specified - validate that all required pins are present
|
||||
errs = [
|
||||
cv.Invalid(
|
||||
f"Required pin '{pin_name}' is missing. "
|
||||
f"Either specify a board preset or provide all pin mappings manually.",
|
||||
path=[pin_name],
|
||||
)
|
||||
for pin_name in REQUIRED_PINS
|
||||
if pin_name not in config
|
||||
]
|
||||
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
# E_PIN is optional
|
||||
return config
|
||||
|
||||
# Get board configuration
|
||||
if board_name not in BOARDS:
|
||||
raise cv.Invalid(
|
||||
f"Unknown board '{board_name}'. Available boards: {', '.join(sorted(BOARDS.keys()))}"
|
||||
)
|
||||
|
||||
board = BOARDS[board_name]
|
||||
|
||||
# Merge board pins with explicit overrides
|
||||
# Explicit pins in config take precedence over board defaults
|
||||
for conf_key, board_key in PIN_MAPPING.items():
|
||||
if conf_key in config or (board_pin := board.get_pin(board_key)) is None:
|
||||
continue
|
||||
# Create pin config
|
||||
pin_config = {"number": board_pin}
|
||||
if conf_key in board.ignore_strapping_pins:
|
||||
pin_config["ignore_strapping_warning"] = True
|
||||
|
||||
# Validate through pin schema to add required fields (id, etc.)
|
||||
config[conf_key] = pins.gpio_output_pin_schema(pin_config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _validate_config(config: ConfigType) -> ConfigType:
|
||||
"""Validate driver and layout requirements."""
|
||||
errs: list[cv.Invalid] = []
|
||||
|
||||
# MBI5124 requires inverted clock phase
|
||||
driver = config.get(CONF_SHIFT_DRIVER, "GENERIC")
|
||||
if driver == "MBI5124" and not config.get(CONF_CLOCK_PHASE, False):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
"MBI5124 shift driver requires 'clock_phase: true' to be set",
|
||||
path=[CONF_CLOCK_PHASE],
|
||||
)
|
||||
)
|
||||
|
||||
# Prevent conflicting min_refresh_rate + update_interval configuration
|
||||
# min_refresh_rate is auto-calculated from update_interval unless using LVGL mode
|
||||
update_interval = config.get(CONF_UPDATE_INTERVAL)
|
||||
if CONF_MIN_REFRESH_RATE in config and update_interval is not None:
|
||||
# Handle both integer (NEVER) and time object cases
|
||||
interval_ms = (
|
||||
update_interval
|
||||
if isinstance(update_interval, int)
|
||||
else update_interval.total_milliseconds
|
||||
)
|
||||
if interval_ms != NEVER:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
"Cannot set both 'min_refresh_rate' and 'update_interval' (except 'never'). "
|
||||
"Refresh rate is auto-calculated from update_interval. "
|
||||
"Remove 'min_refresh_rate' or use 'update_interval: never' for LVGL mode.",
|
||||
path=[CONF_MIN_REFRESH_RATE],
|
||||
)
|
||||
)
|
||||
|
||||
# Validate layout configuration (validate effective config including C++ defaults)
|
||||
layout = config.get(CONF_LAYOUT, "HORIZONTAL")
|
||||
layout_rows = config.get(CONF_LAYOUT_ROWS, 1)
|
||||
layout_cols = config.get(CONF_LAYOUT_COLS, 1)
|
||||
is_zigzag = "ZIGZAG" in layout
|
||||
|
||||
# Single panel (1x1) should use HORIZONTAL
|
||||
if layout_rows == 1 and layout_cols == 1 and layout != "HORIZONTAL":
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Single panel (layout_rows=1, layout_cols=1) should use 'layout: HORIZONTAL' (got {layout})",
|
||||
path=[CONF_LAYOUT],
|
||||
)
|
||||
)
|
||||
|
||||
# HORIZONTAL layout requires single row
|
||||
if layout == "HORIZONTAL" and layout_rows != 1:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"HORIZONTAL layout requires 'layout_rows: 1' (got {layout_rows}). "
|
||||
"For multi-row grids, use TOP_LEFT_DOWN or other grid layouts.",
|
||||
path=[CONF_LAYOUT_ROWS],
|
||||
)
|
||||
)
|
||||
|
||||
# Grid layouts (non-HORIZONTAL) require more than one panel
|
||||
if layout != "HORIZONTAL" and layout_rows == 1 and layout_cols == 1:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Grid layout '{layout}' requires multiple panels (layout_rows > 1 or layout_cols > 1)",
|
||||
path=[CONF_LAYOUT],
|
||||
)
|
||||
)
|
||||
|
||||
# Serpentine layouts (non-ZIGZAG) require multiple rows
|
||||
# Serpentine physically rotates alternate rows upside down (Y-coordinate inversion)
|
||||
# Single-row chains should use HORIZONTAL or ZIGZAG variants
|
||||
if not is_zigzag and layout != "HORIZONTAL" and layout_rows == 1:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Serpentine layout '{layout}' requires layout_rows > 1 "
|
||||
f"(got layout_rows={layout_rows}). "
|
||||
"Serpentine wiring physically rotates alternate rows upside down. "
|
||||
"For single-row chains, use 'layout: HORIZONTAL' or add '_ZIGZAG' suffix.",
|
||||
path=[CONF_LAYOUT_ROWS],
|
||||
)
|
||||
)
|
||||
|
||||
# ZIGZAG layouts require actual grid (both rows AND cols > 1)
|
||||
if is_zigzag and (layout_rows == 1 or layout_cols == 1):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"ZIGZAG layout '{layout}' requires both layout_rows > 1 AND layout_cols > 1 "
|
||||
f"(got rows={layout_rows}, cols={layout_cols}). "
|
||||
"For single row/column chains, use non-zigzag layouts or HORIZONTAL.",
|
||||
path=[CONF_LAYOUT],
|
||||
)
|
||||
)
|
||||
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
"""Validate requirements when using HUB75 display."""
|
||||
# Local imports to avoid circular dependencies
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import VARIANT_ESP32P4
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
|
||||
full_config = fv.full_config.get()
|
||||
errs: list[cv.Invalid] = []
|
||||
|
||||
# ESP32-P4 requires PSRAM
|
||||
variant = get_esp32_variant()
|
||||
if variant == VARIANT_ESP32P4 and PSRAM_DOMAIN not in full_config:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
"HUB75 display on ESP32-P4 requires PSRAM. Add 'psram:' to your configuration.",
|
||||
path=[CONF_ID],
|
||||
)
|
||||
)
|
||||
|
||||
# LVGL-specific validation
|
||||
if LVGL_DOMAIN in full_config:
|
||||
# Check update_interval (converted from "never" to NEVER constant)
|
||||
update_interval = config.get(CONF_UPDATE_INTERVAL)
|
||||
if update_interval is not None:
|
||||
# Handle both integer (NEVER) and time object cases
|
||||
interval_ms = (
|
||||
update_interval
|
||||
if isinstance(update_interval, int)
|
||||
else update_interval.total_milliseconds
|
||||
)
|
||||
if interval_ms != NEVER:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
"HUB75 display with LVGL must have 'update_interval: never'. "
|
||||
"LVGL manages its own refresh timing.",
|
||||
path=[CONF_UPDATE_INTERVAL],
|
||||
)
|
||||
)
|
||||
|
||||
# Check auto_clear_enabled
|
||||
auto_clear = config[CONF_AUTO_CLEAR_ENABLED]
|
||||
if auto_clear is not False:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"HUB75 display with LVGL must have 'auto_clear_enabled: false' (got '{auto_clear}'). "
|
||||
"LVGL manages screen clearing.",
|
||||
path=[CONF_AUTO_CLEAR_ENABLED],
|
||||
)
|
||||
)
|
||||
|
||||
# Check double_buffer (C++ default: false)
|
||||
double_buffer = config.get(CONF_DOUBLE_BUFFER, False)
|
||||
if double_buffer is not False:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"HUB75 display with LVGL must have 'double_buffer: false' (got '{double_buffer}'). "
|
||||
"LVGL uses its own buffering strategy.",
|
||||
path=[CONF_DOUBLE_BUFFER],
|
||||
)
|
||||
)
|
||||
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.Schema(_final_validate)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HUB75Display),
|
||||
# Board preset (optional - provides default pin mappings)
|
||||
cv.Optional(CONF_BOARD): cv.one_of(*BOARDS.keys(), lower=True),
|
||||
# Panel dimensions
|
||||
cv.Required(CONF_PANEL_WIDTH): cv.positive_int,
|
||||
cv.Required(CONF_PANEL_HEIGHT): cv.positive_int,
|
||||
# Multi-panel layout
|
||||
cv.Optional(CONF_LAYOUT_ROWS): cv.positive_int,
|
||||
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
|
||||
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
|
||||
# Panel hardware configuration
|
||||
cv.Optional(CONF_SCAN_WIRING): cv.enum(
|
||||
SCAN_PATTERNS, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
|
||||
# Display configuration
|
||||
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
|
||||
cv.Optional(CONF_BRIGHTNESS): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=6, max=12),
|
||||
cv.Optional(CONF_GAMMA_CORRECT): cv.enum(
|
||||
{"LINEAR": 0, "CIE1931": 1, "GAMMA_2_2": 2}, upper=True
|
||||
),
|
||||
cv.Optional(CONF_MIN_REFRESH_RATE): cv.int_range(min=40, max=200),
|
||||
# RGB data pins
|
||||
cv.Optional(CONF_R1_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_G1_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_B1_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_R2_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_G2_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_B2_PIN): pins.gpio_output_pin_schema,
|
||||
# Address pins
|
||||
cv.Optional(CONF_A_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_B_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_C_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_D_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_E_PIN): pins.gpio_output_pin_schema,
|
||||
# Control pins
|
||||
cv.Optional(CONF_LAT_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_CLK_PIN): pins.gpio_output_pin_schema,
|
||||
# Timing configuration
|
||||
cv.Optional(CONF_CLOCK_SPEED): cv.enum(CLOCK_SPEEDS, upper=True),
|
||||
cv.Optional(CONF_LATCH_BLANKING): cv.positive_int,
|
||||
cv.Optional(CONF_CLOCK_PHASE): cv.boolean,
|
||||
}
|
||||
),
|
||||
_merge_board_pins,
|
||||
_validate_config,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_REFRESH_RATE = 60 # Hz
|
||||
|
||||
|
||||
def _calculate_min_refresh_rate(config: ConfigType) -> int:
|
||||
"""Calculate minimum refresh rate for the display.
|
||||
|
||||
Priority:
|
||||
1. Explicit min_refresh_rate setting (user override)
|
||||
2. Derived from update_interval (ms to Hz conversion)
|
||||
3. Default 60 Hz (for LVGL or unspecified interval)
|
||||
"""
|
||||
if CONF_MIN_REFRESH_RATE in config:
|
||||
return config[CONF_MIN_REFRESH_RATE]
|
||||
|
||||
update_interval = config.get(CONF_UPDATE_INTERVAL)
|
||||
if update_interval is None:
|
||||
return DEFAULT_REFRESH_RATE
|
||||
|
||||
# update_interval can be TimePeriod object or NEVER constant (int)
|
||||
interval_ms = (
|
||||
update_interval
|
||||
if isinstance(update_interval, int)
|
||||
else update_interval.total_milliseconds
|
||||
)
|
||||
|
||||
# "never" or zero means external refresh (e.g., LVGL)
|
||||
if interval_ms in (NEVER, 0):
|
||||
return DEFAULT_REFRESH_RATE
|
||||
|
||||
# Convert ms interval to Hz, clamped to valid range [40, 200]
|
||||
return max(40, min(200, int(round(1000 / interval_ms))))
|
||||
|
||||
|
||||
def _build_pins_struct(
|
||||
pin_expressions: dict[str, Any], e_pin_num: int | cg.RawExpression
|
||||
) -> cg.StructInitializer:
|
||||
"""Build Hub75Pins struct from pin expressions."""
|
||||
|
||||
def pin_cast(pin):
|
||||
return cg.RawExpression(f"static_cast<int8_t>({pin.get_pin()})")
|
||||
|
||||
return cg.StructInitializer(
|
||||
Hub75Pins,
|
||||
("r1", pin_cast(pin_expressions["r1"])),
|
||||
("g1", pin_cast(pin_expressions["g1"])),
|
||||
("b1", pin_cast(pin_expressions["b1"])),
|
||||
("r2", pin_cast(pin_expressions["r2"])),
|
||||
("g2", pin_cast(pin_expressions["g2"])),
|
||||
("b2", pin_cast(pin_expressions["b2"])),
|
||||
("a", pin_cast(pin_expressions["a"])),
|
||||
("b", pin_cast(pin_expressions["b"])),
|
||||
("c", pin_cast(pin_expressions["c"])),
|
||||
("d", pin_cast(pin_expressions["d"])),
|
||||
("e", e_pin_num),
|
||||
("lat", pin_cast(pin_expressions["lat"])),
|
||||
("oe", pin_cast(pin_expressions["oe"])),
|
||||
("clk", pin_cast(pin_expressions["clk"])),
|
||||
)
|
||||
|
||||
|
||||
def _append_config_fields(
|
||||
config: ConfigType,
|
||||
field_mapping: list[tuple[str, str]],
|
||||
config_fields: list[tuple[str, Any]],
|
||||
) -> None:
|
||||
"""Append config fields from mapping if present in config."""
|
||||
for conf_key, struct_field in field_mapping:
|
||||
if conf_key in config:
|
||||
config_fields.append((struct_field, config[conf_key]))
|
||||
|
||||
|
||||
def _build_config_struct(
|
||||
config: ConfigType, pins_struct: cg.StructInitializer, min_refresh: int
|
||||
) -> cg.StructInitializer:
|
||||
"""Build Hub75Config struct from config.
|
||||
|
||||
Fields must be added in declaration order (see hub75_types.h) to satisfy
|
||||
C++ designated initializer requirements. The order is:
|
||||
1. fields_before_pins (panel_width through layout)
|
||||
2. pins
|
||||
3. output_clock_speed
|
||||
4. min_refresh_rate
|
||||
5. fields_after_min_refresh (latch_blanking through brightness)
|
||||
"""
|
||||
fields_before_pins = [
|
||||
(CONF_PANEL_WIDTH, "panel_width"),
|
||||
(CONF_PANEL_HEIGHT, "panel_height"),
|
||||
# scan_pattern - auto-calculated, not set
|
||||
(CONF_SCAN_WIRING, "scan_wiring"),
|
||||
(CONF_SHIFT_DRIVER, "shift_driver"),
|
||||
(CONF_LAYOUT_ROWS, "layout_rows"),
|
||||
(CONF_LAYOUT_COLS, "layout_cols"),
|
||||
(CONF_LAYOUT, "layout"),
|
||||
]
|
||||
fields_after_min_refresh = [
|
||||
(CONF_LATCH_BLANKING, "latch_blanking"),
|
||||
(CONF_DOUBLE_BUFFER, "double_buffer"),
|
||||
(CONF_CLOCK_PHASE, "clk_phase_inverted"),
|
||||
(CONF_BRIGHTNESS, "brightness"),
|
||||
]
|
||||
|
||||
config_fields: list[tuple[str, Any]] = []
|
||||
|
||||
_append_config_fields(config, fields_before_pins, config_fields)
|
||||
|
||||
config_fields.append(("pins", pins_struct))
|
||||
|
||||
if CONF_CLOCK_SPEED in config:
|
||||
config_fields.append(("output_clock_speed", config[CONF_CLOCK_SPEED]))
|
||||
|
||||
config_fields.append(("min_refresh_rate", min_refresh))
|
||||
|
||||
_append_config_fields(config, fields_after_min_refresh, config_fields)
|
||||
|
||||
return cg.StructInitializer(Hub75Config, *config_fields)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
add_idf_component(
|
||||
name="esphome/esp-hub75",
|
||||
ref="0.1.6",
|
||||
)
|
||||
|
||||
# Set compile-time configuration via defines
|
||||
if CONF_BIT_DEPTH in config:
|
||||
cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH])
|
||||
|
||||
if CONF_GAMMA_CORRECT in config:
|
||||
cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT])
|
||||
|
||||
# Await all pin expressions
|
||||
pin_expressions = {
|
||||
"r1": await cg.gpio_pin_expression(config[CONF_R1_PIN]),
|
||||
"g1": await cg.gpio_pin_expression(config[CONF_G1_PIN]),
|
||||
"b1": await cg.gpio_pin_expression(config[CONF_B1_PIN]),
|
||||
"r2": await cg.gpio_pin_expression(config[CONF_R2_PIN]),
|
||||
"g2": await cg.gpio_pin_expression(config[CONF_G2_PIN]),
|
||||
"b2": await cg.gpio_pin_expression(config[CONF_B2_PIN]),
|
||||
"a": await cg.gpio_pin_expression(config[CONF_A_PIN]),
|
||||
"b": await cg.gpio_pin_expression(config[CONF_B_PIN]),
|
||||
"c": await cg.gpio_pin_expression(config[CONF_C_PIN]),
|
||||
"d": await cg.gpio_pin_expression(config[CONF_D_PIN]),
|
||||
"lat": await cg.gpio_pin_expression(config[CONF_LAT_PIN]),
|
||||
"oe": await cg.gpio_pin_expression(config[CONF_OE_PIN]),
|
||||
"clk": await cg.gpio_pin_expression(config[CONF_CLK_PIN]),
|
||||
}
|
||||
|
||||
# E pin is optional
|
||||
if CONF_E_PIN in config:
|
||||
e_pin = await cg.gpio_pin_expression(config[CONF_E_PIN])
|
||||
e_pin_num = cg.RawExpression(f"static_cast<int8_t>({e_pin.get_pin()})")
|
||||
else:
|
||||
e_pin_num = -1
|
||||
|
||||
# Build structs
|
||||
min_refresh = _calculate_min_refresh_rate(config)
|
||||
pins_struct = _build_pins_struct(pin_expressions, e_pin_num)
|
||||
hub75_config = _build_config_struct(config, pins_struct, min_refresh)
|
||||
|
||||
# Create display and register
|
||||
var = cg.new_Pvariable(config[CONF_ID], hub75_config)
|
||||
await display.register_display(var, config)
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
@@ -1,192 +0,0 @@
|
||||
#include "hub75_component.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome::hub75 {
|
||||
|
||||
static const char *const TAG = "hub75";
|
||||
|
||||
// ========================================
|
||||
// Constructor
|
||||
// ========================================
|
||||
|
||||
HUB75Display::HUB75Display(const Hub75Config &config) : config_(config) {
|
||||
// Initialize runtime state from config
|
||||
this->brightness_ = config.brightness;
|
||||
this->enabled_ = (config.brightness > 0);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Core Component methods
|
||||
// ========================================
|
||||
|
||||
void HUB75Display::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HUB75Display...");
|
||||
|
||||
// Create driver with pre-configured config
|
||||
driver_ = new Hub75Driver(config_);
|
||||
if (!driver_->begin()) {
|
||||
ESP_LOGE(TAG, "Failed to initialize HUB75 driver!");
|
||||
return;
|
||||
}
|
||||
|
||||
this->enabled_ = true;
|
||||
}
|
||||
|
||||
void HUB75Display::dump_config() {
|
||||
LOG_DISPLAY("", "HUB75", this);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Panel: %dx%d pixels\n"
|
||||
" Layout: %dx%d panels\n"
|
||||
" Virtual Display: %dx%d pixels",
|
||||
config_.panel_width, config_.panel_height, config_.layout_cols, config_.layout_rows,
|
||||
config_.panel_width * config_.layout_cols, config_.panel_height * config_.layout_rows);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Scan Wiring: %d\n"
|
||||
" Shift Driver: %d",
|
||||
static_cast<int>(config_.scan_wiring), static_cast<int>(config_.shift_driver));
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Pins: R1:%i, G1:%i, B1:%i, R2:%i, G2:%i, B2:%i\n"
|
||||
" Pins: A:%i, B:%i, C:%i, D:%i, E:%i\n"
|
||||
" Pins: LAT:%i, OE:%i, CLK:%i",
|
||||
config_.pins.r1, config_.pins.g1, config_.pins.b1, config_.pins.r2, config_.pins.g2, config_.pins.b2,
|
||||
config_.pins.a, config_.pins.b, config_.pins.c, config_.pins.d, config_.pins.e, config_.pins.lat,
|
||||
config_.pins.oe, config_.pins.clk);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Clock Speed: %u MHz\n"
|
||||
" Latch Blanking: %i\n"
|
||||
" Clock Phase: %s\n"
|
||||
" Min Refresh Rate: %i Hz\n"
|
||||
" Bit Depth: %i\n"
|
||||
" Double Buffer: %s",
|
||||
static_cast<uint32_t>(config_.output_clock_speed) / 1000000, config_.latch_blanking,
|
||||
TRUEFALSE(config_.clk_phase_inverted), config_.min_refresh_rate, HUB75_BIT_DEPTH,
|
||||
YESNO(config_.double_buffer));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Display/PollingComponent methods
|
||||
// ========================================
|
||||
|
||||
void HUB75Display::update() {
|
||||
if (!driver_) [[unlikely]]
|
||||
return;
|
||||
if (!this->enabled_) [[unlikely]]
|
||||
return;
|
||||
|
||||
this->do_update_();
|
||||
|
||||
if (config_.double_buffer) {
|
||||
driver_->flip_buffer();
|
||||
}
|
||||
}
|
||||
|
||||
void HUB75Display::fill(Color color) {
|
||||
if (!driver_) [[unlikely]]
|
||||
return;
|
||||
if (!this->enabled_) [[unlikely]]
|
||||
return;
|
||||
|
||||
// Special case: black (off) - use fast hardware clear
|
||||
if (!color.is_on()) {
|
||||
driver_->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-black colors, fall back to base class (pixel-by-pixel)
|
||||
Display::fill(color);
|
||||
}
|
||||
|
||||
void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!driver_) [[unlikely]]
|
||||
return;
|
||||
if (!this->enabled_) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
|
||||
return;
|
||||
|
||||
driver_->set_pixel(x, y, color.r, color.g, color.b);
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (!driver_) [[unlikely]]
|
||||
return;
|
||||
if (!this->enabled_) [[unlikely]]
|
||||
return;
|
||||
|
||||
// Map ESPHome enums to hub75 enums
|
||||
Hub75PixelFormat format;
|
||||
Hub75ColorOrder color_order = Hub75ColorOrder::RGB;
|
||||
int bytes_per_pixel;
|
||||
|
||||
// Determine format based on bitness
|
||||
if (bitness == ColorBitness::COLOR_BITNESS_565) {
|
||||
format = Hub75PixelFormat::RGB565;
|
||||
bytes_per_pixel = 2;
|
||||
} else if (bitness == ColorBitness::COLOR_BITNESS_888) {
|
||||
#ifdef USE_LVGL
|
||||
#if LV_COLOR_DEPTH == 32
|
||||
// 32-bit: 4 bytes per pixel with padding byte (LVGL mode)
|
||||
format = Hub75PixelFormat::RGB888_32;
|
||||
bytes_per_pixel = 4;
|
||||
|
||||
// Map ESPHome ColorOrder to Hub75ColorOrder
|
||||
// ESPHome ColorOrder is typically BGR for little-endian 32-bit
|
||||
color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR;
|
||||
#elif LV_COLOR_DEPTH == 24
|
||||
// 24-bit: 3 bytes per pixel, tightly packed
|
||||
format = Hub75PixelFormat::RGB888;
|
||||
bytes_per_pixel = 3;
|
||||
// Note: 24-bit is always RGB order in LVGL
|
||||
#else
|
||||
ESP_LOGE(TAG, "Unsupported LV_COLOR_DEPTH: %d", LV_COLOR_DEPTH);
|
||||
return;
|
||||
#endif
|
||||
#else
|
||||
// Non-LVGL mode: standard 24-bit RGB888
|
||||
format = Hub75PixelFormat::RGB888;
|
||||
bytes_per_pixel = 3;
|
||||
color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR;
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unsupported bitness: %d", static_cast<int>(bitness));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if buffer is tightly packed (no stride)
|
||||
const int stride_px = x_offset + w + x_pad;
|
||||
const bool is_packed = (x_offset == 0 && x_pad == 0 && y_offset == 0);
|
||||
|
||||
if (is_packed) {
|
||||
// Tightly packed buffer - single bulk call for best performance
|
||||
driver_->draw_pixels(x_start, y_start, w, h, ptr, format, color_order, big_endian);
|
||||
} else {
|
||||
// Buffer has stride (padding between rows) - draw row by row
|
||||
for (int yy = 0; yy < h; ++yy) {
|
||||
const size_t row_offset = ((y_offset + yy) * stride_px + x_offset) * bytes_per_pixel;
|
||||
const uint8_t *row_ptr = ptr + row_offset;
|
||||
|
||||
driver_->draw_pixels(x_start, y_start + yy, w, 1, row_ptr, format, color_order, big_endian);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HUB75Display::set_brightness(int brightness) {
|
||||
this->brightness_ = brightness;
|
||||
this->enabled_ = (brightness > 0);
|
||||
if (this->driver_ != nullptr) {
|
||||
this->driver_->set_brightness(brightness);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::hub75
|
||||
|
||||
#endif
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "hub75.h" // hub75 library
|
||||
|
||||
namespace esphome::hub75 {
|
||||
|
||||
using esphome::display::ColorBitness;
|
||||
using esphome::display::ColorOrder;
|
||||
|
||||
class HUB75Display : public display::Display {
|
||||
public:
|
||||
// Constructor accepting config
|
||||
explicit HUB75Display(const Hub75Config &config);
|
||||
|
||||
// Core Component methods
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
// Display/PollingComponent methods
|
||||
void update() override;
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
void fill(Color color) override;
|
||||
void draw_pixel_at(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;
|
||||
|
||||
// Brightness control (runtime mutable)
|
||||
void set_brightness(int brightness);
|
||||
|
||||
protected:
|
||||
// Display internal methods
|
||||
int get_width_internal() override { return config_.panel_width * config_.layout_cols; }
|
||||
int get_height_internal() override { return config_.panel_height * config_.layout_rows; }
|
||||
|
||||
// Member variables
|
||||
Hub75Driver *driver_{nullptr};
|
||||
Hub75Config config_; // Immutable configuration
|
||||
|
||||
// Runtime state (mutable)
|
||||
int brightness_{128};
|
||||
bool enabled_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::hub75
|
||||
|
||||
#endif
|
||||
@@ -2,23 +2,6 @@ import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.components.esp32.gpio_esp32_c5 import esp32_c5_validate_lp_i2c
|
||||
from esphome.components.esp32.gpio_esp32_c6 import esp32_c6_validate_lp_i2c
|
||||
from esphome.components.esp32.gpio_esp32_p4 import esp32_p4_validate_lp_i2c
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
zephyr_add_prj_conf,
|
||||
@@ -33,7 +16,6 @@ from esphome.const import (
|
||||
CONF_I2C,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_LOW_POWER_MODE,
|
||||
CONF_SCAN,
|
||||
CONF_SCL,
|
||||
CONF_SDA,
|
||||
@@ -58,25 +40,6 @@ IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
|
||||
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
|
||||
I2CDevice = i2c_ns.class_("I2CDevice")
|
||||
|
||||
ESP32_I2C_CAPABILITIES = {
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/soc_caps.h
|
||||
VARIANT_ESP32: {"NUM": 2, "HP": 2},
|
||||
VARIANT_ESP32C2: {"NUM": 1, "HP": 1},
|
||||
VARIANT_ESP32C3: {"NUM": 1, "HP": 1},
|
||||
VARIANT_ESP32C5: {"NUM": 2, "HP": 1, "LP": 1},
|
||||
VARIANT_ESP32C6: {"NUM": 2, "HP": 1, "LP": 1},
|
||||
VARIANT_ESP32C61: {"NUM": 1, "HP": 1},
|
||||
VARIANT_ESP32H2: {"NUM": 2, "HP": 2},
|
||||
VARIANT_ESP32P4: {"NUM": 3, "HP": 2, "LP": 1},
|
||||
VARIANT_ESP32S2: {"NUM": 2, "HP": 2},
|
||||
VARIANT_ESP32S3: {"NUM": 2, "HP": 2},
|
||||
}
|
||||
VALIDATE_LP_I2C = {
|
||||
VARIANT_ESP32C5: esp32_c5_validate_lp_i2c,
|
||||
VARIANT_ESP32C6: esp32_c6_validate_lp_i2c,
|
||||
VARIANT_ESP32P4: esp32_p4_validate_lp_i2c,
|
||||
}
|
||||
LP_I2C_VARIANT = list(VALIDATE_LP_I2C.keys())
|
||||
|
||||
CONF_SDA_PULLUP_ENABLED = "sda_pullup_enabled"
|
||||
CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled"
|
||||
@@ -128,13 +91,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.positive_time_period,
|
||||
),
|
||||
cv.Optional(CONF_SCAN, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LOW_POWER_MODE): cv.All(
|
||||
cv.only_on_esp32,
|
||||
esp32.only_on_variant(
|
||||
supported=LP_I2C_VARIANT, msg_prefix="Low power i2c"
|
||||
),
|
||||
cv.boolean,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]),
|
||||
@@ -146,31 +102,6 @@ def _final_validate(config):
|
||||
full_config = fv.full_config.get()[CONF_I2C]
|
||||
if CORE.using_zephyr and len(full_config) > 1:
|
||||
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
|
||||
if CORE.using_esp_idf and get_esp32_variant() in ESP32_I2C_CAPABILITIES:
|
||||
variant = get_esp32_variant()
|
||||
max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"]
|
||||
if len(full_config) > max_num:
|
||||
raise cv.Invalid(
|
||||
f"The maximum number of i2c interfaces for {variant} is {max_num}"
|
||||
)
|
||||
if variant in LP_I2C_VARIANT:
|
||||
max_lp_num = ESP32_I2C_CAPABILITIES[variant]["LP"]
|
||||
max_hp_num = ESP32_I2C_CAPABILITIES[variant]["HP"]
|
||||
lp_num = sum(
|
||||
CONF_LOW_POWER_MODE in conf and conf[CONF_LOW_POWER_MODE]
|
||||
for conf in full_config
|
||||
)
|
||||
hp_num = len(full_config) - lp_num
|
||||
if CONF_LOW_POWER_MODE in config and config[CONF_LOW_POWER_MODE]:
|
||||
VALIDATE_LP_I2C[variant](config)
|
||||
if lp_num > max_lp_num:
|
||||
raise cv.Invalid(
|
||||
f"The maximum number of low power i2c interfaces for {variant} is {max_lp_num}"
|
||||
)
|
||||
if hp_num > max_hp_num:
|
||||
raise cv.Invalid(
|
||||
f"The maximum number of high power i2c interfaces for {variant} is {max_hp_num}"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -224,8 +155,6 @@ async def to_code(config):
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
if CORE.using_arduino and not CORE.is_esp32:
|
||||
cg.add_library("Wire", None)
|
||||
if CONF_LOW_POWER_MODE in config:
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
|
||||
|
||||
def i2c_device_schema(default_address):
|
||||
|
||||
@@ -16,10 +16,13 @@ namespace i2c {
|
||||
static const char *const TAG = "i2c.idf";
|
||||
|
||||
void IDFI2CBus::setup() {
|
||||
static i2c_port_t next_hp_port = I2C_NUM_0;
|
||||
#if SOC_LP_I2C_SUPPORTED
|
||||
static i2c_port_t next_lp_port = LP_I2C_NUM_0;
|
||||
#endif
|
||||
static i2c_port_t next_port = I2C_NUM_0;
|
||||
this->port_ = next_port;
|
||||
if (this->port_ == I2C_NUM_MAX) {
|
||||
ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->timeout_ > 13000) {
|
||||
ESP_LOGW(TAG, "Using max allowed timeout: 13 ms");
|
||||
@@ -28,35 +31,23 @@ void IDFI2CBus::setup() {
|
||||
|
||||
this->recover_();
|
||||
|
||||
next_port = (i2c_port_t) (next_port + 1);
|
||||
|
||||
i2c_master_bus_config_t bus_conf{};
|
||||
memset(&bus_conf, 0, sizeof(bus_conf));
|
||||
bus_conf.sda_io_num = gpio_num_t(sda_pin_);
|
||||
bus_conf.scl_io_num = gpio_num_t(scl_pin_);
|
||||
bus_conf.i2c_port = this->port_;
|
||||
bus_conf.glitch_ignore_cnt = 7;
|
||||
#if SOC_LP_I2C_SUPPORTED
|
||||
if (this->lp_mode_) {
|
||||
if ((next_lp_port - LP_I2C_NUM_0) == SOC_LP_I2C_NUM) {
|
||||
ESP_LOGE(TAG, "No more than %u LP buses supported", SOC_LP_I2C_NUM);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->port_ = next_lp_port;
|
||||
next_lp_port = (i2c_port_t) (next_lp_port + 1);
|
||||
bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT;
|
||||
} else {
|
||||
#endif
|
||||
if (next_hp_port == SOC_HP_I2C_NUM) {
|
||||
ESP_LOGE(TAG, "No more than %u HP buses supported", SOC_HP_I2C_NUM);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->port_ = next_hp_port;
|
||||
next_hp_port = (i2c_port_t) (next_hp_port + 1);
|
||||
if (this->port_ < SOC_HP_I2C_NUM) {
|
||||
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
|
||||
#if SOC_LP_I2C_SUPPORTED
|
||||
} else {
|
||||
bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT;
|
||||
}
|
||||
#else
|
||||
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
|
||||
#endif
|
||||
bus_conf.i2c_port = this->port_;
|
||||
bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_;
|
||||
esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_);
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -30,9 +30,6 @@ class IDFI2CBus : public InternalI2CBus, public Component {
|
||||
void set_scl_pullup_enabled(bool scl_pullup_enabled) { this->scl_pullup_enabled_ = scl_pullup_enabled; }
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
#if SOC_LP_I2C_SUPPORTED
|
||||
void set_lp_mode(bool lp_mode) { this->lp_mode_ = lp_mode; }
|
||||
#endif
|
||||
|
||||
int get_port() const override { return this->port_; }
|
||||
|
||||
@@ -51,9 +48,6 @@ class IDFI2CBus : public InternalI2CBus, public Component {
|
||||
uint32_t frequency_{};
|
||||
uint32_t timeout_ = 0;
|
||||
bool initialized_ = false;
|
||||
#if SOC_LP_I2C_SUPPORTED
|
||||
bool lp_mode_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace i2c
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
|
||||
@@ -73,7 +71,6 @@ I2S_PORTS = {
|
||||
VARIANT_ESP32C3: 1,
|
||||
VARIANT_ESP32C5: 1,
|
||||
VARIANT_ESP32C6: 1,
|
||||
VARIANT_ESP32C61: 1,
|
||||
VARIANT_ESP32H2: 1,
|
||||
VARIANT_ESP32P4: 3,
|
||||
VARIANT_ESP32S2: 1,
|
||||
|
||||
@@ -40,7 +40,7 @@ INTERNAL_DAC_OPTIONS = {
|
||||
|
||||
EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO]
|
||||
|
||||
NO_INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32S2]
|
||||
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
|
||||
|
||||
I2C_COMM_FMT_OPTIONS = ["lsb", "msb"]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user