mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 02:32:49 +00:00
Compare commits
116
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2890c82080 | ||
|
|
dcb943ded3 | ||
|
|
2c6f59738c | ||
|
|
04715fb32b | ||
|
|
3df9360876 | ||
|
|
61f3de0bc0 | ||
|
|
cd97c9a9cb | ||
|
|
d3668a7186 | ||
|
|
ac74796263 | ||
|
|
c56bb14037 | ||
|
|
7abab6cca9 | ||
|
|
a4278b8dec | ||
|
|
6d173c7371 | ||
|
|
1779b011c9 | ||
|
|
3c4cf50cb6 | ||
|
|
e869d1f7ab | ||
|
|
e1a996435d | ||
|
|
c5a624a1f2 | ||
|
|
15fb7f775e | ||
|
|
3fa72e61c6 | ||
|
|
163b09a473 | ||
|
|
7afc2768f8 | ||
|
|
56f1a2bf46 | ||
|
|
fee47fe2f1 | ||
|
|
b994e081c3 | ||
|
|
a1f740b71b | ||
|
|
7f144365ba | ||
|
|
928459d16f | ||
|
|
9163359445 | ||
|
|
67307f0eeb | ||
|
|
91e0edc11d | ||
|
|
9fe75e262f | ||
|
|
0132f722a3 | ||
|
|
7d7a78308d | ||
|
|
9c568525b0 | ||
|
|
10f42e3e7c | ||
|
|
25d4b88605 | ||
|
|
d6abf35138 | ||
|
|
66ea99ce65 | ||
|
|
d9298cb7b9 | ||
|
|
34e1cf96d5 | ||
|
|
56cc03c4d4 | ||
|
|
35b416c6c0 | ||
|
|
7f2c3ba3f5 | ||
|
|
a97604d574 | ||
|
|
1146b77836 | ||
|
|
1338902ac5 | ||
|
|
60a1d70805 | ||
|
|
887d4c3a83 | ||
|
|
c1fa73470e | ||
|
|
e41d16ba7e | ||
|
|
5171113c87 | ||
|
|
5718e4b779 | ||
|
|
40e12cd0d8 | ||
|
|
07029d0077 | ||
|
|
bb1119d83f | ||
|
|
7b591b26d6 | ||
|
|
f45e6f0bf4 | ||
|
|
4b8af08155 | ||
|
|
613973b432 | ||
|
|
9ee4314bc2 | ||
|
|
59f55dbe9e | ||
|
|
8fce95454a | ||
|
|
16134ee993 | ||
|
|
bc39d56bb4 | ||
|
|
c5522abc71 | ||
|
|
7cb5906d2a | ||
|
|
d452dbce30 | ||
|
|
81e5b50087 | ||
|
|
426a4a2dce | ||
|
|
c95c17c4e7 | ||
|
|
576d3325fe | ||
|
|
c9cf0ad92e | ||
|
|
f505212e64 | ||
|
|
52a5e6a007 | ||
|
|
3b261d2cf0 | ||
|
|
9238fcc727 | ||
|
|
96902cc122 | ||
|
|
95711d83d1 | ||
|
|
3cdcf0417a | ||
|
|
c6e90de262 | ||
|
|
92f6ab8367 | ||
|
|
f61955baa2 | ||
|
|
36e1d831d3 | ||
|
|
63616bf642 | ||
|
|
d275926f65 | ||
|
|
f265c46c21 | ||
|
|
7d735f0677 | ||
|
|
5c23a827b6 | ||
|
|
e265c0c0fc | ||
|
|
a0c7d754cb | ||
|
|
a6dbd72c53 | ||
|
|
9d530f5c56 | ||
|
|
6562c49686 | ||
|
|
a97ce3a201 | ||
|
|
5a78f636b9 | ||
|
|
53d504137b | ||
|
|
bb0546fc0b | ||
|
|
b43630cf8e | ||
|
|
c9664d4e42 | ||
|
|
39ea6a3220 | ||
|
|
f4a6d6f18b | ||
|
|
6c24d4aa43 | ||
|
|
0dc1d3b5a1 | ||
|
|
42341d05f9 | ||
|
|
f941fee5ed | ||
|
|
8721976066 | ||
|
|
05bf7d3440 | ||
|
|
75dd7b1a94 | ||
|
|
4cb335934e | ||
|
|
736f6b53d2 | ||
|
|
a715bdae7d | ||
|
|
fe151ba802 | ||
|
|
5da9cbdefa | ||
|
|
016ee8a62d | ||
|
|
51a8a56335 |
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
|
||||
- name: Upload compile-test image artifact
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
# The tar is already compressed, so upload it as-is. archive: false
|
||||
# skips the redundant zip and makes the file name the artifact name
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Download image artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: compile-test-image.tar.zst
|
||||
- name: Load image
|
||||
|
||||
+36
-22
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -81,8 +81,8 @@ jobs:
|
||||
- name: Install apt packages (cached)
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
determine-jobs:
|
||||
name: Determine which jobs to run
|
||||
@@ -415,8 +415,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -438,7 +438,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -477,7 +477,7 @@ jobs:
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
||||
path: test_artifacts/
|
||||
@@ -488,7 +488,7 @@ jobs:
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
# runs produce usable data.
|
||||
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: junit-integration-${{ strategy.job-index }}
|
||||
path: junit-integration.xml
|
||||
@@ -528,7 +528,7 @@ jobs:
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
@@ -626,7 +626,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
|
||||
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -680,7 +680,7 @@ jobs:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
|
||||
pio_cache_key: tidyesp8266
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino
|
||||
@@ -693,12 +693,16 @@ jobs:
|
||||
ignore_errors: false
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
|
||||
pio_cache_key: tidyrp2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for host
|
||||
options: --environment host-tidy --grep USE_HOST
|
||||
pio_cache_key: tidyhost
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for LibreTiny
|
||||
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
pio_cache_key: tidylibretiny
|
||||
|
||||
steps:
|
||||
@@ -707,6 +711,16 @@ jobs:
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
- name: Install apt packages (cached)
|
||||
if: matrix.pio_cache_key == 'tidyhost'
|
||||
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
|
||||
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
|
||||
# exactly.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
@@ -989,14 +1003,14 @@ jobs:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM
|
||||
tidy_environment: esp32s3-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 P4
|
||||
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
|
||||
# so their code paths differ -- lint them under the P4 build too.
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
tidy_environment: esp32p4-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 C6
|
||||
@@ -1102,8 +1116,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -1448,7 +1462,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1647,7 +1661,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -1710,7 +1724,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -1740,13 +1754,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -10,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.10
|
||||
rev: v0.17.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
rev: v3.22.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
|
||||
@@ -219,6 +219,7 @@ esphome/components/friedrich/* @rwrozelle
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
esphome/components/fusb302b/* @remcom
|
||||
esphome/components/gcja5/* @gcormier
|
||||
esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/gl_r01_i2c/* @pkejval
|
||||
@@ -240,6 +241,7 @@ esphome/components/gt911/* @clydebarrow @jesserockz
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/haier/binary_sensor/* @paveldn
|
||||
esphome/components/haier/button/* @paveldn
|
||||
esphome/components/haier/select/* @paveldn
|
||||
esphome/components/haier/sensor/* @paveldn
|
||||
esphome/components/haier/switch/* @paveldn
|
||||
esphome/components/haier/text_sensor/* @paveldn
|
||||
@@ -385,6 +387,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/midea_portasplit/* @Fexiven
|
||||
esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_rgb/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
@@ -439,6 +442,7 @@ esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
|
||||
@@ -3043,12 +3043,15 @@ enum SerialProxyIdentityFlag {
|
||||
// IDs below are empty
|
||||
}
|
||||
|
||||
// The descriptor fields of a USB device as seen by the host stack
|
||||
// The descriptor fields of a USB device as seen by the host stack, and the port's index
|
||||
// among those a vendor driver exposes on the interface
|
||||
message UsbDeviceDescriptor {
|
||||
uint32 vendor_id = 1;
|
||||
uint32 product_id = 2;
|
||||
uint32 bcd_device = 3;
|
||||
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
|
||||
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
|
||||
uint32 interface_class = 5; // bInterfaceClass of that interface
|
||||
uint32 port_number = 6;
|
||||
}
|
||||
|
||||
// The identity of the device behind a port. Identifies one physical device among others of
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "api_connection.h"
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -451,6 +454,15 @@ bool APIConnection::send_disconnect_response_() {
|
||||
DisconnectResponse resp;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
void APIConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
logger::log_crash_to_client();
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::on_disconnect_response() {
|
||||
// Don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
#include "subscribe_state.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
#include "esphome/components/esp32/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
#include "esphome/components/rp2/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
@@ -298,21 +289,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
rp2::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
|
||||
@@ -4175,6 +4175,8 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
|
||||
return pos;
|
||||
}
|
||||
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
@@ -4184,6 +4186,8 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.product_id);
|
||||
size += ProtoSize::calc_uint32(1, msg.bcd_device);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_number);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_class);
|
||||
size += ProtoSize::calc_uint32(1, msg.port_number);
|
||||
return size;
|
||||
}
|
||||
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
|
||||
@@ -4083,6 +4083,8 @@ class UsbDeviceDescriptor final : public ProtoMessage {
|
||||
uint32_t product_id{0};
|
||||
uint32_t bcd_device{0};
|
||||
uint32_t interface_number{0};
|
||||
uint32_t interface_class{0};
|
||||
uint32_t port_number{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -2942,6 +2942,8 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
|
||||
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
|
||||
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
|
||||
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
|
||||
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bedjet {
|
||||
|
||||
static const char *const TAG = "bedjet";
|
||||
ESPHOME_LOG_TAG(TAG, "bedjet");
|
||||
|
||||
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
|
||||
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
const char *const Automation::TAG = "ble_client.automation";
|
||||
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
|
||||
|
||||
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
|
||||
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
|
||||
// The chip counts weekdays 0-6 from Sunday (not BCD), ESPTime counts 1-7
|
||||
time.day_of_week = (buf[1] & 0x07) + 1;
|
||||
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
|
||||
|
||||
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
|
||||
@@ -115,7 +116,7 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
void BM8563::set_date_(const ESPTime &time) {
|
||||
uint8_t buf[4] = {
|
||||
this->byte_to_bcd2_(time.day_of_month),
|
||||
this->byte_to_bcd2_(time.day_of_week),
|
||||
static_cast<uint8_t>(time.day_of_week - 1),
|
||||
this->byte_to_bcd2_(time.month),
|
||||
this->byte_to_bcd2_(time.year % 100),
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bme280_base/bme280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bme280_i2c {
|
||||
|
||||
static const char *const TAG = "bme280_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
|
||||
|
||||
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bmp280_base/bmp280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp280_i2c {
|
||||
|
||||
static const char *const TAG = "bmp280_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
|
||||
|
||||
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
|
||||
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bmp581_base/bmp581_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp581_i2c {
|
||||
|
||||
static const char *const TAG = "bmp581_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
|
||||
|
||||
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
|
||||
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::chsc6x {
|
||||
|
||||
static const char *const TAG = "chsc6x.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
|
||||
|
||||
static const uint8_t CHSC6X_REG_STATUS = 0x00;
|
||||
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace esphome::cse7766 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cse7766");
|
||||
|
||||
// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
|
||||
static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
|
||||
|
||||
void CSE7766Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_transmission_ >= 500) {
|
||||
@@ -154,18 +157,33 @@ void CSE7766Component::parse_data_() {
|
||||
if (power_cycle_exceeds_range) {
|
||||
// Datasheet: power cycle exceeding range means active power is 0
|
||||
have_power = true;
|
||||
this->last_power_cycle_ = 0;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
} else if (have_power) {
|
||||
power = power_coeff / float(power_cycle);
|
||||
this->last_power_cycle_ = power_cycle;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else if (this->power_sensor_ != nullptr) {
|
||||
// No valid power measurement from chip - publish 0W to avoid stale readings
|
||||
// This typically happens when current is below the measurable threshold (~50mA)
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
} else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
|
||||
// No new measurement in this frame. With cycles over 1 s the register holds the last
|
||||
// cycle or the time elapsed since the last pulse. While that time is within twice the
|
||||
// last cycle, the next pulse is not overdue, so the last measurement still applies.
|
||||
// Shorter cycles never get here in normal operation and keep the previous behaviour.
|
||||
have_power = true;
|
||||
power = power_coeff / float(this->last_power_cycle_);
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else {
|
||||
// The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
|
||||
// than half also reads 0 W here until its first new pulse arrives.
|
||||
this->last_power_cycle_ = 0;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
float current = 0.0f;
|
||||
|
||||
@@ -36,6 +36,7 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
|
||||
|
||||
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
|
||||
uint8_t raw_data_index_{0};
|
||||
uint16_t cf_pulses_last_{0};
|
||||
uint32_t last_transmission_{0};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
@@ -45,7 +46,8 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
|
||||
sensor::Sensor *reactive_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
uint32_t cf_pulses_total_{0};
|
||||
uint16_t cf_pulses_last_{0};
|
||||
// Power cycle of the last frame that carried a new power measurement, 0 if none
|
||||
uint32_t last_power_cycle_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst816 {
|
||||
|
||||
static const char *const TAG = "cst816.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
|
||||
|
||||
static const uint8_t REG_STATUS = 0x00;
|
||||
static const uint8_t REG_TOUCH_NUM = 0x02;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
static const char *const TAG = "cst9220.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
|
||||
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
|
||||
@@ -250,6 +250,42 @@ def add_metadata(
|
||||
)
|
||||
|
||||
|
||||
KEY_EXTERNAL_WRITERS = "external_writers"
|
||||
|
||||
|
||||
def _get_external_writer_list() -> list[tuple[ID, str]]:
|
||||
"""Get the raw external writer list. Each entry is (display id, owner)."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
|
||||
|
||||
|
||||
def register_external_writer(display_id: ID, owner: str) -> None:
|
||||
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
|
||||
|
||||
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
|
||||
resolves it in place before final validation.
|
||||
"""
|
||||
_get_external_writer_list().append((display_id, owner))
|
||||
|
||||
|
||||
def get_external_writers(display_id: ID) -> list[str]:
|
||||
"""Get the owners registered for a display. Only valid once IDs are resolved."""
|
||||
return [
|
||||
owner
|
||||
for id_, owner in _get_external_writer_list()
|
||||
if id_.id is not None and id_.id == display_id.id
|
||||
]
|
||||
|
||||
|
||||
def has_external_writer(display_id: ID) -> bool:
|
||||
"""Tell a driver's final validation that something else draws into the display.
|
||||
|
||||
LVGL anywhere in the config also counts, as the drivers assumed before.
|
||||
"""
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
|
||||
|
||||
|
||||
async def register_display(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await setup_display_core_(var, config)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
void Display::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) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::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) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
|
||||
@@ -139,6 +139,11 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
|
||||
|
||||
virtual DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -8,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
@@ -154,4 +178,54 @@ class ColorUtil {
|
||||
return color;
|
||||
}
|
||||
};
|
||||
|
||||
/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */
|
||||
static constexpr uint16_t REC601_LUMA_R = 77;
|
||||
static constexpr uint16_t REC601_LUMA_G = 150;
|
||||
static constexpr uint16_t REC601_LUMA_B = 29;
|
||||
|
||||
/** Rec.601 luma of an RGB color, for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division
|
||||
* per channel.
|
||||
*/
|
||||
class Rgb565ToMono {
|
||||
public:
|
||||
explicit Rgb565ToMono(ColorOrder order) {
|
||||
// Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order
|
||||
uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B;
|
||||
if (order == COLOR_ORDER_BGR) {
|
||||
high = REC601_LUMA_B;
|
||||
low = REC601_LUMA_R;
|
||||
} else if (order == COLOR_ORDER_GRB) {
|
||||
high = REC601_LUMA_G;
|
||||
middle = REC601_LUMA_R;
|
||||
}
|
||||
// Each field value scaled to 0..255, then weighted
|
||||
for (unsigned i = 0; i != 32; i++) {
|
||||
this->high_[i] = high * esp_scale(i, 31);
|
||||
this->low_[i] = low * esp_scale(i, 31);
|
||||
}
|
||||
for (unsigned i = 0; i != 64; i++)
|
||||
this->middle_[i] = middle * esp_scale(i, 63);
|
||||
}
|
||||
|
||||
uint8_t operator()(uint16_t value) const {
|
||||
const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F];
|
||||
return ((weighted + 128u) >> 8) >= 128 ? 1 : 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Weighted contribution of each value of each field
|
||||
std::array<uint16_t, 32> high_{};
|
||||
std::array<uint16_t, 64> middle_{};
|
||||
std::array<uint16_t, 32> low_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::display
|
||||
|
||||
@@ -23,11 +23,18 @@ DEPENDENCIES = ["uart"]
|
||||
|
||||
emontx_ns = cg.esphome_ns.namespace("emontx")
|
||||
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
|
||||
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
|
||||
|
||||
CONF_EMONTX_ID = "emontx_id"
|
||||
CONF_TAG_NAME = "tag_name"
|
||||
CONF_ON_JSON = "on_json"
|
||||
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
|
||||
LOCK_ON_REBOOT_AUTO = "auto"
|
||||
LOCK_ON_REBOOT_MODES = {
|
||||
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
|
||||
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
|
||||
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
|
||||
}
|
||||
|
||||
DOMAIN = "emontx"
|
||||
|
||||
@@ -59,7 +66,9 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(EmonTx),
|
||||
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
|
||||
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -135,8 +144,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
await uart.register_uart_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if config[CONF_LOCK_ON_REBOOT]:
|
||||
cg.add(var.set_lock_on_reboot(True))
|
||||
lock = config[CONF_LOCK_ON_REBOOT]
|
||||
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
|
||||
if lock == LOCK_ON_REBOOT_AUTO:
|
||||
# Locking depends on the firmware version the emonTx reports
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
|
||||
|
||||
# Action: emontx.send_command
|
||||
|
||||
@@ -7,30 +7,85 @@ namespace esphome::emontx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "emontx");
|
||||
|
||||
static constexpr uint32_t UNLOCK_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
|
||||
static constexpr uint32_t COMMAND_GAP_MS = 250;
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
|
||||
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 1;
|
||||
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
|
||||
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
|
||||
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
|
||||
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
|
||||
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
|
||||
// Every data line starts with the message counter, in JSON or text format
|
||||
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
|
||||
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
|
||||
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
|
||||
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
|
||||
static constexpr uint32_t LOCK_MIN_MINOR = 2;
|
||||
|
||||
// True if line starts with the PROGMEM prefix and has more after it
|
||||
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
|
||||
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
|
||||
}
|
||||
|
||||
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
|
||||
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
|
||||
const char *start = pos;
|
||||
value = 0;
|
||||
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
|
||||
value = value * 10 + (*pos++ - '0');
|
||||
return pos != start;
|
||||
}
|
||||
|
||||
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
|
||||
static bool supports_lock(const char *version, const char *end) {
|
||||
if (version < end && (*version == 'v' || *version == 'V'))
|
||||
version++;
|
||||
uint32_t major;
|
||||
if (!read_number(version, end, major) || version == end || *version != '.')
|
||||
return false;
|
||||
version++;
|
||||
uint32_t minor;
|
||||
if (!read_number(version, end, minor))
|
||||
return false;
|
||||
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EmonTx::setup() {
|
||||
this->buffer_pos_ = 0;
|
||||
if (this->lock_on_reboot_) {
|
||||
// The line feed ends any boot noise first
|
||||
ESP_LOGI(TAG, "Unlocking emonTx after boot");
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
// The line feed ends any boot noise
|
||||
this->write_byte('\n');
|
||||
this->send_command("emonunlock");
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
this->unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
});
|
||||
return;
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
// With AUTO, the reply decides whether to unlock
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
void EmonTx::unlock_() {
|
||||
ESP_LOGI(TAG, "Unlocking emonTx");
|
||||
this->send_command("emonunlock");
|
||||
}
|
||||
|
||||
void EmonTx::on_shutdown() {
|
||||
if (!this->lock_on_reboot_)
|
||||
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
|
||||
#endif
|
||||
if (!lock)
|
||||
return;
|
||||
if (this->is_paused()) {
|
||||
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
|
||||
@@ -63,7 +118,7 @@ void EmonTx::loop() {
|
||||
this->discarding_line_ = dropped != '\n';
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The emonTx may have been flashed, or a request dropped, during the pause
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
@@ -96,8 +151,10 @@ void EmonTx::loop() {
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
if (this->wants_version_()) {
|
||||
this->parse_msg_number_(this->buffer_.data(), len);
|
||||
this->parse_version_line_(this->buffer_.data(), len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (received == '\r' || this->discarding_line_) {
|
||||
@@ -124,16 +181,6 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
auto msg = root["MSG"];
|
||||
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
uint32_t msg_number = msg;
|
||||
if (msg_number <= this->last_msg_number_)
|
||||
this->request_version_();
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->json_callbacks_.call(root, StringRef(data, len));
|
||||
return true;
|
||||
@@ -145,17 +192,50 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::request_version_() {
|
||||
// The firmware may have changed; until the reply, an unknown version gets no lock commands
|
||||
this->lock_supported_ = false;
|
||||
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
|
||||
this->send_version_request_();
|
||||
}
|
||||
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::send_version_request_() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
|
||||
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
|
||||
}
|
||||
|
||||
void EmonTx::parse_msg_number_(const char *line, size_t len) {
|
||||
const char *pos;
|
||||
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
|
||||
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
uint32_t msg_number;
|
||||
if (!read_number(pos, line + len, msg_number))
|
||||
return;
|
||||
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
if (msg_number <= this->last_msg_number_) {
|
||||
this->request_version_();
|
||||
} else if (this->version_frames_left_ > 0) {
|
||||
if (--this->version_frames_left_ == 0) {
|
||||
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
|
||||
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
|
||||
this->send_version_request_();
|
||||
}
|
||||
}
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
|
||||
void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
|
||||
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
|
||||
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
|
||||
return;
|
||||
const char *version = line + prefix_len;
|
||||
const char *end = line + len;
|
||||
@@ -164,7 +244,23 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
if (version == end)
|
||||
return;
|
||||
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
this->version_frames_left_ = 0;
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
#endif
|
||||
|
||||
bool supported = supports_lock(version, end);
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
|
||||
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->unlock_();
|
||||
this->unlock_sent_ = true;
|
||||
});
|
||||
}
|
||||
this->lock_supported_ = supported;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -173,7 +269,13 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
*/
|
||||
void EmonTx::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "EmonTx:");
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
|
||||
const char *lock = LOG_STR_LITERAL("no");
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
lock = LOG_STR_LITERAL("yes");
|
||||
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
|
||||
lock = LOG_STR_LITERAL("auto");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
|
||||
@@ -183,7 +285,7 @@ void EmonTx::dump_config() {
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace esphome::emontx {
|
||||
/// Maximum line length in bytes (plus one byte reserved for null terminator)
|
||||
static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
|
||||
enum class LockOnReboot : uint8_t {
|
||||
LOCK_ON_REBOOT_NEVER,
|
||||
LOCK_ON_REBOOT_ALWAYS,
|
||||
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
|
||||
};
|
||||
|
||||
/**
|
||||
* @class EmonTx
|
||||
* @brief Main class for the EmonTx component.
|
||||
@@ -30,7 +36,7 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
* extracts tags and values, and publishes them to registered sensors.
|
||||
*/
|
||||
class EmonTx final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(version)
|
||||
#endif
|
||||
public:
|
||||
@@ -56,8 +62,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
|
||||
|
||||
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
|
||||
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
|
||||
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
|
||||
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void init_sensors(size_t count) { this->sensors_.init(count); }
|
||||
@@ -66,8 +72,18 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
void parse_json_(const char *data, size_t len);
|
||||
void unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool wants_version_() const {
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
return true;
|
||||
#endif
|
||||
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
|
||||
}
|
||||
void request_version_();
|
||||
void send_version_request_();
|
||||
void parse_msg_number_(const char *line, size_t len);
|
||||
void parse_version_line_(const char *line, size_t len);
|
||||
#endif
|
||||
|
||||
@@ -79,9 +95,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
uint16_t buffer_pos_{0};
|
||||
std::atomic<bool> paused_{false};
|
||||
bool was_paused_{false};
|
||||
bool lock_on_reboot_{false};
|
||||
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
|
||||
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
|
||||
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
|
||||
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
|
||||
uint32_t last_msg_number_{0};
|
||||
#endif
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
|
||||
@@ -21,3 +21,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
|
||||
@@ -13,21 +14,15 @@
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other
|
||||
// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too.
|
||||
using display::color_to_mono;
|
||||
using display::rec601_luma;
|
||||
using display::Rgb565ToMono;
|
||||
|
||||
/** Delta for when to regard as gray */
|
||||
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
|
||||
|
||||
/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map RGB color to a single monochrome bit
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
* @return 1 = white, 0 = black
|
||||
*/
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** Map RGB color to one of 4 discrete gray levels (2 bits per pixel)
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
|
||||
@@ -40,7 +40,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -198,15 +197,11 @@ def _final_validate(config) -> None:
|
||||
"epaper_spi", require_miso=False, require_mosi=True
|
||||
)(config)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
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")
|
||||
|
||||
@@ -131,9 +131,7 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
#endif
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
|
||||
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
#endif
|
||||
} else {
|
||||
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
void EPaperBase::abandon_update_() {
|
||||
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
|
||||
this->epaper_state_to_string_());
|
||||
this->status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
if (this->state_ == EPaperState::TRANSFER_DATA)
|
||||
this->abort_transfer();
|
||||
// Leave the panel powered down rather than with its high-voltage drivers enabled
|
||||
this->power_off();
|
||||
this->full_update_requested_ = true;
|
||||
this->current_data_index_ = 0;
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the state machine.
|
||||
* Typical state sequence:
|
||||
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
break;
|
||||
@@ -345,6 +362,120 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies
|
||||
// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step
|
||||
// rows.
|
||||
template<bool SwapAxes, bool BigEndianSource>
|
||||
void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count,
|
||||
const Rgb565ToMono &to_mono) {
|
||||
const ptrdiff_t byte_step = SwapAxes ? step * static_cast<ptrdiff_t>(row_width) : 0;
|
||||
for (; count != 0; count--, source += 2) {
|
||||
const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8);
|
||||
if (to_mono(value)) {
|
||||
*byte |= mask;
|
||||
} else {
|
||||
*byte &= ~mask;
|
||||
}
|
||||
if constexpr (SwapAxes) {
|
||||
byte += byte_step;
|
||||
} else if (step > 0) {
|
||||
mask >>= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x80;
|
||||
byte++;
|
||||
}
|
||||
} else {
|
||||
mask <<= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x01;
|
||||
byte--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so
|
||||
// the visible part of a block and its dirty area are worked out once for the whole block.
|
||||
void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
ColorOrder order, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const bool swap = this->effective_transform_ & SWAP_XY;
|
||||
const bool mirror_x = this->effective_transform_ & MIRROR_X;
|
||||
const bool mirror_y = this->effective_transform_ & MIRROR_Y;
|
||||
// The buffer axis a source column moves along, and the one a source row moves along
|
||||
const int column_limit = swap ? this->height_ : this->width_;
|
||||
const int row_limit = swap ? this->width_ : this->height_;
|
||||
const bool column_mirrored = swap ? mirror_y : mirror_x;
|
||||
const bool row_mirrored = swap ? mirror_x : mirror_y;
|
||||
const int first_column = std::max(0, -x_start);
|
||||
const int end_column = std::min(w, column_limit - x_start);
|
||||
const int first_row = std::max(0, -y_start);
|
||||
const int end_row = std::min(h, row_limit - y_start);
|
||||
if (first_column >= end_column || first_row >= end_row)
|
||||
return;
|
||||
auto column_to_buffer = [&](int column) {
|
||||
const int u = x_start + column;
|
||||
return column_mirrored ? column_limit - 1 - u : u;
|
||||
};
|
||||
auto row_to_buffer = [&](int row) {
|
||||
const int v = y_start + row;
|
||||
return row_mirrored ? row_limit - 1 - v : v;
|
||||
};
|
||||
|
||||
const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1));
|
||||
const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1));
|
||||
const int columns = end_column - first_column;
|
||||
const int rows = end_row - first_row;
|
||||
const int x_low = swap ? row_low : column_low;
|
||||
const int y_low = swap ? column_low : row_low;
|
||||
const int x_high = x_low + (swap ? rows : columns);
|
||||
const int y_high = y_low + (swap ? columns : rows);
|
||||
|
||||
const Rgb565ToMono to_mono(order);
|
||||
const size_t line_stride = x_offset + w + x_pad;
|
||||
const size_t row_width = this->row_width_;
|
||||
const int step = column_mirrored ? -1 : 1;
|
||||
for (int row = first_row; row != end_row; row++) {
|
||||
const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2;
|
||||
const int along = column_to_buffer(first_column);
|
||||
const int across = row_to_buffer(row);
|
||||
const int x = swap ? across : along;
|
||||
const int y = swap ? along : across;
|
||||
uint8_t *byte = buffer + y * row_width + x / 8;
|
||||
const uint8_t mask = 0x80u >> (x % 8);
|
||||
if (swap) {
|
||||
if (big_endian) {
|
||||
write_rgb565_row<true, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<true, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
} else if (big_endian) {
|
||||
write_rgb565_row<false, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<false, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
}
|
||||
this->x_low_ = clamp_at_most(this->x_low_, x_low);
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x_high);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y_low);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y_high);
|
||||
}
|
||||
|
||||
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
|
||||
// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at().
|
||||
void HOT EPaperBase::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) {
|
||||
size_t contiguous_length;
|
||||
uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length);
|
||||
if (!this->buffer_is_mono() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 ||
|
||||
contiguous_length != this->buffer_length_) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void EPaperBase::dump_config() {
|
||||
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -106,7 +106,10 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
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, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
// Make the next update a full one. Applied when that update starts, so one in progress is not affected.
|
||||
void request_full_update() { this->full_update_requested_ = true; }
|
||||
// True from the start of an update until the display has refreshed and gone back to sleep.
|
||||
@@ -125,9 +128,14 @@ class EPaperBase : public Display,
|
||||
virtual bool initialise(bool partial);
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
// Whether the buffer holds 1 bit per pixel as draw_pixel_at() writes it, so draw_pixels_at() can write it directly
|
||||
virtual bool buffer_is_mono() const { return false; }
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
@@ -137,6 +145,8 @@ class EPaperBase : public Display,
|
||||
* @return true if done, false if it should be called next loop
|
||||
*/
|
||||
virtual bool transfer_data() = 0;
|
||||
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
|
||||
virtual void abort_transfer() {}
|
||||
/**
|
||||
* Refresh the screen after data transfer
|
||||
*/
|
||||
@@ -187,18 +197,20 @@ class EPaperBase : public Display,
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t waiting_for_idle_last_print_{};
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#endif
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
uint32_t update_start_time_{};
|
||||
#endif
|
||||
|
||||
// properties with specific initialisers go last
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
uint32_t reset_duration_{10};
|
||||
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
|
||||
uint16_t busy_timeout_seconds_{120};
|
||||
uint8_t full_update_every_{1};
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -15,6 +15,7 @@ class EPaperMono : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override {}
|
||||
void power_off() override{};
|
||||
|
||||
@@ -58,7 +58,9 @@ bool HOT EPaperSSD1677::transfer_data() {
|
||||
// A full update ignores 0x26, and the copy may not hold a real frame yet (first update after
|
||||
// boot): send the new frame to both planes.
|
||||
const bool send_copy = this->plane_ == 0 && this->update_count_ != 0;
|
||||
const bool direct = send_copy || this->buffer_is_plane();
|
||||
// A mono buffer already holds the frame as a RAM plane does, so it can be sent as it is; a deeper
|
||||
// buffer (the four-level model) returns false and packs the plane in plane_row().
|
||||
const bool direct = send_copy || this->buffer_is_mono();
|
||||
const auto &source = send_copy ? this->sent_ : this->buffer_;
|
||||
const size_t row_length = this->plane_row_length_();
|
||||
const size_t plane_length = row_length * this->height_;
|
||||
|
||||
@@ -34,9 +34,6 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
void init_comparison_frame_();
|
||||
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
|
||||
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
|
||||
// Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is.
|
||||
// A subclass with a deeper buffer returns false and overrides plane_row().
|
||||
virtual bool buffer_is_plane() const { return true; }
|
||||
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
|
||||
virtual void plane_row(size_t y, uint8_t *out);
|
||||
bool reset() override;
|
||||
|
||||
@@ -38,9 +38,9 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return false; }
|
||||
// A partial update if partial updates are enabled and this is not a full one.
|
||||
bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); }
|
||||
bool buffer_is_plane() const override { return false; }
|
||||
void plane_row(size_t y, uint8_t *out) override;
|
||||
void refresh_screen(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
|
||||
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::abort_transfer() {
|
||||
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
|
||||
if (this->current_data_index_ == 0)
|
||||
return;
|
||||
this->disable();
|
||||
this->cs_pin_->digital_write(true);
|
||||
this->cs1_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
void EPaperT133A01::dump_config() {
|
||||
EPaperBase::dump_config();
|
||||
LOG_PIN(" CS Pin: ", this->cs_pin_);
|
||||
|
||||
@@ -46,6 +46,7 @@ class EPaperT133A01 : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
|
||||
bool transfer_data() override;
|
||||
void abort_transfer() override;
|
||||
|
||||
/**
|
||||
* Send a command (and optional data) selecting one or both controllers.
|
||||
|
||||
@@ -37,6 +37,7 @@ class EPaperUC8179 final : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
|
||||
@@ -87,6 +87,38 @@ jd79660.extend(
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 2.13-G (V2 hardware/init revision, PCB marked "rev 2.1" or later)
|
||||
#
|
||||
# Vendor init derived from vendor sample code
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g_V2.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||
# Note: panel is 122px wide, but vendor code declares the TRES register as a fixed
|
||||
# 124 on this revision. width=124 is declared here (rather than 122) purely so the
|
||||
# transferred bytes-per-row matches what the vendor init tells the IC to expect; the
|
||||
# extra 2 columns fall outside the visible glass. The model width must equal the
|
||||
# TRES value in initsequence, so overriding dimensions: in YAML brings the shear back.
|
||||
#
|
||||
# fmt: off
|
||||
jd79660.extend(
|
||||
"Waveshare-2.13in-G",
|
||||
width=124,
|
||||
height=250,
|
||||
|
||||
initsequence=(
|
||||
(0x61, 0x00, 0x7C, 0x00, 0xFA,), # TRES: 124x250 fixed (124 = TRES stride value)
|
||||
(0xE9, 0x01,),
|
||||
# Power On (0x04): Must be early part of init seq = Disabled later!
|
||||
(0x04,),
|
||||
),
|
||||
fast_update=(
|
||||
(0xE0, 0x02,),
|
||||
(0xE6, 0x5A,),
|
||||
(0xA5,),
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 7.5-H
|
||||
#
|
||||
# Vendor init derived from vendor sample code
|
||||
@@ -130,7 +162,7 @@ jd79660.extend(
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle.
|
||||
#
|
||||
# fmt: off
|
||||
jd79660.extend(
|
||||
gdey0213f51 = jd79660.extend(
|
||||
"goodisplay-gdey0213f51-2.13",
|
||||
width=128,
|
||||
height=250,
|
||||
@@ -154,3 +186,13 @@ jd79660.extend(
|
||||
(0x04,),
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 2.13-G (V1 hardware/init revision, older stock): the same panel and init as the Good
|
||||
# Display entry above, byte for byte, so it only adds the name. Vendor sample:
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||
# Note: width=128 is the TRES value the init sequence programs, not the 122 visible columns; overriding
|
||||
# dimensions: in YAML shears the image. Not tested on V1 hardware.
|
||||
gdey0213f51.extend("Waveshare-2.13in-G-V1")
|
||||
|
||||
@@ -24,3 +24,11 @@ WaveshareB(
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="1s",
|
||||
)
|
||||
|
||||
WaveshareB(
|
||||
"waveshare-2.66in-b",
|
||||
width=152,
|
||||
height=296,
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="15s",
|
||||
)
|
||||
|
||||
@@ -2920,6 +2920,7 @@ async def _finalize_arduino_aware_flags():
|
||||
if not arduino_linked:
|
||||
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
|
||||
cg.add_define("USE_ESP32_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
elif not CORE.using_arduino:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
|
||||
@@ -3043,6 +3044,9 @@ async def to_code(config):
|
||||
"_ZSt25__throw_bad_function_callv",
|
||||
]:
|
||||
cg.add_build_flag(f"-Wl,--wrap={mangled}")
|
||||
# Replace operator new so nothrow new returns nullptr. See operator_new.cpp.
|
||||
# --undefined pulls it from libsrc.a before libstdc++ (_Znwj: 32 bit size_t on every ESP32).
|
||||
cg.add_build_flag("-Wl,--undefined=_Znwj")
|
||||
|
||||
# Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r
|
||||
# (~11 KB). See printf_stubs.cpp for implementation.
|
||||
|
||||
@@ -161,7 +161,7 @@ static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-c
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "esp32.crash");
|
||||
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG);
|
||||
|
||||
// RAM copy of the build timestamp. The generated constant lives in flash,
|
||||
// which the panic handler must not read (cache may be disabled during
|
||||
@@ -175,7 +175,8 @@ static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME)
|
||||
|
||||
// Validate the NOINIT record. Runs on every has_data() call; re-running is
|
||||
// harmless and the magic is left alone so the record survives an OTA
|
||||
// rollback reboot, crash_handler_clear() drops it once an API client has it.
|
||||
// rollback reboot, crash_handler_clear() drops it once a log client has it
|
||||
// (logger::log_crash_to_client(), for the API and MQTT).
|
||||
static void read_crash_data() {
|
||||
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
||||
s_crash_data_valid = true;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
/// The tag the crash record is logged under, so a client can check the level in force for it
|
||||
inline constexpr const char *CRASH_LOG_TAG = "esp32.crash";
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Replaces operator new for ESP-IDF builds without C++ exceptions. libstdc++'s nothrow forms call the
|
||||
* throwing form and catch std::bad_alloc, but the throw aborts here, so they abort instead of returning
|
||||
* nullptr. All forms stay in this file: --undefined=_Znwj pulls it in ahead of libstdc++.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include <sdkconfig.h>
|
||||
#ifndef CONFIG_COMPILER_CXX_EXCEPTIONS
|
||||
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
#include "esp_system.h"
|
||||
|
||||
#ifdef __cpp_exceptions
|
||||
#error "CONFIG_COMPILER_CXX_EXCEPTIONS no longer matches -fexceptions"
|
||||
#endif
|
||||
|
||||
// libstdc++'s aligned delete calls free() only when built with one of these
|
||||
#if !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC) && !defined(_GLIBCXX_HAVE_POSIX_MEMALIGN) && !defined(_GLIBCXX_HAVE_MEMALIGN)
|
||||
#error "libstdc++'s aligned operator delete no longer frees with free()"
|
||||
#endif
|
||||
|
||||
#ifndef CLANG_TIDY // clang-tidy parses Xtensa with a 64 bit host target
|
||||
// --undefined=_Znwj in __init__.py relies on this mangling
|
||||
static_assert(std::is_same_v<std::size_t, unsigned int>, "update --undefined=_Znwj");
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32 {} // namespace esphome::esp32
|
||||
|
||||
namespace {
|
||||
|
||||
// Standard new_handler retry loop; returns nullptr once no handler is installed
|
||||
template<typename Alloc> void *alloc_or_null(std::size_t size, Alloc alloc) {
|
||||
// malloc(0) may return NULL, but operator new must return a unique pointer
|
||||
if (size == 0)
|
||||
size = 1;
|
||||
for (;;) {
|
||||
void *ptr = alloc(size);
|
||||
if (ptr != nullptr)
|
||||
return ptr;
|
||||
std::new_handler handler = std::get_new_handler();
|
||||
if (handler == nullptr)
|
||||
return nullptr;
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
void *abort_if_null(void *ptr) {
|
||||
if (ptr == nullptr)
|
||||
esp_system_abort("std::bad_alloc");
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void *plain_alloc(std::size_t size) {
|
||||
return alloc_or_null(size, [](std::size_t n) { return std::malloc(n); }); // NOLINT(cppcoreguidelines-no-malloc)
|
||||
}
|
||||
|
||||
// IDF's aligned_alloc accepts any size (no C11 multiple of alignment rule) and pairs with free(),
|
||||
// which libstdc++'s aligned delete uses (checked above)
|
||||
void *aligned_alloc_or_null(std::size_t size, std::align_val_t align) {
|
||||
return alloc_or_null(size, [align](std::size_t n) { return ::aligned_alloc(static_cast<std::size_t>(align), n); });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// libstdc++'s operator delete (free) already matches these
|
||||
// NOLINTBEGIN(cert-dcl54-cpp,misc-new-delete-overloads)
|
||||
void *operator new(std::size_t size) { return abort_if_null(plain_alloc(size)); }
|
||||
void *operator new[](std::size_t size) { return abort_if_null(plain_alloc(size)); }
|
||||
void *operator new(std::size_t size, const std::nothrow_t & /*tag*/) noexcept { return plain_alloc(size); }
|
||||
void *operator new[](std::size_t size, const std::nothrow_t & /*tag*/) noexcept { return plain_alloc(size); }
|
||||
|
||||
void *operator new(std::size_t size, std::align_val_t align) {
|
||||
return abort_if_null(aligned_alloc_or_null(size, align));
|
||||
}
|
||||
void *operator new[](std::size_t size, std::align_val_t align) {
|
||||
return abort_if_null(aligned_alloc_or_null(size, align));
|
||||
}
|
||||
void *operator new(std::size_t size, std::align_val_t align, const std::nothrow_t & /*tag*/) noexcept {
|
||||
return aligned_alloc_or_null(size, align);
|
||||
}
|
||||
void *operator new[](std::size_t size, std::align_val_t align, const std::nothrow_t & /*tag*/) noexcept {
|
||||
return aligned_alloc_or_null(size, align);
|
||||
}
|
||||
// NOLINTEND(cert-dcl54-cpp,misc-new-delete-overloads)
|
||||
|
||||
#endif // CONFIG_COMPILER_CXX_EXCEPTIONS
|
||||
#endif // USE_ESP_IDF
|
||||
@@ -131,6 +131,7 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
// Components that need NVS check nvs_handle instead of initializing it again.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
#include <esp_bt.h>
|
||||
@@ -28,7 +29,6 @@ extern "C" {
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
|
||||
@@ -77,8 +77,9 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
|
||||
void ESP32BLE::setup() {
|
||||
global_ble = this;
|
||||
if (!ble_pre_setup_()) {
|
||||
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
|
||||
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
|
||||
if (global_preferences->nvs_handle == 0) {
|
||||
ESP_LOGE(TAG, "NVS unavailable");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -185,15 +186,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ESP32BLE::ble_pre_setup_() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_init_() {
|
||||
if (this->advertising_ != nullptr)
|
||||
|
||||
@@ -185,7 +185,6 @@ class ESP32BLE final : public Component {
|
||||
}
|
||||
this->ble_events_.get_and_reset_dropped_count();
|
||||
}
|
||||
bool ble_pre_setup_();
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_OTA
|
||||
|
||||
@@ -6,6 +6,8 @@ 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 (
|
||||
CONF_MINIMUM_CHIP_REVISION,
|
||||
KEY_ESP32,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
@@ -20,6 +22,8 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_RX_PIN,
|
||||
@@ -27,6 +31,7 @@ from esphome.const import (
|
||||
CONF_TX_PIN,
|
||||
CONF_TX_QUEUE_LEN,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@Sympatron"]
|
||||
@@ -42,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
|
||||
CAN_MODES = {
|
||||
"NORMAL": CanMode.CAN_MODE_NORMAL,
|
||||
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
|
||||
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
|
||||
}
|
||||
|
||||
# Currently the driver only supports a subset of the bit rates defined in canbus
|
||||
@@ -49,6 +55,7 @@ CAN_MODES = {
|
||||
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
|
||||
|
||||
CAN_SPEEDS_ESP32 = {
|
||||
"20KBPS": CanSpeed.CAN_20KBPS,
|
||||
"25KBPS": CanSpeed.CAN_25KBPS,
|
||||
"50KBPS": CanSpeed.CAN_50KBPS,
|
||||
"100KBPS": CanSpeed.CAN_100KBPS,
|
||||
@@ -114,6 +121,22 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS":
|
||||
esp32_config = fv.full_config.get()[KEY_ESP32]
|
||||
min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get(
|
||||
CONF_MINIMUM_CHIP_REVISION
|
||||
)
|
||||
if min_revision is None or min_revision < "2.0":
|
||||
raise cv.Invalid(
|
||||
"20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher",
|
||||
path=[CONF_BIT_RATE],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def get_default_tx_enqueue_timeout(bit_rate: str) -> int:
|
||||
bit_rate_numeric = canbus.get_rate(bit_rate)
|
||||
bits_per_packet = 140 # ~max CAN message length
|
||||
|
||||
@@ -33,6 +33,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
|
||||
case canbus::CAN_16KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS();
|
||||
return true;
|
||||
#endif
|
||||
#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) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200)
|
||||
case canbus::CAN_20KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS();
|
||||
return true;
|
||||
@@ -75,10 +79,13 @@ bool ESP32Can::setup_internal() {
|
||||
}
|
||||
|
||||
// Select TWAI mode based on configuration
|
||||
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
|
||||
|
||||
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
twai_mode = TWAI_MODE_LISTEN_ONLY;
|
||||
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
|
||||
} else if (this->mode_ == CAN_MODE_NO_ACK) {
|
||||
twai_mode = TWAI_MODE_NO_ACK;
|
||||
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
|
||||
}
|
||||
|
||||
twai_general_config_t g_config =
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
|
||||
enum CanMode : uint8_t {
|
||||
CAN_MODE_NORMAL = 0,
|
||||
CAN_MODE_LISTEN_ONLY = 1,
|
||||
CAN_MODE_NO_ACK = 2,
|
||||
};
|
||||
|
||||
class ESP32Can final : public canbus::Canbus {
|
||||
|
||||
@@ -42,6 +42,7 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include "esp_now_hosted_rpc.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -52,6 +53,8 @@ ESPHOME_LOG_TAG(TAG, "esp_now_hosted");
|
||||
// single esp-hosted RPC RX thread. Serializing requests keeps the shared
|
||||
// response slot race-free; a sequence number stops a late/stale response from
|
||||
// being mistaken for ours.
|
||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables): the shim is
|
||||
// C-ABI state shared between the main loop and the esp-hosted RX thread.
|
||||
SemaphoreHandle_t g_req_mutex = nullptr;
|
||||
SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
|
||||
bool g_setup_done = false; // set only after setup fully succeeds
|
||||
@@ -85,6 +88,7 @@ constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
|
||||
uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
|
||||
size_t g_peer_count = 0;
|
||||
portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// Caller must hold g_peer_lock.
|
||||
int peer_cache_find_locked(const uint8_t *mac) {
|
||||
@@ -231,12 +235,14 @@ esp_err_t ensure_setup() {
|
||||
g_resp_sem = xSemaphoreCreateBinary();
|
||||
if (g_req_mutex == nullptr || g_resp_sem == nullptr)
|
||||
return ESP_ERR_NO_MEM;
|
||||
esp_err_t err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK)
|
||||
esp_err_t err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK)
|
||||
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK)
|
||||
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
g_setup_done = true;
|
||||
return ESP_OK;
|
||||
@@ -422,10 +428,11 @@ esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len
|
||||
uint8_t hdr[sizeof(esp_now_hosted_send_req_t)];
|
||||
auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
|
||||
s->has_addr = peer_addr != nullptr ? 1 : 0;
|
||||
if (peer_addr != nullptr)
|
||||
if (peer_addr != nullptr) {
|
||||
memcpy(s->peer_addr, peer_addr, 6);
|
||||
else
|
||||
} else {
|
||||
memset(s->peer_addr, 0, 6);
|
||||
}
|
||||
s->data_len = static_cast<uint16_t>(len);
|
||||
// Fire-and-forget (wait=false): native esp_now_send returns once the frame is
|
||||
// queued, with the real TX result delivered later through the send callback.
|
||||
|
||||
@@ -53,10 +53,14 @@ from .const import (
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
PREFERENCES_RTC_EBOOT_WORDS,
|
||||
PREFERENCES_RTC_NORMAL_WORDS,
|
||||
THROW_STUBS_HEADER,
|
||||
check_preferences_fit,
|
||||
enable_serial,
|
||||
enable_serial1,
|
||||
esp8266_ns,
|
||||
preferences_flash_words,
|
||||
)
|
||||
from .gpio import PinInitialState, add_pin_initial_states_array
|
||||
|
||||
@@ -416,6 +420,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
cg.add_define("USE_ESP8266_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
|
||||
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
|
||||
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
|
||||
@@ -467,6 +472,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Other projects like Tasmota & ESPEasy also use this
|
||||
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
|
||||
|
||||
cg.add_define("ESP8266_PREFERENCES_RTC_NORMAL_WORDS", PREFERENCES_RTC_NORMAL_WORDS)
|
||||
cg.add_define("ESP8266_PREFERENCES_RTC_EBOOT_WORDS", PREFERENCES_RTC_EBOOT_WORDS)
|
||||
cg.add_define(
|
||||
"ESP8266_PREFERENCES_FLASH_WORDS",
|
||||
preferences_flash_words(config[CONF_RESTORE_FROM_FLASH]),
|
||||
)
|
||||
if config[CONF_RESTORE_FROM_FLASH]:
|
||||
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
|
||||
|
||||
@@ -528,6 +539,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(add_pin_initial_states_array)
|
||||
CORE.add_job(finalize_waveform_config)
|
||||
CORE.add_job(finalize_serial_config)
|
||||
CORE.add_job(check_preferences_fit)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
|
||||
# Re-exported from the shared definition; here it indexes the BOARDS
|
||||
# metadata dicts, whose entries in boards.py spell the literal.
|
||||
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
@@ -24,6 +28,87 @@ KEY_LDSCRIPT = "ldscript"
|
||||
# Crash-decode tools, resolved once per run by __init__
|
||||
KEY_DECODE_TOOLS = "decode_tools"
|
||||
|
||||
# Saved-settings space in 4-byte words; preferences.cpp gets these as defines. A value cannot span the two
|
||||
# RTC regions, and the flash store is smaller when only explicit flash preferences use it.
|
||||
PREFERENCES_RTC_NORMAL_WORDS = 78
|
||||
PREFERENCES_RTC_EBOOT_WORDS = 32
|
||||
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
|
||||
PREFERENCES_FLASH_WORDS = 128
|
||||
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
|
||||
KEY_PREFERENCES = "preferences"
|
||||
|
||||
|
||||
def preferences_flash_words(restore_from_flash: bool) -> int:
|
||||
"""Size of the flash store for this configuration."""
|
||||
return (
|
||||
PREFERENCES_FLASH_WORDS
|
||||
if restore_from_flash
|
||||
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
|
||||
)
|
||||
|
||||
|
||||
def preference_words(value_bytes: int) -> int:
|
||||
"""Words a saved value takes, checksum word included (rtc_pref_bytes_to_words plus one)."""
|
||||
return (value_bytes + 3) // 4 + 1
|
||||
|
||||
|
||||
def reserve_preferences(
|
||||
count: int, value_bytes: int, in_flash: bool | None = None
|
||||
) -> None:
|
||||
"""Record saved values a component creates; check_preferences_fit() adds them all up.
|
||||
|
||||
in_flash is the value's own storage, as passed to make_preference(); None is the two-argument
|
||||
form, which follows restore_from_flash.
|
||||
"""
|
||||
if in_flash is None:
|
||||
in_flash = CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
|
||||
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
|
||||
reservations.extend([(preference_words(value_bytes), in_flash)] * count)
|
||||
|
||||
|
||||
def _rtc_values_can_split(values: list[int]) -> bool:
|
||||
"""Whether some order fills the two RTC regions without a value spanning them."""
|
||||
total = sum(values)
|
||||
if total <= PREFERENCES_RTC_NORMAL_WORDS:
|
||||
return True
|
||||
# Bit i of reachable is set when some subset of the values sums to i
|
||||
reachable = 1
|
||||
for words in values:
|
||||
reachable |= reachable << words
|
||||
lowest = max(0, total - PREFERENCES_RTC_EBOOT_WORDS)
|
||||
highest = min(total, PREFERENCES_RTC_NORMAL_WORDS)
|
||||
return any(reachable >> normal & 1 for normal in range(lowest, highest + 1))
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def check_preferences_fit() -> None:
|
||||
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
|
||||
flash_store = preferences_flash_words(
|
||||
CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
|
||||
)
|
||||
reservations = CORE.data[KEY_ESP8266].get(KEY_PREFERENCES, [])
|
||||
flash_words = sum(w for w, in_flash in reservations if in_flash)
|
||||
rtc_values = [w for w, in_flash in reservations if not in_flash]
|
||||
if flash_words > flash_store:
|
||||
raise EsphomeError(
|
||||
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
|
||||
"use fewer or smaller saved values"
|
||||
)
|
||||
if not _rtc_values_can_split(rtc_values):
|
||||
raise EsphomeError(
|
||||
f"Saved values need {sum(rtc_values)} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS} in "
|
||||
f"regions of {PREFERENCES_RTC_NORMAL_WORDS} and {PREFERENCES_RTC_EBOOT_WORDS} that a value cannot "
|
||||
"span; use fewer or smaller saved values, or store them in flash"
|
||||
)
|
||||
if sum(rtc_values) > PREFERENCES_RTC_NORMAL_WORDS:
|
||||
_LOGGER.warning(
|
||||
"Saved values reserve %d of %d words of ESP8266 RTC memory; the fit depends on setup order, and "
|
||||
"saved settings that do not reserve are not counted",
|
||||
sum(rtc_values),
|
||||
PREFERENCES_RTC_WORDS,
|
||||
)
|
||||
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "rtc_layout.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -46,11 +47,8 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
|
||||
// Recover the actual code address from a windowed-ABI return address on the stack.
|
||||
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
|
||||
|
||||
// RTC user memory layout for crash backtrace data.
|
||||
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
|
||||
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
|
||||
// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
|
||||
// Store 16 raw candidates, filter to real return addresses at log time.
|
||||
static constexpr uint8_t RTC_CRASH_BASE = 174;
|
||||
static constexpr size_t MAX_BACKTRACE = 16;
|
||||
|
||||
// Magic word packs sentinel, version, and count into one uint32_t:
|
||||
@@ -78,7 +76,7 @@ struct RtcCrashData {
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint32_t epc1; // Fault PC, used to filter duplicates
|
||||
};
|
||||
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
|
||||
static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
@@ -129,7 +127,7 @@ static const LogString *get_reset_reason(uint32_t reason) {
|
||||
// Returns the number of valid backtrace entries (0 if no data found).
|
||||
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
|
||||
RtcCrashData rtc_data;
|
||||
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
return 0;
|
||||
uint32_t magic = rtc_data.magic;
|
||||
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
|
||||
@@ -233,7 +231,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3
|
||||
data.epc1 = epc1;
|
||||
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
|
||||
|
||||
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
|
||||
system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
|
||||
}
|
||||
|
||||
#endif // USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
@@ -6,6 +6,7 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include "preferences.h"
|
||||
#include "rtc_layout.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences_rtc.h"
|
||||
@@ -17,31 +18,26 @@ namespace esphome::esp8266 {
|
||||
ESPHOME_LOG_TAG(TAG, "preferences");
|
||||
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = RTC_USER_MEM_WORDS;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
||||
|
||||
// RTC memory layout:
|
||||
// RTC memory layout (sizes come from const.py through codegen):
|
||||
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
|
||||
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
|
||||
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = ESP8266_PREFERENCES_RTC_EBOOT_WORDS;
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = ESP8266_PREFERENCES_RTC_NORMAL_WORDS;
|
||||
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
|
||||
static_assert(PREF_TOTAL_WORDS <= RTC_CRASH_BASE - RTC_SYSTEM_USER_BASE,
|
||||
"preference regions overlap the crash handler's RTC words");
|
||||
|
||||
// Maximum preference size in words (limited by uint8_t length_words field)
|
||||
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
||||
|
||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||
|
||||
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||
#else
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
|
||||
#endif
|
||||
// Smaller without restore_from_flash, when only explicit flash preferences live here
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = ESP8266_PREFERENCES_FLASH_WORDS;
|
||||
|
||||
static uint32_t
|
||||
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -60,7 +56,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
|
||||
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
|
||||
return false;
|
||||
}
|
||||
if (index < 32 && s_prevent_write) {
|
||||
if (index < RTC_EBOOT_REGION_WORDS && s_prevent_write) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,7 +119,7 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
||||
|
||||
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (78) - a single pref can't span both RTC regions.
|
||||
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
@@ -180,8 +176,8 @@ ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t
|
||||
} else {
|
||||
uint32_t start = this->current_offset;
|
||||
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
|
||||
// Normal: offset 0-95 maps to RTC offset 32-127
|
||||
// Eboot: offset 96-127 maps to RTC offset 0-31
|
||||
// Normal: offset 0-77 maps to RTC offset 32-109
|
||||
// Eboot: offset 78-109 maps to RTC offset 0-31
|
||||
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
|
||||
// start is in normal but end is not -> switch to Eboot
|
||||
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
// User RTC memory is system blocks 64-191, one 4-byte word each. preferences.cpp owns the first
|
||||
// words, crash_handler.cpp the last; const.py hands preferences.cpp its region sizes.
|
||||
static constexpr uint32_t RTC_SYSTEM_USER_BASE = 64;
|
||||
static constexpr uint32_t RTC_USER_MEM_WORDS = 128;
|
||||
static constexpr uint32_t RTC_CRASH_WORDS = 18;
|
||||
static constexpr uint32_t RTC_CRASH_BASE = RTC_SYSTEM_USER_BASE + RTC_USER_MEM_WORDS - RTC_CRASH_WORDS;
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -3,11 +3,12 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <deque>
|
||||
|
||||
namespace esphome::ezo {
|
||||
|
||||
static const char *const TAG = "ezo.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "ezo.sensor");
|
||||
|
||||
enum EzoCommandType : uint8_t {
|
||||
EZO_READ = 0,
|
||||
|
||||
@@ -330,7 +330,9 @@ async def write_image(
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
pixels = encoder.convert(
|
||||
image.resize((width, height)), path
|
||||
).get_flattened_data()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@remcom"]
|
||||
DOMAIN = "fusb302b"
|
||||
@@ -0,0 +1,330 @@
|
||||
#include "pd.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "fusb302b.pd");
|
||||
|
||||
static constexpr uint32_t AMS_TIMEOUT_MS = 2000;
|
||||
// tSinkRequest: delay before repeating a request that the source answered with Wait
|
||||
static constexpr uint32_t SINK_REQUEST_MS = 100;
|
||||
static constexpr uint8_t MAX_WAIT_RETRIES = 10;
|
||||
|
||||
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object flags; only the first (5 V) object carries them
|
||||
static constexpr uint32_t SINK_PDO_HIGHER_CAPABILITY = 1u << 28;
|
||||
static constexpr uint32_t SINK_PDO_USB_COMMS_CAPABLE = 1u << 26;
|
||||
// Operational current advertised for every sink object: 3 A (300 x 10 mA)
|
||||
static constexpr uint16_t SINK_PDO_CURRENT = 300;
|
||||
|
||||
// USB PD 6.4.1.2.3 Source Fixed Supply Power Data Object; other PDO types only report their type
|
||||
static PdContract parse_pdo(uint32_t pdo) {
|
||||
PdContract info{};
|
||||
info.type = static_cast<PdPdoType>(pdo >> 30);
|
||||
if (info.type == PD_PDO_TYPE_FIXED_SUPPLY) {
|
||||
info.max_v = (pdo >> 10) & 0x3FF;
|
||||
info.max_i = pdo & 0x3FF;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object; voltage in 50 mV units, current in 10 mA units
|
||||
static constexpr uint32_t sink_fixed_pdo(uint16_t voltage, uint16_t current) {
|
||||
return (static_cast<uint32_t>(PD_PDO_TYPE_FIXED_SUPPLY) << 30) | (static_cast<uint32_t>(voltage & 0x3FF) << 10) |
|
||||
(current & 0x3FF);
|
||||
}
|
||||
|
||||
void PdMsg::set_header(uint16_t header) {
|
||||
this->type = header & 0x1F;
|
||||
this->spec_rev = static_cast<PdSpecRevision>((header >> 6) & 0x3);
|
||||
this->id = (header >> 9) & 0x7;
|
||||
this->num_of_obj = (header >> 12) & 0x7;
|
||||
this->extended = (header >> 15) & 0x1;
|
||||
}
|
||||
|
||||
uint16_t PdMsg::get_coded_header() const {
|
||||
// Data role UFP (bit 5 = 0), power role sink (bit 8 = 0)
|
||||
return static_cast<uint16_t>(this->type & 0x1F) | (static_cast<uint16_t>(this->spec_rev) << 6) |
|
||||
(static_cast<uint16_t>(this->id & 0x7) << 9) | (static_cast<uint16_t>(this->num_of_obj & 0x7) << 12) |
|
||||
(static_cast<uint16_t>(this->extended) << 15);
|
||||
}
|
||||
|
||||
PdMsg PowerDelivery::make_control_msg_(PdControlMsgType type) const {
|
||||
PdMsg msg;
|
||||
msg.type = type;
|
||||
msg.id = this->msg_counter_ % 8;
|
||||
return msg;
|
||||
}
|
||||
|
||||
PdMsg PowerDelivery::make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const {
|
||||
PdMsg msg;
|
||||
msg.type = type;
|
||||
msg.id = this->msg_counter_ % 8;
|
||||
msg.num_of_obj = len;
|
||||
memcpy(msg.data_objects, objects, len * sizeof(uint32_t));
|
||||
return msg;
|
||||
}
|
||||
|
||||
void PowerDelivery::reset_protocol_() {
|
||||
this->last_received_msg_id_ = 255;
|
||||
this->msg_counter_ = 0;
|
||||
this->active_ams_ = false;
|
||||
this->ps_rdy_expected_ = false;
|
||||
this->retry_request_pending_ = false;
|
||||
this->wait_retries_ = 0;
|
||||
}
|
||||
|
||||
void PowerDelivery::set_state_(PdState state) {
|
||||
if (this->state_.exchange(state) != state)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::set_contract_(const PdContract &contract) {
|
||||
this->accepted_contract_ = contract;
|
||||
const uint32_t packed = PackedContract::from(contract).value;
|
||||
if (this->contract_.exchange(packed) != packed)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::clear_contract_() {
|
||||
this->accepted_contract_ = {};
|
||||
if (this->contract_.exchange(0) != 0)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::set_state_and_contract_(PdState state, const PdContract &contract) {
|
||||
this->accepted_contract_ = contract;
|
||||
const uint32_t packed = PackedContract::from(contract).value;
|
||||
// The contract is stored first, so a reader that sees the new state also sees the new contract
|
||||
const bool contract_changed = this->contract_.exchange(packed) != packed;
|
||||
const bool state_changed = this->state_.exchange(state) != state;
|
||||
if (contract_changed || state_changed)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::set_ams_(bool active) {
|
||||
this->active_ams_ = active;
|
||||
// A new sequence has no accepted request yet, and an ended one can no longer complete
|
||||
this->ps_rdy_expected_ = false;
|
||||
if (active)
|
||||
this->ams_start_ = millis();
|
||||
}
|
||||
|
||||
bool PowerDelivery::check_ams_() {
|
||||
if (this->active_ams_ && millis() - this->ams_start_ > AMS_TIMEOUT_MS)
|
||||
this->set_ams_(false);
|
||||
return this->active_ams_;
|
||||
}
|
||||
|
||||
void PowerDelivery::check_request_retry_() {
|
||||
if (!this->retry_request_pending_ || millis() - this->wait_received_ms_ < SINK_REQUEST_MS)
|
||||
return;
|
||||
if (this->send_request_()) {
|
||||
this->retry_request_pending_ = false;
|
||||
} else if (this->wait_retries_ < MAX_WAIT_RETRIES) {
|
||||
// Try again after another tSinkRequest; failed attempts count against the same limit as Wait replies
|
||||
this->wait_retries_++;
|
||||
this->wait_received_ms_ = millis();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Giving up repeating the request");
|
||||
this->retry_request_pending_ = false;
|
||||
this->request_failed_();
|
||||
}
|
||||
}
|
||||
|
||||
bool PowerDelivery::send_request_() {
|
||||
this->set_ams_(true);
|
||||
if (this->send_message(this->make_data_msg_(PD_DATA_REQUEST, &this->last_rdo_, 1)))
|
||||
return true;
|
||||
ESP_LOGW(TAG, "Sending the request failed");
|
||||
this->set_ams_(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
void PowerDelivery::request_failed_() {
|
||||
// A contract already in place stays valid. Without one, report that negotiation is over at default power.
|
||||
if (this->state_ == PdState::PD_STATE_DEFAULT_CONTRACT)
|
||||
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
|
||||
}
|
||||
|
||||
void PowerDelivery::check_transition_timeout_() {
|
||||
// The AMS that sent the request only ends on PS_RDY, so an ended AMS while in transition means a timeout
|
||||
if (this->state_ != PdState::PD_STATE_TRANSITION || this->check_ams_())
|
||||
return;
|
||||
ESP_LOGW(TAG, "Source did not report the new supply ready, falling back to default power");
|
||||
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
|
||||
this->on_transition_timeout();
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_transmit_failed_() {
|
||||
// USB PD 6.12.2.2: the counter also moves on after a failed transmission. The source may have received the
|
||||
// message and only its GoodCRC was lost, so reusing the MessageID would make it drop the next one as a repeat.
|
||||
ESP_LOGD(TAG, "Message %u not acknowledged", this->msg_counter_ % 8);
|
||||
this->msg_counter_++;
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_message_(const PdMsg &msg) {
|
||||
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_GOODCRC) {
|
||||
// A GoodCRC carries the MessageID it acknowledges; a duplicate or stray one must not advance the counter
|
||||
if (msg.id != this->msg_counter_ % 8) {
|
||||
ESP_LOGD(TAG, "Ignoring GoodCRC for MessageID %u, expected %u", msg.id, this->msg_counter_ % 8);
|
||||
return;
|
||||
}
|
||||
// Our last message was received, so the next one gets a new MessageID
|
||||
this->msg_counter_++;
|
||||
return;
|
||||
}
|
||||
// Soft_Reset always has MessageID 0, so it must not be dropped as a retransmission of the last message
|
||||
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_SOFT_RESET) {
|
||||
this->handle_soft_reset_(msg);
|
||||
return;
|
||||
}
|
||||
// Retransmissions repeat the MessageID of a message that was already handled
|
||||
if (msg.id == this->last_received_msg_id_)
|
||||
return;
|
||||
this->last_received_msg_id_ = msg.id;
|
||||
|
||||
if (msg.num_of_obj == 0) {
|
||||
this->handle_control_message_(msg);
|
||||
} else {
|
||||
this->handle_data_message_(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_data_message_(const PdMsg &msg) {
|
||||
if (msg.extended || msg.type != PD_DATA_SOURCE_CAP) {
|
||||
// BIST and Vendor_Defined messages must not be answered with Reject, so unsupported data messages are ignored
|
||||
ESP_LOGV(TAG, "Ignoring data message type %u%s", msg.type,
|
||||
msg.extended ? LOG_STR_LITERAL(" (extended)") : LOG_STR_LITERAL(""));
|
||||
return;
|
||||
}
|
||||
this->on_source_caps_received();
|
||||
this->respond_to_source_caps_(msg);
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_control_message_(const PdMsg &msg) {
|
||||
switch (msg.type) {
|
||||
case PD_CNTRL_ACCEPT:
|
||||
if (!this->check_ams_()) {
|
||||
ESP_LOGD(TAG, "Ignoring Accept outside a request");
|
||||
break;
|
||||
}
|
||||
this->ps_rdy_expected_ = true;
|
||||
if (this->requested_contract_ != this->accepted_contract_)
|
||||
this->set_state_and_contract_(PdState::PD_STATE_TRANSITION, this->requested_contract_);
|
||||
break;
|
||||
case PD_CNTRL_PS_RDY:
|
||||
// check_ams_() first, so a PS_RDY that arrives after the sequence timed out is ignored too
|
||||
if (!this->check_ams_() || !this->ps_rdy_expected_) {
|
||||
ESP_LOGD(TAG, "Ignoring PS_RDY without an accepted request");
|
||||
break;
|
||||
}
|
||||
this->set_ams_(false);
|
||||
this->set_state_(PdState::PD_STATE_EXPLICIT_CONTRACT);
|
||||
break;
|
||||
case PD_CNTRL_REJECT:
|
||||
if (!this->check_ams_()) {
|
||||
ESP_LOGD(TAG, "Ignoring Reject outside a request");
|
||||
break;
|
||||
}
|
||||
ESP_LOGW(TAG, "Source rejected the request");
|
||||
this->set_ams_(false);
|
||||
this->request_failed_();
|
||||
break;
|
||||
case PD_CNTRL_WAIT:
|
||||
if (!this->check_ams_()) {
|
||||
ESP_LOGD(TAG, "Ignoring Wait outside a request");
|
||||
break;
|
||||
}
|
||||
this->set_ams_(false);
|
||||
if (this->wait_retries_ < MAX_WAIT_RETRIES) {
|
||||
ESP_LOGD(TAG, "Source asked to wait, repeating the request");
|
||||
this->wait_retries_++;
|
||||
this->retry_request_pending_ = true;
|
||||
this->wait_received_ms_ = millis();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Source kept answering Wait, giving up");
|
||||
this->request_failed_();
|
||||
}
|
||||
break;
|
||||
case PD_CNTRL_PING:
|
||||
case PD_CNTRL_NOT_SUPPORTED:
|
||||
break;
|
||||
case PD_CNTRL_GET_SINK_CAP: {
|
||||
// 5 V first, then the requested voltage. Higher Capability tells the source that 5 V is not enough for
|
||||
// full operation.
|
||||
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
|
||||
uint32_t pdos[2];
|
||||
uint8_t count = 0;
|
||||
pdos[count++] = sink_fixed_pdo(100, SINK_PDO_CURRENT) | SINK_PDO_USB_COMMS_CAPABLE |
|
||||
(request_v > 100 ? SINK_PDO_HIGHER_CAPABILITY : 0);
|
||||
if (request_v > 100)
|
||||
pdos[count++] = sink_fixed_pdo(request_v, SINK_PDO_CURRENT);
|
||||
this->send_message(this->make_data_msg_(PD_DATA_SINK_CAP, pdos, count));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Messages are sent as PD 2.0, which has no Not_Supported message
|
||||
this->send_message(this->make_control_msg_(PD_CNTRL_REJECT));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_soft_reset_(const PdMsg &msg) {
|
||||
this->reset_protocol_();
|
||||
this->last_received_msg_id_ = msg.id;
|
||||
// If this fails the source escalates to a hard reset. The hook still runs, so the PHY starts waiting for source
|
||||
// capabilities and recovers on its own if neither follows.
|
||||
if (!this->send_message(this->make_control_msg_(PD_CNTRL_ACCEPT))) {
|
||||
ESP_LOGW(TAG, "Accepting the soft reset failed");
|
||||
}
|
||||
// A soft reset keeps the power level, but the outcome of a transition in progress is unknown
|
||||
if (this->state_ == PdState::PD_STATE_TRANSITION)
|
||||
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
|
||||
this->on_soft_reset_received();
|
||||
}
|
||||
|
||||
void PowerDelivery::respond_to_source_caps_(const PdMsg &msg) {
|
||||
// Pick the highest fixed supply PDO that does not exceed the requested voltage.
|
||||
// The first PDO is always the 5 V fixed supply.
|
||||
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
|
||||
PdContract selected{};
|
||||
uint8_t position = 0; // 1-based object position, 0 = none
|
||||
for (uint8_t idx = 0; idx < msg.num_of_obj; idx++) {
|
||||
PdContract info = parse_pdo(msg.data_objects[idx]);
|
||||
if (info.type != PD_PDO_TYPE_FIXED_SUPPLY)
|
||||
continue;
|
||||
if (position == 0 || (info.max_v <= request_v && info.max_v > selected.max_v)) {
|
||||
selected = info;
|
||||
position = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// USB PD 6.4.2 Request Data Object: no USB suspend (bit 24), USB communications capable (bit 25)
|
||||
uint32_t rdo = (1u << 24) | (1u << 25);
|
||||
if (position == 0) {
|
||||
ESP_LOGW(TAG, "No usable PDO in source capabilities, requesting 5 V");
|
||||
// Object 1 (5 V fixed supply), 300 mA maximum, 100 mA operating current
|
||||
selected = PdContract{PD_PDO_TYPE_FIXED_SUPPLY, 100, 30};
|
||||
position = 1;
|
||||
rdo |= (30u << 0) | (10u << 10);
|
||||
} else {
|
||||
// Request the full current the source offers
|
||||
const uint32_t current = selected.max_i;
|
||||
rdo |= (current << 0) | (current << 10);
|
||||
}
|
||||
rdo |= static_cast<uint32_t>(position) << 28;
|
||||
ESP_LOGD(TAG, "Requesting PDO %u: %.2f V", position, selected.max_v * 0.05f);
|
||||
this->requested_contract_ = selected;
|
||||
this->last_rdo_ = rdo;
|
||||
this->retry_request_pending_ = false;
|
||||
this->wait_retries_ = 0;
|
||||
// A repeat would come too late: the source resets if it gets no Request within tSenderResponse (24-30 ms)
|
||||
if (!this->send_request_())
|
||||
this->request_failed_();
|
||||
}
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
static constexpr uint8_t PD_MAX_NUM_DATA_OBJECTS = 7;
|
||||
// Standard Power Range fixed supplies, which are the only ones this sink negotiates
|
||||
static constexpr uint8_t MIN_REQUEST_VOLTAGE = 5;
|
||||
static constexpr uint8_t MAX_REQUEST_VOLTAGE = 20;
|
||||
|
||||
enum PdSpecRevision : uint8_t {
|
||||
PD_SPEC_REV_1 = 0,
|
||||
PD_SPEC_REV_2 = 1,
|
||||
PD_SPEC_REV_3 = 2,
|
||||
};
|
||||
|
||||
enum PdDataMsgType : uint8_t {
|
||||
PD_DATA_SOURCE_CAP = 0x01,
|
||||
PD_DATA_REQUEST = 0x02,
|
||||
PD_DATA_SINK_CAP = 0x04,
|
||||
};
|
||||
|
||||
enum PdControlMsgType : uint8_t {
|
||||
PD_CNTRL_GOODCRC = 0x01,
|
||||
PD_CNTRL_ACCEPT = 0x03,
|
||||
PD_CNTRL_REJECT = 0x04,
|
||||
PD_CNTRL_PING = 0x05,
|
||||
PD_CNTRL_PS_RDY = 0x06,
|
||||
PD_CNTRL_GET_SINK_CAP = 0x08,
|
||||
PD_CNTRL_WAIT = 0x0C,
|
||||
PD_CNTRL_SOFT_RESET = 0x0D,
|
||||
PD_CNTRL_NOT_SUPPORTED = 0x10,
|
||||
};
|
||||
|
||||
enum PdPdoType : uint8_t {
|
||||
PD_PDO_TYPE_FIXED_SUPPLY = 0,
|
||||
PD_PDO_TYPE_BATTERY = 1,
|
||||
PD_PDO_TYPE_VARIABLE_SUPPLY = 2,
|
||||
PD_PDO_TYPE_AUGMENTED = 3,
|
||||
};
|
||||
|
||||
enum class PdState : uint8_t {
|
||||
PD_STATE_DISCONNECTED,
|
||||
/// Negotiation is over without a PD contract (no capabilities, or the request was not accepted); the source
|
||||
/// supplies Type-C default power.
|
||||
PD_STATE_PD_TIMEOUT,
|
||||
PD_STATE_DEFAULT_CONTRACT,
|
||||
PD_STATE_TRANSITION,
|
||||
PD_STATE_EXPLICIT_CONTRACT,
|
||||
PD_STATE_ERROR,
|
||||
};
|
||||
|
||||
/// Returns true for the states in which a source is attached and supplying power.
|
||||
inline bool is_connected_state(PdState state) {
|
||||
return state == PdState::PD_STATE_DEFAULT_CONTRACT || state == PdState::PD_STATE_TRANSITION ||
|
||||
state == PdState::PD_STATE_EXPLICIT_CONTRACT || state == PdState::PD_STATE_PD_TIMEOUT;
|
||||
}
|
||||
|
||||
struct PdContract {
|
||||
PdPdoType type{PD_PDO_TYPE_FIXED_SUPPLY};
|
||||
uint16_t max_v{0}; // 50 mV units
|
||||
uint16_t max_i{0}; // 10 mA units
|
||||
|
||||
bool operator==(const PdContract &other) const {
|
||||
return this->max_v == other.max_v && this->max_i == other.max_i && this->type == other.type;
|
||||
}
|
||||
bool operator!=(const PdContract &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
// Type-C default power: 5 V (100 x 50 mV) at 500 mA (50 x 10 mA)
|
||||
static constexpr PdContract DEFAULT_CONTRACT{PD_PDO_TYPE_FIXED_SUPPLY, 100, 50};
|
||||
|
||||
/// A contract packed into one word so it can be shared between tasks as an atomic.
|
||||
/// 0 means there is no contract (nothing attached).
|
||||
struct PackedContract {
|
||||
uint32_t value{0};
|
||||
|
||||
static PackedContract from(const PdContract &contract) {
|
||||
return {(static_cast<uint32_t>(contract.max_v) << 16) | contract.max_i};
|
||||
}
|
||||
bool is_set() const { return this->value != 0; }
|
||||
float voltage() const { return (this->value >> 16) * 0.05f; }
|
||||
float current() const { return (this->value & 0xFFFF) * 0.01f; }
|
||||
};
|
||||
|
||||
struct PdMsg {
|
||||
uint8_t type{0};
|
||||
PdSpecRevision spec_rev{PD_SPEC_REV_2};
|
||||
uint8_t id{0};
|
||||
uint8_t num_of_obj{0};
|
||||
bool extended{false};
|
||||
uint32_t data_objects[PD_MAX_NUM_DATA_OBJECTS]{};
|
||||
|
||||
void set_header(uint16_t header);
|
||||
uint16_t get_coded_header() const;
|
||||
};
|
||||
|
||||
/// USB-PD sink protocol logic, independent of the PHY chip.
|
||||
/// Everything except the getters and set_request_voltage() runs in the PHY task, which must also call
|
||||
/// check_transition_timeout_() and check_request_retry_() regularly; the protocol has no timers of its own.
|
||||
class PowerDelivery {
|
||||
public:
|
||||
void set_request_voltage(uint8_t voltage) {
|
||||
this->request_voltage_ = std::clamp(voltage, MIN_REQUEST_VOLTAGE, MAX_REQUEST_VOLTAGE);
|
||||
}
|
||||
uint8_t get_request_voltage() const { return this->request_voltage_; }
|
||||
|
||||
PdState get_state() const { return this->state_; }
|
||||
bool is_connected() const { return is_connected_state(this->state_); }
|
||||
|
||||
protected:
|
||||
/// Returns false when the message could not be handed to the chip; the PHY logs the failure.
|
||||
virtual bool send_message(const PdMsg &msg) = 0;
|
||||
/// Called after state_ or contract_ changed, to let the main loop publish it.
|
||||
virtual void on_state_changed() = 0;
|
||||
/// Called after the source soft reset the protocol; it sends new source capabilities next.
|
||||
virtual void on_soft_reset_received() = 0;
|
||||
/// Called when source capabilities arrive, before the request for them is sent.
|
||||
virtual void on_source_caps_received() = 0;
|
||||
/// Called after an accepted request was not followed by PS_RDY in time and the state fell back to default power;
|
||||
/// the PHY sends a soft reset so the source offers its capabilities again.
|
||||
virtual void on_transition_timeout() = 0;
|
||||
|
||||
void handle_message_(const PdMsg &msg);
|
||||
/// Called by the PHY when the chip gave up on a message after its retries without receiving a GoodCRC.
|
||||
void handle_transmit_failed_();
|
||||
void reset_protocol_();
|
||||
void set_state_(PdState state);
|
||||
void set_contract_(const PdContract &contract);
|
||||
void clear_contract_();
|
||||
/// Stores both before calling on_state_changed() once, so the main loop never sees the new state with the old
|
||||
/// contract.
|
||||
void set_state_and_contract_(PdState state, const PdContract &contract);
|
||||
void set_ams_(bool active);
|
||||
/// Returns true while an atomic message sequence is in progress; ends it after a timeout.
|
||||
bool check_ams_();
|
||||
/// Falls back to default power when the source accepted a request but did not report the new supply ready in time.
|
||||
void check_transition_timeout_();
|
||||
/// Sends the last request again once tSinkRequest has passed after the source answered Wait.
|
||||
void check_request_retry_();
|
||||
|
||||
PdMsg make_control_msg_(PdControlMsgType type) const;
|
||||
PdMsg make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const;
|
||||
|
||||
// Shared with the main loop
|
||||
std::atomic<PdState> state_{PdState::PD_STATE_DISCONNECTED};
|
||||
std::atomic<uint32_t> contract_{0};
|
||||
std::atomic<uint8_t> request_voltage_{5};
|
||||
|
||||
// PHY task only
|
||||
uint32_t ams_start_{0};
|
||||
uint32_t wait_received_ms_{0};
|
||||
bool retry_request_pending_{false};
|
||||
|
||||
private:
|
||||
void handle_data_message_(const PdMsg &msg);
|
||||
void handle_control_message_(const PdMsg &msg);
|
||||
void handle_soft_reset_(const PdMsg &msg);
|
||||
void respond_to_source_caps_(const PdMsg &msg);
|
||||
bool send_request_();
|
||||
void request_failed_();
|
||||
|
||||
PdContract requested_contract_{};
|
||||
PdContract accepted_contract_{};
|
||||
uint32_t last_rdo_{0};
|
||||
bool active_ams_{false};
|
||||
// Set by Accept; PS_RDY is only valid after the source accepted the request of the current sequence
|
||||
bool ps_rdy_expected_{false};
|
||||
uint8_t last_received_msg_id_{255};
|
||||
uint8_t msg_counter_{0};
|
||||
uint8_t wait_retries_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
@@ -2,7 +2,10 @@
|
||||
#include <string>
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "hon_climate.h"
|
||||
#include "hon_packet.h"
|
||||
|
||||
@@ -11,6 +14,26 @@ using namespace esphome::uart;
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
static bool vertical_axis_is_auto(climate::ClimateSwingMode swing_mode) {
|
||||
return (swing_mode == climate::CLIMATE_SWING_VERTICAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
|
||||
static bool horizontal_axis_is_auto(climate::ClimateSwingMode swing_mode) {
|
||||
return (swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
|
||||
static bool is_auto(hon_protocol::VerticalSwingMode mode) {
|
||||
return (mode == hon_protocol::VerticalSwingMode::AUTO) || (mode == hon_protocol::VerticalSwingMode::AUTO_SPECIAL);
|
||||
}
|
||||
|
||||
static bool is_auto(hon_protocol::HorizontalSwingMode mode) { return mode == hon_protocol::HorizontalSwingMode::AUTO; }
|
||||
|
||||
// A position the user just set wins over the appliance's echo of the old one for this long
|
||||
constexpr uint32_t AIRFLOW_SELECT_DEBOUNCE_MS = 10000;
|
||||
static bool in_debounce(uint32_t set_time, uint32_t now) {
|
||||
return set_time != 0 && now - set_time < AIRFLOW_SELECT_DEBOUNCE_MS;
|
||||
}
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "haier.climate");
|
||||
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
|
||||
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
|
||||
@@ -20,6 +43,27 @@ constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
|
||||
const uint8_t ONE_BUF[] = {0x00, 0x01};
|
||||
const uint8_t ZERO_BUF[] = {0x00, 0x00};
|
||||
|
||||
// Alarm texts indexed by alarm code; the last entry is the fallback for unknown codes
|
||||
PROGMEM_STRING_TABLE(
|
||||
HonAlarmMessages, "Outdoor module failure", "Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure", "Outdoor EEPROM abnormality", "Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure", "Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit", "Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality", "Full water protection", "Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure", "CBD and module communication failure", "Indoor DC fan failure",
|
||||
"Outdoor DC fan failure", "Door switch failure", "Dust filter needs cleaning reminder", "Water shortage protection",
|
||||
"Humidity sensor failure", "Indoor temperature sensor failure", "Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure", "Outdoor PM2.5 sensor failure", "Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection", "Outdoor compressor operation abnormality", "Outdoor DC current protection",
|
||||
"Outdoor no-load failure", "CT current abnormality", "Indoor cooling freeze protection",
|
||||
"High and low pressure protection", "Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure", "Outdoor cooling overload", "Water pump drainage failure",
|
||||
"Three-phase power supply failure", "Four-way valve failure", "External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm", "Different mode operation failure", "Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure", "Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure", "50Hz zero crossing failure", "Outdoor unit failure",
|
||||
"Formaldehyde sensor failure", "VOC sensor failure", "CO2 sensor failure", "Firewall failure", "Unknown");
|
||||
|
||||
HonClimate::HonClimate()
|
||||
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
@@ -67,21 +111,60 @@ bool HonClimate::get_quiet_mode_state() const {
|
||||
}
|
||||
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
|
||||
if (!this->current_vertical_swing_.has_value()) {
|
||||
return this->current_vertical_swing_;
|
||||
}
|
||||
// While the appliance swings, or right after the user chose a position, report the chosen position
|
||||
if (is_auto(this->current_vertical_swing_.value()) ||
|
||||
in_debounce(this->vertical_direction_set_time_, App.get_loop_component_start_time())) {
|
||||
return this->settings_.last_vertiacal_swing;
|
||||
}
|
||||
return this->current_vertical_swing_;
|
||||
};
|
||||
}
|
||||
|
||||
void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) {
|
||||
if (is_auto(direction)) {
|
||||
ESP_LOGW(TAG, "Vertical AUTO mode is not allowed for airflow position selector");
|
||||
return;
|
||||
}
|
||||
if (this->settings_.last_vertiacal_swing != direction) {
|
||||
this->settings_.last_vertiacal_swing = direction;
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
this->pending_vertical_direction_ = direction;
|
||||
this->force_send_control_ = true;
|
||||
this->vertical_direction_set_time_ = App.get_loop_component_start_time();
|
||||
#ifdef USE_SELECT
|
||||
this->publish_airflow_selects_();
|
||||
#endif
|
||||
}
|
||||
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> HonClimate::get_horizontal_airflow() const {
|
||||
if (!this->current_horizontal_swing_.has_value()) {
|
||||
return this->current_horizontal_swing_;
|
||||
}
|
||||
if (is_auto(this->current_horizontal_swing_.value()) ||
|
||||
in_debounce(this->horizontal_direction_set_time_, App.get_loop_component_start_time())) {
|
||||
return this->settings_.last_horizontal_swing;
|
||||
}
|
||||
return this->current_horizontal_swing_;
|
||||
}
|
||||
|
||||
void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) {
|
||||
if (is_auto(direction)) {
|
||||
ESP_LOGW(TAG, "Horizontal AUTO mode is not allowed for airflow position selector");
|
||||
return;
|
||||
}
|
||||
if (this->settings_.last_horizontal_swing != direction) {
|
||||
this->settings_.last_horizontal_swing = direction;
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
this->pending_horizontal_direction_ = direction;
|
||||
this->force_send_control_ = true;
|
||||
this->horizontal_direction_set_time_ = App.get_loop_component_start_time();
|
||||
#ifdef USE_SELECT
|
||||
this->publish_airflow_selects_();
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *HonClimate::get_cleaning_status_text() const {
|
||||
@@ -511,6 +594,18 @@ void HonClimate::initialization() {
|
||||
} else {
|
||||
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
|
||||
}
|
||||
bool settings_fixed = false;
|
||||
if (is_auto(this->settings_.last_vertiacal_swing)) {
|
||||
this->settings_.last_vertiacal_swing = hon_protocol::VerticalSwingMode::CENTER;
|
||||
settings_fixed = true;
|
||||
}
|
||||
if (is_auto(this->settings_.last_horizontal_swing)) {
|
||||
this->settings_.last_horizontal_swing = hon_protocol::HorizontalSwingMode::CENTER;
|
||||
settings_fixed = true;
|
||||
}
|
||||
if (settings_fixed) {
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
|
||||
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
|
||||
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
@@ -646,12 +741,21 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
}
|
||||
}
|
||||
}
|
||||
const ClimateSwingMode target_swing_mode = this->current_hvac_settings_.swing_mode.value_or(this->swing_mode);
|
||||
if (this->pending_vertical_direction_.has_value()) {
|
||||
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
|
||||
if (!vertical_axis_is_auto(target_swing_mode)) {
|
||||
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
|
||||
}
|
||||
this->pending_vertical_direction_.reset();
|
||||
}
|
||||
if (this->pending_horizontal_direction_.has_value()) {
|
||||
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
|
||||
if (!horizontal_axis_is_auto(target_swing_mode)) {
|
||||
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
|
||||
}
|
||||
this->pending_horizontal_direction_.reset();
|
||||
}
|
||||
{
|
||||
@@ -687,9 +791,17 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
|
||||
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
|
||||
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
|
||||
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
|
||||
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
|
||||
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
|
||||
: "Unknown";
|
||||
#ifdef USE_ESP8266
|
||||
// Static so a delayed on_alarm automation can still read it, but only until the next
|
||||
// alarm change, including a later one in this packet; `code` stays correct
|
||||
static char alarm_message[HonAlarmMessages::MAX_LENGTH + 1];
|
||||
ESPHOME_strncpy_P(alarm_message,
|
||||
reinterpret_cast<const char *>(
|
||||
HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX)),
|
||||
sizeof(alarm_message));
|
||||
#else
|
||||
const char *alarm_message = HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX);
|
||||
#endif
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
|
||||
alarm_code, alarm_message);
|
||||
if (alarm_status) {
|
||||
@@ -810,6 +922,41 @@ void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
|
||||
}
|
||||
#endif // USE_SWITCH
|
||||
|
||||
#ifdef USE_SELECT
|
||||
template<typename Mode, size_t N>
|
||||
static void publish_airflow_index(select::Select *sel, const std::array<Mode, N> &order, Mode mode) {
|
||||
if (sel == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Saved settings never hold AUTO after initialization(); any other value outside the table shows as Center
|
||||
size_t index = N;
|
||||
size_t center = 0;
|
||||
for (size_t i = 0; i < N; i++) {
|
||||
const Mode entry = airflow_order_at(order, i);
|
||||
if (entry == mode) {
|
||||
index = i;
|
||||
} else if (entry == Mode::CENTER) {
|
||||
center = i;
|
||||
}
|
||||
}
|
||||
if (index == N) {
|
||||
index = center;
|
||||
}
|
||||
const auto active = sel->active_index();
|
||||
if (active.has_value() && *active == index) {
|
||||
return;
|
||||
}
|
||||
sel->publish_state(index);
|
||||
}
|
||||
|
||||
void HonClimate::publish_airflow_selects_() {
|
||||
publish_airflow_index(this->vertical_airflow_select_, VERTICAL_SWING_MODES_ORDER,
|
||||
this->settings_.last_vertiacal_swing);
|
||||
publish_airflow_index(this->horizontal_airflow_select_, HORIZONTAL_SWING_MODES_ORDER,
|
||||
this->settings_.last_horizontal_swing);
|
||||
}
|
||||
#endif // USE_SELECT
|
||||
|
||||
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||
size_t expected_size =
|
||||
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
|
||||
@@ -1044,16 +1191,25 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
// Saving last known non auto mode for vertical and horizontal swing
|
||||
this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode;
|
||||
this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode;
|
||||
bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) &&
|
||||
(this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) &&
|
||||
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) ||
|
||||
((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) &&
|
||||
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing));
|
||||
if (save_settings) {
|
||||
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
|
||||
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
|
||||
const uint32_t now_ms = App.get_loop_component_start_time();
|
||||
bool save_vertical = !in_debounce(this->vertical_direction_set_time_, now_ms) &&
|
||||
!is_auto(this->current_vertical_swing_.value()) &&
|
||||
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing);
|
||||
bool save_horizontal = !in_debounce(this->horizontal_direction_set_time_, now_ms) &&
|
||||
!is_auto(this->current_horizontal_swing_.value()) &&
|
||||
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing);
|
||||
if (save_vertical || save_horizontal) {
|
||||
if (save_vertical) {
|
||||
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
|
||||
}
|
||||
if (save_horizontal) {
|
||||
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
|
||||
}
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
#ifdef USE_SELECT
|
||||
this->publish_airflow_selects_();
|
||||
#endif
|
||||
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||
}
|
||||
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||
@@ -1229,8 +1385,10 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
(uint8_t) hon_protocol::DataParameters::SET_POINT,
|
||||
buffer, 2);
|
||||
}
|
||||
ClimateSwingMode target_swing_mode = this->swing_mode;
|
||||
// Vertical swing mode
|
||||
if (climate_control.swing_mode.has_value()) {
|
||||
target_swing_mode = climate_control.swing_mode.value();
|
||||
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO};
|
||||
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO};
|
||||
switch (climate_control.swing_mode.value()) {
|
||||
@@ -1256,6 +1414,32 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
|
||||
vertical_swing_buf, 2);
|
||||
}
|
||||
// A swing change above already sent both axes from settings_.last_*, which set_*_airflow() updated
|
||||
const bool swing_sent = climate_control.swing_mode.has_value();
|
||||
if (this->pending_vertical_direction_.has_value()) {
|
||||
if (vertical_axis_is_auto(target_swing_mode)) {
|
||||
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
|
||||
} else if (!swing_sent) {
|
||||
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) this->pending_vertical_direction_.value()};
|
||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
|
||||
vertical_swing_buf, 2);
|
||||
}
|
||||
this->pending_vertical_direction_.reset();
|
||||
}
|
||||
if (this->pending_horizontal_direction_.has_value()) {
|
||||
if (horizontal_axis_is_auto(target_swing_mode)) {
|
||||
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
|
||||
} else if (!swing_sent) {
|
||||
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) this->pending_horizontal_direction_.value()};
|
||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
|
||||
horizontal_swing_buf, 2);
|
||||
}
|
||||
this->pending_horizontal_direction_.reset();
|
||||
}
|
||||
// Fan mode
|
||||
if (climate_control.fan_mode.has_value()) {
|
||||
switch (climate_control.fan_mode.value()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <queue>
|
||||
#ifdef USE_SENSOR
|
||||
@@ -14,12 +15,36 @@
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "haier_base.h"
|
||||
#include "hon_packet.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
#ifdef USE_SELECT
|
||||
// Positions in the order the select platform lists them; in flash on ESP8266, so read with airflow_order_at()
|
||||
inline constexpr std::array<hon_protocol::VerticalSwingMode, 7> VERTICAL_SWING_MODES_ORDER PROGMEM{
|
||||
hon_protocol::VerticalSwingMode::HEALTH_UP, hon_protocol::VerticalSwingMode::MAX_UP,
|
||||
hon_protocol::VerticalSwingMode::UP, hon_protocol::VerticalSwingMode::CENTER,
|
||||
hon_protocol::VerticalSwingMode::DOWN, hon_protocol::VerticalSwingMode::MAX_DOWN,
|
||||
hon_protocol::VerticalSwingMode::HEALTH_DOWN,
|
||||
};
|
||||
inline constexpr std::array<hon_protocol::HorizontalSwingMode, 5> HORIZONTAL_SWING_MODES_ORDER PROGMEM{
|
||||
hon_protocol::HorizontalSwingMode::MAX_LEFT, hon_protocol::HorizontalSwingMode::LEFT,
|
||||
hon_protocol::HorizontalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::RIGHT,
|
||||
hon_protocol::HorizontalSwingMode::MAX_RIGHT,
|
||||
};
|
||||
|
||||
template<typename Mode, size_t N> Mode airflow_order_at(const std::array<Mode, N> &order, size_t index) {
|
||||
static_assert(sizeof(Mode) == 1);
|
||||
return static_cast<Mode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(order.data() + index)));
|
||||
}
|
||||
#endif
|
||||
|
||||
enum class CleaningState : uint8_t {
|
||||
NO_CLEANING = 0,
|
||||
SELF_CLEAN = 1,
|
||||
@@ -100,6 +125,16 @@ class HonClimate final : public HaierClimateBase {
|
||||
protected:
|
||||
switch_::Switch *beeper_switch_{nullptr};
|
||||
switch_::Switch *quiet_mode_switch_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
public:
|
||||
void set_vertical_airflow_select(select::Select *sel) { this->vertical_airflow_select_ = sel; }
|
||||
void set_horizontal_airflow_select(select::Select *sel) { this->horizontal_airflow_select_ = sel; }
|
||||
|
||||
protected:
|
||||
void publish_airflow_selects_();
|
||||
select::Select *vertical_airflow_select_{nullptr};
|
||||
select::Select *horizontal_airflow_select_{nullptr};
|
||||
#endif
|
||||
public:
|
||||
HonClimate();
|
||||
@@ -195,6 +230,9 @@ class HonClimate final : public HaierClimateBase {
|
||||
uint8_t big_data_counter_{0};
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
|
||||
// millis() of the last user-set position per axis, 0 when none
|
||||
uint32_t vertical_direction_set_time_{0};
|
||||
uint32_t horizontal_direction_set_time_{0};
|
||||
HonSettings settings_{};
|
||||
ESPPreferenceObject hon_rtc_;
|
||||
SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF};
|
||||
|
||||
@@ -197,60 +197,4 @@ enum class SubcommandsControl : uint16_t {
|
||||
// content: all values like in status packet)
|
||||
};
|
||||
|
||||
const std::string HON_ALARM_MESSAGES[] = {
|
||||
"Outdoor module failure",
|
||||
"Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure",
|
||||
"Outdoor EEPROM abnormality",
|
||||
"Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure",
|
||||
"Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit",
|
||||
"Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality",
|
||||
"Full water protection",
|
||||
"Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure",
|
||||
"CBD and module communication failure",
|
||||
"Indoor DC fan failure",
|
||||
"Outdoor DC fan failure",
|
||||
"Door switch failure",
|
||||
"Dust filter needs cleaning reminder",
|
||||
"Water shortage protection",
|
||||
"Humidity sensor failure",
|
||||
"Indoor temperature sensor failure",
|
||||
"Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure",
|
||||
"Outdoor PM2.5 sensor failure",
|
||||
"Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection",
|
||||
"Outdoor compressor operation abnormality",
|
||||
"Outdoor DC current protection",
|
||||
"Outdoor no-load failure",
|
||||
"CT current abnormality",
|
||||
"Indoor cooling freeze protection",
|
||||
"High and low pressure protection",
|
||||
"Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure",
|
||||
"Outdoor cooling overload",
|
||||
"Water pump drainage failure",
|
||||
"Three-phase power supply failure",
|
||||
"Four-way valve failure",
|
||||
"External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm",
|
||||
"Different mode operation failure",
|
||||
"Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure",
|
||||
"Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure",
|
||||
"50Hz zero crossing failure",
|
||||
"Outdoor unit failure",
|
||||
"Formaldehyde sensor failure",
|
||||
"VOC sensor failure",
|
||||
"CO2 sensor failure",
|
||||
"Firewall failure",
|
||||
};
|
||||
|
||||
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
|
||||
|
||||
} // namespace esphome::haier::hon_protocol
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import (
|
||||
CONF_HAIER_ID,
|
||||
CONF_HORIZONTAL_AIRFLOW,
|
||||
CONF_VERTICAL_AIRFLOW,
|
||||
HonClimate,
|
||||
haier_ns,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@paveldn"]
|
||||
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
|
||||
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
|
||||
|
||||
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
|
||||
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
|
||||
"Health Up",
|
||||
"Max Up",
|
||||
"Up",
|
||||
"Center",
|
||||
"Down",
|
||||
"Max Down",
|
||||
"Health Down",
|
||||
]
|
||||
|
||||
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = [
|
||||
"Max Left",
|
||||
"Left",
|
||||
"Center",
|
||||
"Right",
|
||||
"Max Right",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
|
||||
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
|
||||
VerticalAirflowSelect,
|
||||
icon="mdi:arrow-expand-vertical",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
|
||||
HorizontalAirflowSelect,
|
||||
icon="mdi:arrow-expand-horizontal",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
await selects(
|
||||
CONF_VERTICAL_AIRFLOW,
|
||||
hub.set_vertical_airflow_select,
|
||||
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
|
||||
)
|
||||
await selects(
|
||||
CONF_HORIZONTAL_AIRFLOW,
|
||||
hub.set_horizontal_airflow_select,
|
||||
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
|
||||
protected:
|
||||
void control(size_t index) override {
|
||||
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
|
||||
protected:
|
||||
void control(size_t index) override {
|
||||
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
|
||||
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
|
||||
var = await climate_ir.new_climate_ir(config, tag())
|
||||
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
|
||||
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <IRSender.h>
|
||||
#include <HeatpumpIRFactory.h>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
|
||||
|
||||
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
|
||||
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
|
||||
};
|
||||
template<Protocol P> struct ProtocolClass;
|
||||
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
|
||||
return new typename ProtocolClass<P>::type(); // NOLINT
|
||||
}
|
||||
|
||||
// Codegen calls only the configured protocols, so the linker drops the other classes
|
||||
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
|
||||
template<> struct ProtocolClass<protocol> { using type = cls; }; \
|
||||
template HeatpumpIR *create_heatpump_ir<protocol>();
|
||||
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
|
||||
|
||||
#undef HEATPUMPIR_PROTOCOL
|
||||
|
||||
void HeatpumpIRClimate::setup() {
|
||||
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
|
||||
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
|
||||
ESP_LOGE(TAG, "Invalid protocol");
|
||||
return;
|
||||
}
|
||||
this->heatpump_ir_ = protocol_constructor->second();
|
||||
climate_ir::ClimateIR::setup();
|
||||
if (this->sensor_) {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
|
||||
@@ -89,25 +89,21 @@ enum VerticalDirection {
|
||||
VERTICAL_DIRECTION_DOWN = 5,
|
||||
};
|
||||
|
||||
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir();
|
||||
|
||||
/// Selects the protocol at compile time so the enum and the library object always match.
|
||||
template<Protocol P> struct ProtocolTag {};
|
||||
|
||||
// Temperature
|
||||
const float TEMP_MIN = 0; // Celsius
|
||||
const float TEMP_MAX = 100; // Celsius
|
||||
|
||||
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
HeatpumpIRClimate()
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
template<Protocol P>
|
||||
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
|
||||
void setup() override;
|
||||
void set_protocol(Protocol protocol) {
|
||||
this->protocol_ = protocol;
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
||||
this->default_horizontal_direction_ = horizontal_direction;
|
||||
}
|
||||
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
|
||||
|
||||
protected:
|
||||
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
|
||||
heatpump_ir_(heatpump_ir),
|
||||
protocol_(protocol) {
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
HeatpumpIR *heatpump_ir_;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
|
||||
@@ -59,8 +59,8 @@ namespace esphome::host {
|
||||
char **get_argv() { return s_argv; }
|
||||
|
||||
const std::string &get_exe_path() {
|
||||
static const std::string empty;
|
||||
return s_exe_path != nullptr ? *s_exe_path : empty;
|
||||
static const std::string EMPTY;
|
||||
return s_exe_path != nullptr ? *s_exe_path : EMPTY;
|
||||
}
|
||||
|
||||
void arm_reexec(const std::string &path) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "core.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <ctime>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -39,8 +39,8 @@ bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock();
|
||||
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
static const uint8_t esphome_host_mac_address[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
|
||||
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
|
||||
static constexpr uint8_t HOST_MAC_ADDRESS[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
|
||||
memcpy(mac, HOST_MAC_ADDRESS, sizeof(HOST_MAC_ADDRESS));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -45,11 +46,10 @@ void HostPreferences::setup_() {
|
||||
break;
|
||||
if (fread(&len, sizeof(len), 1, fp) != 1)
|
||||
break;
|
||||
uint8_t data[len];
|
||||
if (fread(data, sizeof(uint8_t), len, fp) != len)
|
||||
std::vector<uint8_t> data(len);
|
||||
if (fread(data.data(), sizeof(uint8_t), len, fp) != len)
|
||||
break;
|
||||
std::vector vec(data, data + len);
|
||||
this->data_[key] = vec;
|
||||
this->data_[key] = std::move(data);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
for (const auto &[name, value] : request_headers) {
|
||||
h_headers.emplace(name, value);
|
||||
}
|
||||
httplib::Client client(scheme_host.c_str());
|
||||
httplib::Client client(scheme_host);
|
||||
if (!client.is_valid()) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
|
||||
return nullptr;
|
||||
@@ -114,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
}
|
||||
|
||||
container->content_length = container->response_body_.size();
|
||||
for (auto header : response.headers) {
|
||||
for (const auto &header : response.headers) {
|
||||
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
|
||||
auto lower_name = str_lower_case(header.first); // NOLINT
|
||||
if (should_collect_header(lower_case_collect_headers, lower_name)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, speaker
|
||||
from esphome.components.microphone import DOMAIN as MICROPHONE_DOMAIN
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -14,9 +15,11 @@ from esphome.const import (
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_TIMEOUT,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_I2S_AUDIO_ID,
|
||||
CONF_I2S_DOUT_PIN,
|
||||
CONF_I2S_MODE,
|
||||
CONF_LEFT,
|
||||
@@ -44,6 +47,7 @@ I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
|
||||
|
||||
CONF_DAC_TYPE = "dac_type"
|
||||
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
CONF_SPDIF_MODE = "spdif_mode"
|
||||
|
||||
I2SAudioSpeakerBase = i2s_audio_ns.class_(
|
||||
@@ -173,6 +177,7 @@ BASE_SCHEMA = (
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.one_of(CONF_NEVER, lower=True),
|
||||
),
|
||||
cv.Optional(CONF_KEEP_ALIVE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -235,6 +240,23 @@ def _final_validate(config: ConfigType) -> None:
|
||||
raise cv.Invalid("SPDIF mode requires 'i2s_comm_fmt: stand_i2s'")
|
||||
if config[CONF_MCLK_MULTIPLE] != 256:
|
||||
raise cv.Invalid("SPDIF mode requires 'mclk_multiple: 256'")
|
||||
if config[CONF_KEEP_ALIVE]:
|
||||
raise cv.Invalid("SPDIF mode does not support 'keep_alive'")
|
||||
|
||||
if config[CONF_KEEP_ALIVE]:
|
||||
# A kept channel holds the port between sessions, so nothing else may share the bus
|
||||
bus = config[CONF_I2S_AUDIO_ID]
|
||||
full_config = fv.full_config.get()
|
||||
on_bus = sum(
|
||||
1
|
||||
for domain in (speaker.DOMAIN, MICROPHONE_DOMAIN)
|
||||
for entry in full_config.get(domain, [])
|
||||
if entry.get(CONF_I2S_AUDIO_ID) == bus
|
||||
)
|
||||
if on_bus > 1:
|
||||
raise cv.Invalid(
|
||||
"'keep_alive' needs its own I2S bus; another speaker or microphone shares this one"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -262,6 +284,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_TIMEOUT] != CONF_NEVER:
|
||||
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
|
||||
if config[CONF_KEEP_ALIVE]:
|
||||
cg.add(var.set_keep_alive(True))
|
||||
|
||||
|
||||
# The SPDIF encoder and speaker are fully #ifdef'd on USE_I2S_AUDIO_SPDIF_MODE,
|
||||
|
||||
@@ -56,8 +56,9 @@ void I2SAudioSpeakerBase::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Speaker:\n"
|
||||
" Pin: %d\n"
|
||||
" Buffer duration: %" PRIu32,
|
||||
static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_);
|
||||
" Buffer duration: %" PRIu32 "\n"
|
||||
" Keep alive: %s",
|
||||
static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_, YESNO(this->keep_alive_));
|
||||
if (this->timeout_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
|
||||
}
|
||||
@@ -105,7 +106,13 @@ void I2SAudioSpeakerBase::loop() {
|
||||
vTaskDelete(this->speaker_task_handle_);
|
||||
this->speaker_task_handle_ = nullptr;
|
||||
|
||||
this->stop_i2s_driver_();
|
||||
// The task raises this bit before it exits, possibly a loop pass before TASK_STOPPED is seen, so read it
|
||||
// live here; a channel the task could not bring up is rebuilt next time rather than kept
|
||||
const bool task_failed = xEventGroupGetBits(this->event_group_) & SpeakerEventGroupBits::ERR_ESP_NO_MEM;
|
||||
if (task_failed) {
|
||||
ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)");
|
||||
}
|
||||
this->stop_i2s_driver_(this->keep_alive_ && !task_failed);
|
||||
// ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at
|
||||
// the top of loop(): the audio source's task can raise a start at any point above, including
|
||||
// during stop_i2s_driver_(), and nothing would ever re-issue it.
|
||||
@@ -121,11 +128,6 @@ void I2SAudioSpeakerBase::loop() {
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
}
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)");
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
}
|
||||
|
||||
// Handle the speaker's state
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING:
|
||||
@@ -133,8 +135,8 @@ void I2SAudioSpeakerBase::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Still starting up or winding down from a previous run
|
||||
if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) {
|
||||
// Still winding down from a previous run
|
||||
if (this->speaker_task_handle_ != nullptr) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -154,7 +156,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
if (this->speaker_task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
this->stop_i2s_driver_(false); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
}
|
||||
break;
|
||||
case speaker::STATE_RUNNING: // Intentional fallthrough
|
||||
@@ -270,31 +272,25 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
|
||||
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize I2S channel");
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = nullptr;
|
||||
this->delete_channel_();
|
||||
this->parent_->unlock();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Both queues are drained by the speaker task before it preloads, whether the channel is new or kept
|
||||
if (this->i2s_event_queue_ == nullptr) {
|
||||
this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t));
|
||||
} else {
|
||||
// Reset queue to clear any stale events from previous task
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
}
|
||||
|
||||
// Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue
|
||||
// so a fully-saturated DMA pipeline cannot overflow either side before drain.
|
||||
if (this->write_records_queue_ == nullptr) {
|
||||
this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t));
|
||||
} else {
|
||||
xQueueReset(this->write_records_queue_);
|
||||
}
|
||||
|
||||
if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)");
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = nullptr;
|
||||
this->delete_channel_();
|
||||
this->parent_->unlock();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
@@ -302,22 +298,30 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::stop_i2s_driver_() {
|
||||
void I2SAudioSpeakerBase::stop_i2s_driver_(bool keep_channel) {
|
||||
if (this->tx_handle_ != nullptr) {
|
||||
i2s_channel_disable(this->tx_handle_);
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = nullptr;
|
||||
|
||||
// i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only
|
||||
// clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that
|
||||
// setup installed. If another speaker reuses this port (shared bus), its audio still reaches our
|
||||
// dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a
|
||||
// SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise.
|
||||
this->park_dout_pin_();
|
||||
// A kept channel lets the next start skip i2s_new_channel and i2s_channel_init_std_mode, whose GPIO and
|
||||
// clock reconfiguration is the audible pop
|
||||
if (!keep_channel) {
|
||||
this->delete_channel_();
|
||||
}
|
||||
}
|
||||
this->parent_->unlock();
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::delete_channel_() {
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = nullptr;
|
||||
|
||||
// i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only
|
||||
// clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that
|
||||
// setup installed. If another speaker reuses this port (shared bus), its audio still reaches our
|
||||
// dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a
|
||||
// SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise.
|
||||
this->park_dout_pin_();
|
||||
}
|
||||
|
||||
bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
|
||||
int64_t now = esp_timer_get_time();
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
|
||||
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
|
||||
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
|
||||
void set_keep_alive(bool keep_alive) { this->keep_alive_ = keep_alive; }
|
||||
void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; }
|
||||
|
||||
/// @brief Get the I2S TX channel handle
|
||||
@@ -126,12 +127,15 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
|
||||
size_t event_queue_size);
|
||||
|
||||
/// @brief Stops the I2S driver and unlocks the I2S port
|
||||
void stop_i2s_driver_();
|
||||
/// @brief Stops the I2S driver and unlocks the I2S port; keep_channel leaves the disabled channel allocated
|
||||
void stop_i2s_driver_(bool keep_channel);
|
||||
|
||||
/// @brief Detaches dout from the I2S signal and drives it low
|
||||
void park_dout_pin_();
|
||||
|
||||
/// @brief Deletes the I2S channel and parks dout; the caller holds the bus lock
|
||||
void delete_channel_();
|
||||
|
||||
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
|
||||
virtual void on_task_stopped() {}
|
||||
|
||||
@@ -177,6 +181,7 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
optional<uint32_t> timeout_;
|
||||
|
||||
bool pause_state_{false};
|
||||
bool keep_alive_{false};
|
||||
|
||||
// Smooths software gain changes. The main loop posts targets, the speaker task processes;
|
||||
// GainRamp's mailbox makes that safe. The main loop is the only poster.
|
||||
|
||||
@@ -75,6 +75,9 @@ void I2SAudioSpeaker::dump_config() {
|
||||
|
||||
void I2SAudioSpeaker::run_speaker_task() {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
|
||||
// Stale records from the previous run would pair with the wrong DMA events
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
xQueueReset(this->write_records_queue_);
|
||||
|
||||
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
|
||||
// Ensure ring buffer duration is at least the duration of all DMA buffers
|
||||
@@ -389,8 +392,6 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
|
||||
esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) {
|
||||
this->current_stream_info_ = audio_stream_info;
|
||||
|
||||
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
|
||||
// Can't reconfigure I2S bus, so the sample rate must match the configured value
|
||||
ESP_LOGE(TAG, "Incompatible stream settings");
|
||||
@@ -428,6 +429,16 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// Reuse a kept channel while the stream format matches
|
||||
if (this->tx_handle_ != nullptr) {
|
||||
if (this->current_stream_info_ == audio_stream_info) {
|
||||
return ESP_OK;
|
||||
}
|
||||
this->delete_channel_();
|
||||
}
|
||||
|
||||
this->current_stream_info_ = audio_stream_info;
|
||||
|
||||
// The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info.
|
||||
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_);
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ DOMAIN = "ili9xxx"
|
||||
DEPRECATED_COMPONENT = """
|
||||
The 'ili9xxx' component is deprecated and no new models will be added to it.
|
||||
New model PRs should target the newer and more performant 'mipi_spi' component.
|
||||
It will be removed in ESPHome 2027.5.0.
|
||||
"""
|
||||
|
||||
@@ -220,8 +220,10 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Remove before 2027.5.0
|
||||
LOGGER.warning(
|
||||
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||
"The 'ili9xxx' component is deprecated and will be removed in ESPHome 2027.5.0, "
|
||||
"migrate to 'mipi_spi' instead."
|
||||
)
|
||||
rhs = MODELS[config[CONF_MODEL]].new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "ili9xxx_defines.h"
|
||||
#include "ili9xxx_init.h"
|
||||
|
||||
namespace esphome::ili9xxx {
|
||||
|
||||
static const char *const TAG = "ili9xxx";
|
||||
ESPHOME_LOG_TAG(TAG, "ili9xxx");
|
||||
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
|
||||
|
||||
enum ILI9XXXColorMode {
|
||||
|
||||
@@ -37,7 +37,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
@@ -348,11 +347,8 @@ def _final_validate(config: ConfigType) -> None:
|
||||
config
|
||||
)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
|
||||
@@ -891,12 +891,10 @@ static uint8_t color_to_nibble(const Color &color) {
|
||||
if (color.raw_32 == 0xFFFFFFFF)
|
||||
return 0x0F; // white
|
||||
|
||||
// Derive luma from RGB using Rec.601 weights (0.299/0.587/0.114, scaled by
|
||||
// 256). Rec.601 is the standard for converting SDR images to grayscale and
|
||||
// spreads saturated colours across the mid-range; Rec.709 instead crams them
|
||||
// against white/black where the 16 panel levels are hard to tell apart.
|
||||
auto luma = static_cast<uint8_t>((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
return quantize_8bit_to_nibble(luma);
|
||||
// Rec.601 luma is the standard for converting SDR images to grayscale and spreads saturated colours
|
||||
// across the mid-range; Rec.709 instead crams them against white/black where the 16 panel levels are
|
||||
// hard to tell apart.
|
||||
return quantize_8bit_to_nibble(display::rec601_luma(color));
|
||||
}
|
||||
|
||||
// 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535).
|
||||
|
||||
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE
|
||||
from esphome.types import ConfigType
|
||||
@@ -16,6 +17,8 @@ LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDev
|
||||
|
||||
|
||||
CONF_LD2450_ID = "ld2450_id"
|
||||
CONF_POLYGON_ZONE_ID = "polygon_zone_id"
|
||||
CONF_POLYGON_ZONES = "polygon_zones"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -50,6 +53,14 @@ _CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"),
|
||||
)
|
||||
|
||||
# Polygon zone sources build only when zones are configured
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{
|
||||
"polygon.cpp": "LD2450_POLYGON_ZONE_COUNT",
|
||||
"polygon_zone.cpp": "LD2450_POLYGON_ZONE_COUNT",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILTERS,
|
||||
CONF_HAS_MOVING_TARGET,
|
||||
CONF_HAS_STILL_TARGET,
|
||||
CONF_HAS_TARGET,
|
||||
@@ -11,7 +12,14 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2450_ID, LD2450Component
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_LD2450_ID,
|
||||
CONF_POLYGON_ZONE_ID,
|
||||
CONF_POLYGON_ZONES,
|
||||
FILTER_SOURCE_FILES,
|
||||
LD2450Component,
|
||||
)
|
||||
from .text import PolygonZone
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
@@ -19,6 +27,18 @@ ICON_MEDITATION = "mdi:meditation"
|
||||
ICON_SHIELD_ACCOUNT = "mdi:shield-account"
|
||||
ICON_TARGET_ACCOUNT = "mdi:target-account"
|
||||
|
||||
|
||||
def _reject_filters(config: ConfigType) -> ConfigType:
|
||||
# Clearing a polygon makes presence unknown without going through filters, so they would get out of step
|
||||
if CONF_FILTERS in config:
|
||||
raise cv.Invalid(
|
||||
"Polygon zone presence binary sensors can't have filters; use the ld2450 number platform's "
|
||||
"'presence_timeout' to keep presence on after targets leave",
|
||||
path=[CONF_FILTERS],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
|
||||
@@ -37,6 +57,16 @@ CONFIG_SCHEMA = {
|
||||
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_MEDITATION,
|
||||
),
|
||||
cv.Optional(CONF_POLYGON_ZONES): cv.ensure_list(
|
||||
binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_POLYGON_ZONE_ID): cv.use_id(PolygonZone),
|
||||
}
|
||||
),
|
||||
_reject_filters,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -46,3 +76,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
|
||||
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
|
||||
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
|
||||
for presence_conf in config.get(CONF_POLYGON_ZONES, []):
|
||||
presence = await binary_sensor.new_binary_sensor(presence_conf)
|
||||
zone = await cg.get_variable(presence_conf[CONF_POLYGON_ZONE_ID])
|
||||
cg.add(zone.set_presence_binary_sensor(presence))
|
||||
|
||||
@@ -179,6 +179,11 @@ void LD2450Component::setup() {
|
||||
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
|
||||
this->set_presence_timeout();
|
||||
}
|
||||
#endif
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
for (PolygonZone *zone : this->polygon_zones_) {
|
||||
zone->setup();
|
||||
}
|
||||
#endif
|
||||
this->restart_and_read_all_info();
|
||||
}
|
||||
@@ -265,6 +270,12 @@ void LD2450Component::dump_config() {
|
||||
LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
|
||||
LOG_BUTTON(" ", "Restart", this->restart_button_);
|
||||
#endif
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
ESP_LOGCONFIG(TAG, "Polygon Zones:");
|
||||
for (PolygonZone *zone : this->polygon_zones_) {
|
||||
zone->dump_config();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD2450Component::loop() {
|
||||
@@ -465,15 +476,15 @@ void LD2450Component::handle_periodic_data_() {
|
||||
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
|
||||
// Loop thru targets
|
||||
for (index = 0; index < MAX_TARGETS; index++) {
|
||||
#ifdef USE_SENSOR
|
||||
// X
|
||||
start = TARGET_X + index * 8;
|
||||
is_moving = false;
|
||||
// tx is used for further calculations, so always needs to be populated
|
||||
// tx and ty are used for further calculations, so always need to be populated
|
||||
tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
// Y
|
||||
start = TARGET_Y + index * 8;
|
||||
ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
#ifdef USE_SENSOR
|
||||
// RESOLUTION
|
||||
start = TARGET_RESOLUTION + index * 8;
|
||||
res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
|
||||
@@ -542,6 +553,23 @@ void LD2450Component::handle_periodic_data_() {
|
||||
still_target_count = target_count - moving_target_count;
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
for (PolygonZone *zone : this->polygon_zones_) {
|
||||
if (!zone->has_polygon())
|
||||
continue;
|
||||
bool target_inside = false;
|
||||
for (const Target &target : this->target_info_) {
|
||||
// Untracked targets are stored as (0, 0)
|
||||
if ((target.x != 0 || target.y != 0) && zone->contains(target.x, target.y)) {
|
||||
target_inside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
zone->update(target_inside, now, this->get_timeout_status_(zone->get_last_inside_ms()));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Loop thru zones
|
||||
uint8_t zone_still_targets = 0;
|
||||
@@ -602,7 +630,7 @@ void LD2450Component::handle_periodic_data_() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// For presence timeout check
|
||||
if (target_count > 0) {
|
||||
this->presence_millis_ = App.get_loop_component_start_time();
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
#include "polygon_zone.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -137,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s);
|
||||
void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s);
|
||||
void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s);
|
||||
#endif
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
void register_polygon_zone(PolygonZone *zone) { this->polygon_zones_.push_back(zone); }
|
||||
#endif
|
||||
void reset_radar_zone();
|
||||
void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2,
|
||||
@@ -167,7 +174,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
uint32_t presence_millis_ = 0;
|
||||
uint32_t still_presence_millis_ = 0;
|
||||
uint32_t moving_presence_millis_ = 0;
|
||||
uint32_t timeout_ = 5;
|
||||
uint32_t timeout_ = DEFAULT_PRESENCE_TIMEOUT * 1000; // presence_timeout, in ms
|
||||
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||
uint8_t mac_address_[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
|
||||
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
|
||||
@@ -200,6 +207,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
StaticVector<PolygonZone *, LD2450_POLYGON_ZONE_COUNT> polygon_zones_;
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> data_callback_;
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user