Compare commits

...
Author SHA1 Message Date
J. Nick Koston b53609bf8e Shorten the comments 2026-10-13 21:57:50 -10:00
J. Nick Koston 62031f595f Test the RGB565 reader in both byte orders through get_pixel() and a chroma keyed draw 2026-10-13 21:57:50 -10:00
J. Nick Koston 86bf291cbb Expect the byte order constructor argument and compare the fallback colour without alpha 2026-10-13 21:57:50 -10:00
J. Nick Koston 89f0d978ef Hand the pixel loop its row major index with the row part hoisted, test the RGB fallback colour order 2026-10-13 21:57:50 -10:00
J. Nick Koston 6164fc56ee [image] Draw rows in the outer loop and hand opaque RGB565 and RGB blocks to the display 2026-10-13 21:57:50 -10:00
J. Nick Koston 906a9b8db3 Send a block drawn under a clipping region through the per pixel path 2026-10-13 21:55:40 -10:00
J. Nick Koston 33d441d734 Copy only the part of a block that is on the panel into the buffer 2026-10-13 17:41:36 -10:00
J. Nick Koston b7be3cc8c5 Shorten the comments 2026-10-13 17:35:31 -10:00
J. Nick Koston ad58ce888f Flush a buffered block right away unless update() will flush it 2026-10-13 17:31:18 -10:00
J. Nick Koston 1af952dcda Put a compatible block into the buffer when a writer owns one, so the update flush keeps it 2026-10-13 17:31:18 -10:00
J. Nick Koston 2620c897e7 Keep the write_to_display_ doc comment on its own declaration 2026-10-13 17:31:18 -10:00
J. Nick Koston d88c48835b Pass the configured colour order to the mipi_dsi driver 2026-10-13 17:31:18 -10:00
J. Nick Koston aac450a874 Replace an empty dirty box with the drawn rectangle instead of widening it 2026-10-13 17:31:18 -10:00
J. Nick Koston bc628413e0 Keep the algorithm include with the other includes 2026-10-13 17:31:18 -10:00
J. Nick Koston 8ad644a7a7 Mark the whole drawn rectangle dirty before flushing a converted block 2026-10-13 17:31:18 -10:00
J. Nick Koston 11506eab56 Copy an 888 block into the mipi_dsi buffer only when its byte layout matches the colour order 2026-10-13 17:31:18 -10:00
J. Nick Koston 7ee141a6e9 Name the virtual fallback hook without the trailing underscore 2026-10-13 17:31:18 -10:00
J. Nick Koston 32e06599f4 Keep non RGB blocks off the raw copy paths and drop them with an error on the unbuffered mipi_spi 2026-10-13 17:31:18 -10:00
J. Nick Koston f4a6beb8cd Compare against the configured byte order and write pixels in it, flush the mipi_dsi fallback 2026-10-13 17:31:18 -10:00
J. Nick Koston 0b87cbd9fc [display] Fall back per pixel when a bulk blit does not match the panel 2026-10-13 17:31:18 -10:00
J. Nick Koston c1fa73470e [display] Walk the blit fallback in frame buffer order and resolve the bitness once (#20651) 2026-10-13 17:31:13 -10:00
J. Nick Koston e41d16ba7e [haier] Keep the alarm message table in flash (#20641) 2026-10-14 03:23:28 +00:00
J. Nick Koston 5171113c87 [core] Let PROGMEM_STRING_TABLE hold more than 255 bytes (#20646) 2026-10-13 17:17:48 -10:00
techdregsandJ. Nick Koston 5718e4b779 [tmp102] Add text sensor for diagnostics (#20629)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:06:56 -10:00
J. Nick Koston 40e12cd0d8 [image] Add CodSpeed benchmarks for image, runtime image and display block draws (#20649) 2026-10-13 17:01:47 -10:00
Brandon HarveyandJ. Nick Koston 07029d0077 [sen6x] Add measurement control and maintenance actions (#18783)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 15:47:31 +13:00
Christopher Reimer bb1119d83f [esp32_can] Support 20 kbit/s on ESP32 revision 2 or later (#20426) 2026-10-13 22:34:03 -04:00
7b591b26d6 [sy6970] Service the I2C watchdog so charge/LED settings persist (#19424)
Co-authored-by: Willem Oldemans <>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:19:58 -10:00
f45e6f0bf4 [midea] Add display_control and send the display toggle over UART without a transmitter (#18671)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:03:18 -10:00
Keith BurzinskiandClaude Opus 5.5 4b8af08155 [network] Notify listeners when the active interface changes (#20684)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:57:59 -05:00
613973b432 Bump ruff from 0.16.10 to 0.17.0 (#20694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-13 21:53:44 -04:00
dependabot[bot] 9ee4314bc2 Bump esptool from 5.4.0 to 5.5.0 (#20693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:41:30 -10:00
fredkotzeandJ. Nick Koston 59f55dbe9e [epaper_spi] Add Waveshare 2.13in-G (JD79660) display models (#18141)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 15:40:28 -10:00
dependabot[bot] 8fce95454a Bump github/codeql-action/init from 4.38.2 to 4.38.3 (#20701)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:49 -10:00
dependabot[bot] 16134ee993 Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 (#20704)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:38 -10:00
dependabot[bot] bc39d56bb4 Bump CodSpeedHQ/action from 5.4.0 to 5.4.2 (#20705)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:29 -10:00
dependabot[bot] c5522abc71 Bump github/codeql-action/analyze from 4.38.2 to 4.38.3 (#20699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:39:11 -04:00
dependabot[bot] 7cb5906d2a Bump actions/upload-artifact from 7.0.1 to 7.0.2 (#20702)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:42 +00:00
dependabot[bot] d452dbce30 Bump actions/download-artifact from 8.0.1 to 8.0.2 (#20703)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:39 +00:00
dependabot[bot] 81e5b50087 Bump backports-zstd from 1.7.0 to 1.8.0 (#20695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:35 -10:00
dependabot[bot] 426a4a2dce Bump filelock from 4.0.12 to 4.1.0 (#20696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:26 -10:00
dependabot[bot]andesphome[bot] c95c17c4e7 Bump pyupgrade from 3.21.2 to 3.22.0 (#20698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-13 15:38:17 -10:00
dependabot[bot] 576d3325fe Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 in /.github/actions/restore-python (#20700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:37:47 -10:00
dependabot[bot] c9cf0ad92e Bump platformdirs from 4.12.3 to 4.12.4 (#20697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:37:36 -04:00
Felipe Guedes f505212e64 [epaper_spi] Add Waveshare 2.66" e-Paper Module (B) support (#20546) 2026-10-13 15:03:05 -10:00
J. Nick Koston 52a5e6a007 [rpi_dpi_rgb] Set removal of the deprecated component for 2027.5.0 (#20691) 2026-10-14 13:32:18 +13:00
J. Nick Koston 3b261d2cf0 [st7789v] Set removal of the deprecated component for 2027.5.0 (#20689) 2026-10-14 13:30:52 +13:00
J. Nick Koston 9238fcc727 [st7735] Set removal of the deprecated component for 2027.5.0 (#20688) 2026-10-14 13:30:21 +13:00
J. Nick Koston 96902cc122 [qspi_dbi] Set removal of the deprecated component for 2027.5.0 (#20690) 2026-10-14 13:29:52 +13:00
J. Nick Koston 95711d83d1 [ili9xxx] Set removal of the deprecated component for 2027.5.0 (#20687) 2026-10-14 13:29:29 +13:00
Panoplian 3cdcf0417a [lvgl] Make the list_outside_block tests work offline (#20676) 2026-10-13 13:31:08 -10:00
J. Nick Koston c6e90de262 [ci] Catch out-of-class member log tags and keep midea's in flash (#20674) 2026-10-14 12:21:24 +13:00
J. Nick Koston 92f6ab8367 [ci] Lint log tags in headers and core (#20668) 2026-10-14 12:21:24 +13:00
J. Nick Koston f61955baa2 [core] Declare the remaining log tags with ESPHOME_LOG_TAG (#20666) 2026-10-14 12:21:23 +13:00
J. Nick Koston 36e1d831d3 [core] Add a decimal fast path to parse_number<float> (#20639) 2026-10-14 12:18:35 +13:00
J. Nick Koston 63616bf642 [rtttl] Play a configured song from static storage without copying it (#19569) 2026-10-14 12:06:27 +13:00
d275926f65 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:55:22 -05:00
Andrej Friesen f265c46c21 [PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667) 2026-10-13 12:50:43 -10:00
Frédéric MetrichandClaude Opus 5.5 7d735f0677 [emontx] Lock the emonTx on reboot when its firmware supports it (#20621)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:09 -10:00
Remco van Essen 5c23a827b6 [tmc2209] Add current and diagnostic sensors (#20638) 2026-10-13 12:35:55 -10:00
Jonathan Swoboda e265c0c0fc [core] Sync the static analysis dependency pins with the code (#20681) 2026-10-13 12:30:42 -10:00
J. Nick Koston a0c7d754cb [syslog] Keep the log level to severity table in flash (#20662) 2026-10-13 12:22:35 -10:00
Jesse Hills a6dbd72c53 Merge branch 'beta' into dev 2026-10-14 11:15:09 +13:00
Jesse Hills 9d530f5c56 Merge pull request #20680 from esphome/bump-2026.10.0b4
2026.10.0b4
2026-10-14 11:14:49 +13:00
Panoplian 6562c49686 [thermostat] Allow a dry-only thermostat without cool_action (#20682) 2026-10-13 12:08:13 -10:00
guillempages a97ce3a201 [image] Fix pillow deprecation warning (#20679) 2026-10-13 11:58:21 -10:00
Jesse Hills 5a78f636b9 Bump version to 2026.10.0b4 2026-10-14 10:43:50 +13:00
tomaszduda23andClaude 53d504137b [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 10:43:49 +13:00
bb0546fc0b [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:43:49 +13:00
emes30andJ. Nick Koston b43630cf8e [rp2_pio] Use pioasm 4.1.0 so PIO programs run on RP2350 (#19260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:43:49 +13:00
Firat HajiyevandFirat17aze c9664d4e42 [wifi] Reject unsupported WPA3 on ESP8266 (#20592)
Co-authored-by: Firat17aze <187820330+Firat17aze@users.noreply.github.com>
2026-10-14 10:43:49 +13:00
Kevin Ahrendt 39ea6a3220 [audio] Bump microFLAC to v0.3.0 (#20591) 2026-10-14 10:43:49 +13:00
J. Nick Koston f4a6d6f18b [core] Create the PlatformIO HTTP cache dir before the prefetch resolve threads run (#20579) 2026-10-14 10:43:49 +13:00
esphome[bot] 6c24d4aa43 Bump aioesphomeapi from 46.9.0 to 46.10.0 (#20563) 2026-10-14 10:43:49 +13:00
J. Nick Koston 0dc1d3b5a1 [toshiba] Move the lookup tables to PROGMEM (#20636) 2026-10-13 11:25:18 -10:00
tomaszduda23andClaude 42341d05f9 [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 11:12:08 -10:00
J. Nick Koston f941fee5ed [homeassistant] Add sensor platform benchmarks (#20637) 2026-10-13 11:10:14 -10:00
Bonne Eggleston 8721976066 [epaper_spi] Abandon an update when the busy pin never clears (#20555) 2026-10-14 07:47:09 +11:00
188 changed files with 5205 additions and 508 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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: |
+2 -2
View File
@@ -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
+11 -11
View File
@@ -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
@@ -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
@@ -1448,7 +1448,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 +1647,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 +1710,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 +1740,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
+2 -2
View File
@@ -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}}"
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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]
+1
View File
@@ -439,6 +439,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
+2 -2
View File
@@ -5,8 +5,8 @@
namespace esphome::animation {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
image::ImageType type, image::Transparency transparent)
: Image(data_start, width, height, type, transparent),
image::ImageType type, image::Transparency transparent, bool big_endian)
: Image(data_start, width, height, type, transparent, big_endian),
animation_data_start_(data_start),
current_frame_(0),
animation_frame_count_(animation_frame_count),
+1 -1
View File
@@ -6,7 +6,7 @@ namespace esphome::animation {
class Animation final : public image::Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
image::Transparency transparent);
image::Transparency transparent, bool big_endian = false);
uint32_t get_animation_frame_count() const;
int get_current_frame() const;
+2 -1
View File
@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
from esphome.components.const import CONF_BYTE_ORDER, CONF_LOOP
from esphome.components.file import image as file_image
from esphome.components.file.image import image_schema, write_image
from esphome.components.image import Image_, validate_settings
@@ -90,6 +90,7 @@ async def setup_animation(config: ConfigType) -> None:
frame_count,
image_type,
trans_value,
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
+2 -1
View File
@@ -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) {
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,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
+53 -27
View File
@@ -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++)
+14
View File
@@ -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;
@@ -358,6 +363,9 @@ class Display : public PollingComponent {
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
*
* The length of each source buffer line (stride) will be x_offset + w + x_pad.
*
* An override that copies the block as is must send any other format, byte order or software
* rotation through this base version, which draws per pixel. Callers keep the block on screen.
*/
virtual 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);
@@ -717,6 +725,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.
*/
@@ -2,6 +2,7 @@
#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 };
@@ -10,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;
+15 -3
View File
@@ -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
+129 -27
View File
@@ -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
}
+24 -5
View File
@@ -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")
+19 -2
View File
@@ -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;
+8 -2
View File
@@ -106,6 +106,7 @@ 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;
@@ -127,6 +128,7 @@ 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);
@@ -143,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
*/
@@ -193,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
@@ -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.
@@ -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",
)
+22
View File
@@ -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"]
@@ -49,6 +54,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 +120,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;
+2 -1
View File
@@ -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,
+10 -2
View File
@@ -254,7 +254,13 @@ async def new_image(config: ConfigType) -> MockObj:
"""
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
var = cg.new_Pvariable(
config[CONF_ID], prog_arr, width, height, image_type, trans_value
config[CONF_ID],
prog_arr,
width,
height,
image_type,
trans_value,
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
)
add_metadata(
config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY]
@@ -330,7 +336,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])
+33 -3
View File
@@ -3,6 +3,7 @@
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include "hon_climate.h"
#include "hon_packet.h"
@@ -20,6 +21,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,
@@ -687,9 +709,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) {
-56
View File
@@ -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
+1
View File
@@ -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.
"""
+3 -1
View File
@@ -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 -1
View File
@@ -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 {
+133 -78
View File
@@ -1,5 +1,7 @@
#include "image.h"
#include <algorithm>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -7,92 +9,137 @@ ESPHOME_LOG_TAG(TAG, "image");
namespace esphome::image {
// Walks the clipped range with rows (ROWS_OUTER) or columns outer and hands the
// body its coordinates and row major index. The body must be ESPHOME_ALWAYS_INLINE,
// at -Os GCC otherwise calls it once per pixel.
template<bool ROWS_OUTER, typename F>
static void for_each_pixel(int x_start, int x_end, int y_start, int y_end, int width, F &&f) {
const int outer_start = ROWS_OUTER ? y_start : x_start;
const int outer_end = ROWS_OUTER ? y_end : x_end;
const int inner_start = ROWS_OUTER ? x_start : y_start;
const int inner_end = ROWS_OUTER ? x_end : y_end;
for (int outer = outer_start; outer < outer_end; outer++) {
const int outer_pos = ROWS_OUTER ? outer * width : outer;
for (int inner = inner_start; inner < inner_end; inner++) {
f(ROWS_OUTER ? inner : outer, ROWS_OUTER ? outer : inner,
ROWS_OUTER ? outer_pos + inner : outer_pos + inner * width);
}
}
}
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
int img_x0 = 0;
int img_y0 = 0;
int w = width_;
int h = height_;
int x_start = 0;
int y_start = 0;
int x_end = width_;
int y_end = height_;
auto clipping = display->get_clipping();
if (clipping.is_set()) {
if (clipping.x > x)
img_x0 += clipping.x - x;
x_start += clipping.x - x;
if (clipping.y > y)
img_y0 += clipping.y - y;
if (w > clipping.x2() - x)
w = clipping.x2() - x;
if (h > clipping.y2() - y)
h = clipping.y2() - y;
y_start += clipping.y - y;
if (x_end > clipping.x2() - x)
x_end = clipping.x2() - x;
if (y_end > clipping.y2() - y)
y_end = clipping.y2() - y;
}
switch (type_) {
case IMAGE_TYPE_BINARY: {
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparency_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
}
}
#ifndef USE_ESP8266
// Opaque RGB565 and RGB are already laid out for draw_pixels_at(), so the display
// gets the whole window. Not on the ESP8266, whose image bytes need progmem_read_byte().
if (this->transparency_ == TRANSPARENCY_OPAQUE &&
(this->type_ == IMAGE_TYPE_RGB565 || this->type_ == IMAGE_TYPE_RGB)) {
// Keep the block on screen
x_start = std::max(x_start, -x);
y_start = std::max(y_start, -y);
x_end = std::min(x_end, display->get_width() - x);
y_end = std::min(y_end, display->get_height() - y);
if (x_end <= x_start || y_end <= y_start)
return;
const bool rgb565 = this->type_ == IMAGE_TYPE_RGB565;
display->draw_pixels_at(x + x_start, y + y_start, x_end - x_start, y_end - y_start, this->data_start_,
display::COLOR_ORDER_RGB, rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_888,
this->big_endian_, x_start, y_start, this->width_ - x_end);
return;
}
#endif
// Rows keep the read and the write sequential; when the display swaps the axes
// columns keep the write sequential instead, and a strided write costs more.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
this->draw_<false>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
return;
}
#endif
// Without PSRAM a strided write is free, so one copy of the loops is enough
this->draw_<true>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
}
template<bool ROWS_OUTER>
void Image::draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
int y_start, int y_end) {
// Local copies, or the virtual draw_pixel_at() forces a reload every pixel
const Transparency transparency = this->transparency_;
const int width = this->width_;
switch (this->type_) {
case IMAGE_TYPE_BINARY:
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (transparency == TRANSPARENCY_OPAQUE) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
});
break;
case IMAGE_TYPE_RGB111:
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
auto color = this->get_rgb111_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
});
break;
case IMAGE_TYPE_GRAYSCALE: {
const uint8_t *const data = this->data_start_;
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
const uint8_t gray = progmem_read_byte(data + pos);
Color color = Color(gray, gray, gray, 0xFF);
if (transparency == TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
return; // transparent pixel, skip drawing
}
} else if (transparency == TRANSPARENCY_ALPHA_CHANNEL) {
// gray is the alpha: blend from color_off to color_on, drawn opaque
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
Color::blend_channel(color_off.g, color_on.g, gray),
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
}
display->draw_pixel_at(x + img_x, y + img_y, color);
});
break;
}
case IMAGE_TYPE_RGB111: {
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb111_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
}
case IMAGE_TYPE_GRAYSCALE:
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
const uint32_t pos = (img_x + img_y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
Color color = Color(gray, gray, gray, 0xFF);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
if (gray == 1) {
continue; // skip drawing
}
break;
case TRANSPARENCY_ALPHA_CHANNEL:
// gray is the alpha: blend from color_off to color_on, drawn opaque
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
Color::blend_channel(color_off.g, color_on.g, gray),
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
break;
default:
break;
}
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
break;
case IMAGE_TYPE_RGB565:
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
});
break;
case IMAGE_TYPE_RGB:
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
});
break;
}
}
@@ -185,7 +232,7 @@ Color Image::get_rgb111_pixel_(int x, int y) const {
return Color(rgb & 4 ? 0xFF : 0x00, rgb & 2 ? 0xFF : 0x00, rgb & 1 ? 0xFF : 0x00, opaque ? 0xFF : 0x00);
}
Color Image::get_rgb_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
const uint32_t pos = (x + y * this->width_) * static_cast<size_t>(this->bpp_) / 8;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 0), 0xFF);
@@ -205,8 +252,10 @@ Color Image::get_rgb_pixel_(int x, int y) const {
return color;
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * static_cast<size_t>(this->bpp_) / 8;
// High byte first when big endian
const uint8_t high = this->big_endian_ ? 0 : 1;
const uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + high), progmem_read_byte(pos + 1 - high));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
@@ -242,8 +291,14 @@ Color Image::get_grayscale_pixel_(int x, int y) const {
int Image::get_width() const { return this->width_; }
int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
: width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency,
bool big_endian)
: width_(width),
height_(height),
type_(type),
data_start_(data_start),
transparency_(transparency),
big_endian_(big_endian && type == IMAGE_TYPE_RGB565) {
switch (this->type_) {
case IMAGE_TYPE_BINARY:
this->bpp_ = 1;
+9 -2
View File
@@ -24,7 +24,9 @@ enum Transparency {
class Image : public display::BaseImage {
public:
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
/// big_endian is the RGB565 byte order; codegen writes little endian unless configured otherwise.
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency,
bool big_endian = false);
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
int get_width() const override;
int get_height() const override;
@@ -44,6 +46,10 @@ class Image : public display::BaseImage {
lv_image_dsc_t *get_lv_image_dsc();
#endif
protected:
/// Per-pixel draw of the clipped range, rows (true) or columns (false) outer.
template<bool ROWS_OUTER>
void draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
int y_start, int y_end);
bool get_binary_pixel_(int x, int y) const;
Color get_rgb111_pixel_(int x, int y) const;
Color get_rgb_pixel_(int x, int y) const;
@@ -55,7 +61,8 @@ class Image : public display::BaseImage {
ImageType type_;
const uint8_t *data_start_;
Transparency transparency_;
size_t bpp_{};
bool big_endian_;
uint8_t bpp_{};
#ifdef USE_LVGL
lv_img_dsc_t dsc_{};
#endif
+1
View File
@@ -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 -1
View File
@@ -214,7 +214,7 @@ enum class Orientation : uint8_t {
};
class LvglComponent final : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
constexpr static const char *const TAG = "lvgl"; // NOLINT: lvgl does not build for ESP8266
public:
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
+1 -1
View File
@@ -19,7 +19,7 @@ using alloc_string_t = std::basic_string<char, std::char_traits<char>, RAMAlloca
* @tparam V The type of the value in the mapping. Should be a basic type or pointer.
*/
static const char *const TAG = "mapping";
ESPHOME_LOG_TAG(TAG, "mapping");
template<typename K, typename V> class Mapping {
public:
+19
View File
@@ -3,7 +3,9 @@ from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
import esphome.config_validation as cv
from esphome.const import (
CONF_API,
CONF_DISABLED,
CONF_ETHERNET,
CONF_ID,
CONF_OPENTHREAD,
CONF_PORT,
@@ -285,6 +287,23 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# With both wifi and ethernet, the _esphomelib "network" TXT value follows the
# connected interface. OpenThread hands its services to the SRP client once,
# so it keeps the fixed value.
if (
CONF_API in CORE.config
and CONF_WIFI in CORE.config
and CONF_ETHERNET in CORE.config
and CONF_OPENTHREAD not in CORE.config
):
from esphome.components import network
cg.add_define("USE_MDNS_DYNAMIC_NETWORK_TXT")
# Where the network component tracks the default route (ESP32), follow
# it; otherwise mdns checks the connected interface every second.
if await network.register_active_interface_listener(var):
cg.add_define("USE_MDNS_ACTIVE_INTERFACE_LISTENER")
for service in config[CONF_SERVICES]:
txt_records = [
await _mdns_txt_record_templated(var, txt_key, txt_value)
+64 -1
View File
@@ -20,6 +20,10 @@
#ifdef USE_DASHBOARD_IMPORT
#include "esphome/components/dashboard_import/dashboard_import.h"
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
#include "esphome/components/ethernet/ethernet_component.h"
#include "esphome/components/wifi/wifi_component.h"
#endif
namespace esphome::mdns {
@@ -39,6 +43,57 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
// Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
// Same order as the default route in NetworkComponent::loop()
const char *MDNSComponent::connected_network_() const {
const bool ethernet = ethernet::global_eth_component->is_connected();
const bool wifi = wifi::global_wifi_component->is_connected();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
if (wifi)
return NETWORK_WIFI;
#endif
if (ethernet)
return NETWORK_ETHERNET;
if (wifi)
return NETWORK_WIFI;
return nullptr;
}
void MDNSComponent::update_network_txt_(const char *network) {
// With no interface connected, keep the last value instead of flapping to wifi
if (this->network_txt_ == nullptr || network == nullptr || network == this->network_txt_)
return;
if (!this->publish_network_txt_(network)) {
// The _esphomelib service is missing, so a retry would fail too
this->network_txt_ = nullptr;
return;
}
ESP_LOGD(TAG, "Network TXT: %s", network);
this->network_txt_ = network;
#ifdef USE_MDNS_STORE_SERVICES
this->network_record_->value = MDNS_STR(network);
#endif
}
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
void MDNSComponent::on_active_interface(network::NetworkInterface interface) {
switch (interface) {
case network::NetworkInterface::NETWORK_INTERFACE_ETHERNET:
this->update_network_txt_(NETWORK_ETHERNET);
break;
case network::NetworkInterface::NETWORK_INTERFACE_WIFI:
this->update_network_txt_(NETWORK_WIFI);
break;
default:
break;
}
}
#endif
#endif
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
#ifdef USE_MDNS_STORE_SERVICES
auto &services = this->services_;
@@ -160,7 +215,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI)
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
const char *network = this->connected_network_();
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
#ifdef USE_MDNS_STORE_SERVICES
// services is services_ here, which never reallocates
this->network_record_ = &txt_records.back();
#endif
#elif defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET)
+31
View File
@@ -32,6 +32,13 @@
#define USE_MDNS_DEVICE_INFO_TXT
#endif
// USE_MDNS_DYNAMIC_NETWORK_TXT (wifi + ethernet + api): the _esphomelib "network" TXT value follows the
// connected interface. With USE_MDNS_ACTIVE_INTERFACE_LISTENER (ESP32) the network component reports
// default route changes; otherwise the value is checked every NETWORK_TXT_CHECK_INTERVAL_MS.
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
#include "esphome/components/network/network_component.h"
#endif
namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -73,6 +80,10 @@ class MDNSComponent final : public Component
,
public ethernet::EthernetIPStateListener
#endif
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
,
public network::ActiveInterfaceListener
#endif
{
public:
void setup() override;
@@ -134,6 +145,10 @@ class MDNSComponent final : public Component
}
#endif
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
void on_active_interface(network::NetworkInterface interface) override;
#endif
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
@@ -150,6 +165,22 @@ class MDNSComponent final : public Component
void setup_buffers_and_register_(PlatformRegisterFn platform_register);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
const char *connected_network_() const;
/// Publish network as the "network" TXT value if it changed; nullptr keeps the current value.
void update_network_txt_(const char *network);
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
bool publish_network_txt_(const char *network);
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
const char *network_txt_{nullptr};
#ifdef USE_MDNS_STORE_SERVICES
/// The "network" record in services_, kept in step with network_txt_.
MDNSTXTRecord *network_record_{nullptr};
#endif
#endif
#ifdef USE_MDNS_DYNAMIC_TXT
/// Storage for runtime-generated TXT values from user lambdas
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
+19 -1
View File
@@ -89,7 +89,25 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
}
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
bool MDNSComponent::publish_network_txt_(const char *network) {
// ESP-IDF copies the value and announces the changed record
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
return false;
}
return true;
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp32);
#if defined(USE_MDNS_DYNAMIC_NETWORK_TXT) && !defined(USE_MDNS_ACTIVE_INTERFACE_LISTENER)
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
[this]() { this->update_network_txt_(this->connected_network_()); });
#endif
}
void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
+20
View File
@@ -46,7 +46,27 @@ void MDNSComponent::start_polling_window_() {
}
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
ESPHOME_LOG_TAG(TAG, "mdns");
bool MDNSComponent::publish_network_txt_(const char *network) {
// LEAmDNS replaces the value of an existing key; announce() sends the change
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
ESP_LOGW(TAG, "Failed to update network TXT");
return false;
}
MDNS.announce();
this->start_polling_window_();
return true;
}
#endif
void MDNSComponent::setup() {
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
// No-op until the first IP-up event registers the services
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
[this]() { this->update_network_txt_(this->connected_network_()); });
#endif
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
+1 -1
View File
@@ -5,7 +5,7 @@
namespace esphome::midea::ac {
const char *const Constants::TAG = "midea";
const char Constants::TAG[] PROGMEM = "midea";
const char *const Constants::FREEZE_PROTECTION = "Freeze Protection";
const char *const Constants::SILENT = "Silent";
const char *const Constants::TURBO = "Turbo";
+1 -1
View File
@@ -17,7 +17,7 @@ using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const char TAG[]; // PROGMEM, like ESPHOME_LOG_TAG
static const char *const FREEZE_PROTECTION;
static const char *const SILENT;
static const char *const TURBO;
+27 -7
View File
@@ -127,6 +127,19 @@ ClimateTraits AirConditioner::traits() {
return traits;
}
#ifdef USE_REMOTE_TRANSMITTER
static const LogString *display_control_str(DisplayControl control) {
switch (control) {
case DisplayControl::DISPLAY_CONTROL_UART:
return LOG_STR("UART");
case DisplayControl::DISPLAY_CONTROL_IR:
return LOG_STR("IR");
default:
return LOG_STR("AUTO");
}
}
#endif
void AirConditioner::dump_config() {
ESP_LOGCONFIG(Constants::TAG,
"MideaDongle:\n"
@@ -135,7 +148,10 @@ void AirConditioner::dump_config() {
" [x] Request attempts: %d",
this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts());
#ifdef USE_REMOTE_TRANSMITTER
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
ESP_LOGCONFIG(Constants::TAG,
" [x] Using RemoteTransmitter\n"
" [x] Display control: %s",
LOG_STR_ARG(display_control_str(this->display_control_)));
#endif
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
this->base_.getCapabilities().dump();
@@ -183,16 +199,20 @@ void AirConditioner::do_swing_step() {
}
void AirConditioner::do_display_toggle() {
if (this->base_.getCapabilities().supportLightControl()) {
this->base_.displayToggle();
} else {
#ifdef USE_REMOTE_TRANSMITTER
// AUTO uses IR unless the appliance reports light control over UART. Without a transmitter
// built in, every setting sends the UART request: Mundoclima, Rotenso and others accept it
// without advertising it in the 0xB5 capabilities report.
const bool use_ir = this->display_control_ == DisplayControl::DISPLAY_CONTROL_IR ||
(this->display_control_ == DisplayControl::DISPLAY_CONTROL_AUTO &&
!this->base_.getCapabilities().supportLightControl());
if (use_ir) {
IrSpecialData data(0x08);
this->transmitter_.transmit(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
return;
}
#endif
this->base_.displayToggle();
}
} // namespace esphome::midea::ac
@@ -21,12 +21,20 @@ using climate::ClimateModeMask;
using climate::ClimateSwingModeMask;
using climate::ClimatePresetMask;
// How the display toggle reaches the appliance; the AUTO rules are in do_display_toggle()
enum class DisplayControl : uint8_t {
DISPLAY_CONTROL_UART, // always the UART request
DISPLAY_CONTROL_AUTO, // UART when the appliance reports it or no transmitter is built in, else IR
DISPLAY_CONTROL_IR, // always the IR remote, needs a transmitter
};
class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate {
public:
void dump_config() override;
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
void set_display_control(DisplayControl control) { this->display_control_ = control; }
void on_status_change() override;
/* ############### */
@@ -54,6 +62,7 @@ class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditi
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
bool frost_protection_set_{false};
DisplayControl display_control_{DisplayControl::DISPLAY_CONTROL_AUTO};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
+24
View File
@@ -33,15 +33,18 @@ from esphome.const import (
UNIT_WATT,
)
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@dudanov"]
DEPENDENCIES = ["climate", "uart"]
AUTO_LOAD = ["sensor"]
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
CONF_DISPLAY_CONTROL = "display_control"
midea_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac")
AirConditioner = midea_ac_ns.class_("AirConditioner", climate.Climate, cg.Component)
Capabilities = midea_ac_ns.namespace("Constants")
DisplayControl = midea_ac_ns.enum("DisplayControl", is_class=True)
def templatize(value):
@@ -82,11 +85,29 @@ CUSTOM_PRESETS = {
"FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
}
# How midea_ac.display_toggle reaches the appliance; see do_display_toggle() for the AUTO rules
DISPLAY_CONTROLS = {
"UART": DisplayControl.DISPLAY_CONTROL_UART,
"AUTO": DisplayControl.DISPLAY_CONTROL_AUTO,
"IR": DisplayControl.DISPLAY_CONTROL_IR,
}
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
validate_display_control = cv.enum(DISPLAY_CONTROLS, upper=True)
def validate_display_control_with_transmitter(config: ConfigType) -> ConfigType:
"""`display_control: IR` needs a `remote_transmitter` to send through; UART and AUTO do not."""
if config.get(CONF_DISPLAY_CONTROL) == "IR" and CONF_TRANSMITTER_ID not in config:
raise cv.Invalid(
f"'{CONF_DISPLAY_CONTROL}: IR' requires '{CONF_TRANSMITTER_ID}' to be set"
)
return config
CONFIG_SCHEMA = cv.All(
climate.climate_schema(AirConditioner)
@@ -100,6 +121,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
cv.Optional(CONF_DISPLAY_CONTROL, default="AUTO"): validate_display_control,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
validate_swing_modes
@@ -134,6 +156,7 @@ CONFIG_SCHEMA = cv.All(
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_display_control_with_transmitter,
cv.only_on(
[
PLATFORM_ESP32,
@@ -204,6 +227,7 @@ async def to_code(config):
cg.add(var.set_transmitter(transmitter_))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
cg.add(var.set_display_control(config[CONF_DISPLAY_CONTROL]))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config:
+2
View File
@@ -198,6 +198,8 @@ async def to_code(config: ConfigType) -> None:
model.get_dimensions(config)
)
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
cg.add(var.set_big_endian(config[CONF_BYTE_ORDER] == BYTE_ORDER_BIG))
sequence = model.get_sequence(config)
cg.add(var.set_model(config[CONF_MODEL]))
+81 -13
View File
@@ -3,6 +3,9 @@
#include "mipi_dsi.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cstring>
namespace esphome::mipi_dsi {
// Maximum bytes to log for init commands (truncated if larger)
@@ -180,6 +183,8 @@ void MipiDsi::update() {
if (this->auto_clear_enabled_) {
this->clear();
}
// Blocks drawn by the writer stay in the buffer until the one flush below
this->updating_ = true;
if (this->show_test_card_) {
this->test_card();
} else if (this->page_ != nullptr) {
@@ -189,6 +194,17 @@ void MipiDsi::update() {
} else {
this->stop_poller();
}
this->updating_ = false;
this->flush_dirty_();
}
// Outside update() nothing else would flush, inside it the end of update() does.
void MipiDsi::flush_now_() {
if (!this->updating_)
this->flush_dirty_();
}
void MipiDsi::flush_dirty_() {
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
@@ -207,11 +223,36 @@ void MipiDsi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (w <= 0 || h <= 0)
return;
// if color mapping is required, pass the buck.
// note that endianness is not considered here - it is assumed to match!
if (bitness != this->color_depth_) {
// Copy as is only when the bytes are laid out the way draw_pixel_at() writes them; an RGB
// ordered little endian 888 block is b,g,r in memory. Anything else, or a block drawn under a clipping
// region, goes per pixel.
const bool same_layout = bitness == display::COLOR_BITNESS_565
? big_endian == this->big_endian_
: big_endian == (this->color_mode_ == display::COLOR_ORDER_RGB);
if (bitness != this->color_depth_ || order != display::COLOR_ORDER_RGB || !same_layout ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
this->mark_dirty_(x_start, y_start, w, h);
this->flush_now_();
return;
}
if (this->buffer_ != nullptr) {
// With a buffer the block goes through it, or a flush of the old contents would paint over it
// Only the part on the panel
const int first_col = std::max(0, -x_start), end_col = std::min(w, static_cast<int>(this->width_) - x_start);
const int first_row = std::max(0, -y_start), end_row = std::min(h, static_cast<int>(this->height_) - y_start);
if (first_col >= end_col || first_row >= end_row)
return;
const size_t bytes_per_pixel = 3 - this->color_depth_;
const size_t stride = x_offset + w + x_pad;
for (int y = first_row; y != end_row; y++) {
memcpy(this->buffer_ + ((y_start + y) * this->width_ + x_start + first_col) * bytes_per_pixel,
ptr + ((y_offset + y) * stride + x_offset + first_col) * bytes_per_pixel,
(end_col - first_col) * bytes_per_pixel);
}
this->mark_dirty_(x_start + first_col, y_start + first_row, end_col - first_col, end_row - first_row);
this->flush_now_();
return;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
@@ -259,10 +300,8 @@ bool MipiDsi::check_buffer_() {
return true;
}
void MipiDsi::draw_pixel_at(int x, int y, Color color) {
if (this->is_point_clipped(x, y))
return;
// Maps drawn coordinates to the buffer the way draw_pixel_at() stores them.
void MipiDsi::rotate_(int &x, int &y) const {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
@@ -279,6 +318,39 @@ void MipiDsi::draw_pixel_at(int x, int y, Color color) {
y = this->height_ - y - 1;
break;
}
}
// Widen the dirty box to the drawn rectangle, even where the buffer already held the same values.
void MipiDsi::mark_dirty_(int x_start, int y_start, int w, int h) {
int x1 = x_start, y1 = y_start;
int x2 = x_start + w - 1, y2 = y_start + h - 1;
this->rotate_(x1, y1);
this->rotate_(x2, y2);
const int right = static_cast<int>(this->width_) - 1;
const int bottom = static_cast<int>(this->height_) - 1;
const int left = std::clamp(std::min(x1, x2), 0, right);
const int top = std::clamp(std::min(y1, y2), 0, bottom);
x2 = std::clamp(std::max(x1, x2), 0, right);
y2 = std::clamp(std::max(y1, y2), 0, bottom);
// An empty box is low above high, so it is replaced rather than widened
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) {
this->x_low_ = left;
this->x_high_ = x2;
this->y_low_ = top;
this->y_high_ = y2;
return;
}
this->x_low_ = std::min<int>(this->x_low_, left);
this->x_high_ = std::max<int>(this->x_high_, x2);
this->y_low_ = std::min<int>(this->y_low_, top);
this->y_high_ = std::max<int>(this->y_high_, y2);
}
void MipiDsi::draw_pixel_at(int x, int y, Color color) {
if (this->is_point_clipped(x, y))
return;
this->rotate_(x, y);
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
@@ -288,9 +360,7 @@ void MipiDsi::draw_pixel_at(int x, int y, Color color) {
switch (this->color_depth_) {
case display::COLOR_BITNESS_565: {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
uint16_t new_color = lo_byte | (hi_byte << 8); // little endian
const uint16_t new_color = this->to_panel_565_(color);
if (ptr_16[pos] == new_color)
return;
ptr_16[pos] = new_color;
@@ -333,9 +403,7 @@ void MipiDsi::fill(Color color) {
switch (this->color_depth_) {
case display::COLOR_BITNESS_565: {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
uint16_t new_color = lo_byte | (hi_byte << 8); // little endian
const uint16_t new_color = this->to_panel_565_(color);
std::fill_n(ptr_16, this->width_ * this->height_, new_color);
break;
}
+16 -1
View File
@@ -6,11 +6,13 @@
// only applicable on ESP32-P4
#ifdef USE_ESP32_VARIANT_ESP32P4
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/gpio.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_io.h"
@@ -18,7 +20,7 @@
namespace esphome::mipi_dsi {
constexpr static const char *const TAG = "display.mipi_dsi";
ESPHOME_LOG_TAG(TAG, "display.mipi_dsi");
const uint8_t SW_RESET_CMD = 0x01;
const uint8_t SLEEP_OUT = 0x11;
const uint8_t SDIR_CMD = 0xC7;
@@ -41,6 +43,7 @@ class MipiDsi final : public display::Display {
: width_(width), height_(height), color_depth_(color_depth), pixel_mode_(pixel_mode) {}
display::ColorOrder get_color_mode() { return this->color_mode_; }
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; }
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
@@ -83,6 +86,16 @@ class MipiDsi final : public display::Display {
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
bool check_buffer_();
/// Send the region touched since the last flush to the panel.
void flush_dirty_();
void flush_now_();
void rotate_(int &x, int &y) const;
void mark_dirty_(int x_start, int y_start, int w, int h);
/// Pack a colour as the 565 value the panel's byte order expects in memory.
uint16_t to_panel_565_(Color color) const {
const uint16_t rgb565 = display::ColorUtil::color_to_565(color);
return this->big_endian_ ? convert_big_endian(rgb565) : rgb565;
}
GPIOPin *reset_pin_{nullptr};
std::vector<GPIOPin *> enable_pins_{};
size_t width_{};
@@ -104,6 +117,7 @@ class MipiDsi final : public display::Display {
bool invert_colors_{};
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
display::ColorBitness color_depth_;
bool big_endian_{false};
uint8_t pixel_mode_{};
esp_lcd_panel_handle_t handle_{};
@@ -111,6 +125,7 @@ class MipiDsi final : public display::Display {
esp_lcd_panel_io_handle_t io_handle_{};
SemaphoreHandle_t io_lock_{};
uint8_t *buffer_{nullptr};
bool updating_{false};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
+1
View File
@@ -290,6 +290,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_init_sequence(table, len(sequence)))
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
cg.add(var.set_big_endian(config[CONF_BYTE_ORDER] == BYTE_ORDER_BIG))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
+73 -11
View File
@@ -4,6 +4,9 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cstring>
#include <driver/gpio.h>
#include <esp_lcd_panel_ops.h>
#include <span>
@@ -185,6 +188,8 @@ void MipiRgb::update() {
if (this->auto_clear_enabled_) {
this->clear();
}
// Blocks drawn by the writer stay in the buffer until the one flush below
this->updating_ = true;
if (this->show_test_card_) {
this->test_card();
} else if (this->page_ != nullptr) {
@@ -194,6 +199,17 @@ void MipiRgb::update() {
} else {
this->stop_poller();
}
this->updating_ = false;
this->flush_dirty_();
}
// Outside update() nothing else would flush, inside it the end of update() does.
void MipiRgb::flush_now_() {
if (!this->updating_)
this->flush_dirty_();
}
void MipiRgb::flush_dirty_() {
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
@@ -212,12 +228,27 @@ void MipiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (w <= 0 || h <= 0 || this->is_failed())
return;
// if color mapping is required, pass the buck.
// note that endianness is not considered here - it is assumed to match!
if (bitness != display::COLOR_BITNESS_565) {
// RGB565 in the configured byte order at rotation 0 copies as is; anything else, or a block drawn under a
// clipping region, goes per pixel
if (bitness != display::COLOR_BITNESS_565 || order != display::COLOR_ORDER_RGB || big_endian != this->big_endian_ ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const uint8_t *>(this->buffer_), x_start, y_start,
this->width_ - w - x_start);
this->mark_dirty_(x_start, y_start, w, h);
this->flush_now_();
} else if (this->buffer_ != nullptr) {
// With a buffer the block goes through it, or a flush of the old contents would paint over it
// Only the part on the panel
const int first_col = std::max(0, -x_start), end_col = std::min(w, static_cast<int>(this->width_) - x_start);
const int first_row = std::max(0, -y_start), end_row = std::min(h, static_cast<int>(this->height_) - y_start);
if (first_col >= end_col || first_row >= end_row)
return;
const size_t stride = x_offset + w + x_pad;
for (int y = first_row; y != end_row; y++) {
memcpy(this->buffer_ + (y_start + y) * this->width_ + x_start + first_col,
ptr + ((y_offset + y) * stride + x_offset + first_col) * 2, (end_col - first_col) * 2);
}
this->mark_dirty_(x_start + first_col, y_start + first_row, end_col - first_col, end_row - first_row);
this->flush_now_();
} else {
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
@@ -260,10 +291,8 @@ bool MipiRgb::check_buffer_() {
return true;
}
void MipiRgb::draw_pixel_at(int x, int y, Color color) {
if (this->is_point_clipped(x, y) || this->is_failed())
return;
// Maps drawn coordinates to the buffer the way draw_pixel_at() stores them.
void MipiRgb::rotate_(int &x, int &y) const {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
@@ -280,13 +309,46 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) {
y = this->height_ - y - 1;
break;
}
}
// Widen the dirty box to the drawn rectangle, even where the buffer already held the same values.
void MipiRgb::mark_dirty_(int x_start, int y_start, int w, int h) {
int x1 = x_start, y1 = y_start;
int x2 = x_start + w - 1, y2 = y_start + h - 1;
this->rotate_(x1, y1);
this->rotate_(x2, y2);
const int right = static_cast<int>(this->width_) - 1;
const int bottom = static_cast<int>(this->height_) - 1;
const int left = std::clamp(std::min(x1, x2), 0, right);
const int top = std::clamp(std::min(y1, y2), 0, bottom);
x2 = std::clamp(std::max(x1, x2), 0, right);
y2 = std::clamp(std::max(y1, y2), 0, bottom);
// An empty box is low above high, so it is replaced rather than widened
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) {
this->x_low_ = left;
this->x_high_ = x2;
this->y_low_ = top;
this->y_high_ = y2;
return;
}
this->x_low_ = std::min<int>(this->x_low_, left);
this->x_high_ = std::max<int>(this->x_high_, x2);
this->y_low_ = std::min<int>(this->y_low_, top);
this->y_high_ = std::max<int>(this->y_high_, y2);
}
void MipiRgb::draw_pixel_at(int x, int y, Color color) {
if (this->is_point_clipped(x, y) || this->is_failed())
return;
this->rotate_(x, y);
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
if (!this->check_buffer_())
return;
size_t pos = (y * this->width_) + x;
uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color));
uint16_t new_color = this->to_panel_565_(color);
if (this->buffer_[pos] == new_color)
return;
this->buffer_[pos] = new_color;
@@ -311,7 +373,7 @@ void MipiRgb::fill(Color color) {
}
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color));
uint16_t new_color = this->to_panel_565_(color);
std::fill_n(ptr_16, this->width_ * this->height_, new_color);
this->x_low_ = 0;
this->y_low_ = 0;
+16 -1
View File
@@ -2,7 +2,9 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include <esp_lcd_panel_rgb.h>
#ifdef USE_SPI
#include "esphome/components/spi/spi.h"
@@ -10,7 +12,7 @@
namespace esphome::mipi_rgb {
constexpr static const char *const TAG = "display.mipi_rgb";
ESPHOME_LOG_TAG(TAG, "display.mipi_rgb");
const uint8_t SW_RESET_CMD = 0x01;
const uint8_t SLEEP_OUT = 0x11;
const uint8_t SDIR_CMD = 0xC7;
@@ -38,6 +40,7 @@ class MipiRgb : public display::Display {
display::ColorOrder get_color_mode() { return this->color_mode_; }
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; }
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; };
@@ -70,6 +73,16 @@ class MipiRgb : public display::Display {
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
bool check_buffer_();
/// Pack a colour as the 565 value the panel's byte order expects in memory.
uint16_t to_panel_565_(Color color) const {
const uint16_t rgb565 = display::ColorUtil::color_to_565(color);
return this->big_endian_ ? convert_big_endian(rgb565) : rgb565;
}
/// Send the region touched since the last flush to the panel.
void flush_dirty_();
void flush_now_();
void rotate_(int &x, int &y) const;
void mark_dirty_(int x_start, int y_start, int w, int h);
void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset);
void setup_enables_();
void common_setup_();
@@ -90,10 +103,12 @@ class MipiRgb : public display::Display {
const char *model_{"Unknown"};
bool invert_colors_{};
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
bool big_endian_{true};
size_t width_;
size_t height_;
uint16_t *buffer_{nullptr};
std::vector<GPIOPin *> enable_pins_{};
bool updating_{false};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
+23 -17
View File
@@ -7,10 +7,11 @@
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi";
ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
// Maximum bytes to log for commands (truncated if larger)
static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64;
@@ -129,19 +130,8 @@ class MipiSpi : public display::Display,
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
int get_width() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
int get_width() override { return display::rotation_swaps_axes(this->rotation_) ? HEIGHT : WIDTH; }
int get_height() override { return display::rotation_swaps_axes(this->rotation_) ? WIDTH : HEIGHT; }
void set_init_sequence(const uint8_t *sequence, size_t len) {
this->init_sequence_ = sequence;
@@ -215,9 +205,9 @@ class MipiSpi : public display::Display,
return;
if (w <= 0 || h <= 0)
return;
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
// note that the usual logging macros are banned in header files, so use their replacement
esph_log_e(TAG, "Unsupported color depth or bit order");
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN || order != display::COLOR_ORDER_RGB ||
(!HAS_HARDWARE_ROTATION && this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES) || this->is_clipping()) {
this->draw_pixels_slow(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
@@ -429,6 +419,12 @@ class MipiSpi : public display::Display,
}
}
// Without a buffer there is nothing to convert a foreign block into, so it is dropped
virtual void draw_pixels_slow(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
esph_log_e(TAG, "Unsupported color depth, order, bit order or rotation");
}
/**
* Writes a buffer to the display.
*
@@ -620,6 +616,9 @@ class MipiSpiBuffer
#endif
}
// With hardware rotation the buffer is written in drawn coordinates.
bool pixel_axes_swapped() const override { return !HAS_HARDWARE_ROTATION && display::Display::pixel_axes_swapped(); }
// Draw a pixel at the given coordinates.
void draw_pixel_at(int x, int y, Color color) override {
if (this->is_point_clipped(x, y))
@@ -672,6 +671,13 @@ class MipiSpiBuffer
}
protected:
// Convert per pixel into the buffer
void draw_pixels_slow(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset,
int x_pad) override {
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
}
// Rotate the coordinates to match the display orientation.
// Convert a color to the buffer pixel format.
+32 -1
View File
@@ -100,6 +100,37 @@ if (
network_ns = cg.esphome_ns.namespace("network")
NetworkComponent = network_ns.class_("NetworkComponent", cg.Component)
_request_active_interface_listener_slot = cg.slot_counter(
"NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT"
)
def has_active_interface() -> bool:
"""Return True when NetworkComponent tracks the interface holding the default route.
That is ESP32 with more than one interface in 'network: priority:', the same
condition that emits USE_NETWORK_DEFAULT_ROUTE.
"""
return CORE.is_esp32 and len(CORE.config.get(DOMAIN, {}).get(CONF_PRIORITY, [])) > 1
async def register_active_interface_listener(listener: cg.MockObj) -> bool:
"""Register an ActiveInterfaceListener with the NetworkComponent.
The listener is notified from the main loop when the interface holding the
default route changes. Returns False, and registers nothing, when
has_active_interface() is False; the caller then needs another way to
follow interface changes.
"""
if not has_active_interface():
return False
_request_active_interface_listener_slot()
network = await cg.get_variable(CORE.config[DOMAIN][CONF_ID])
cg.add(network.add_active_interface_listener(listener))
return True
IPAddress = network_ns.class_("IPAddress")
@@ -402,7 +433,7 @@ async def to_code(config: ConfigType) -> None:
# frameworks build from source.
# The ethernet/wifi-only assumption behind the arbitration is enforced in
# _final_validate() so a future unsupported type fails as a config error.
if len(priority_list) > 1 and CORE.is_esp32:
if has_active_interface():
cg.add_define("USE_NETWORK_DEFAULT_ROUTE")
# Have lwIP switch to the DNS servers of the netif that owns the
# default route whenever the arbitration changes it.
@@ -78,16 +78,24 @@ void NetworkComponent::loop() {
// USE_NETWORK_PRIMARY_INTERFACE_WIFI is emitted for a wifi-first priority list;
// it selects the reported address in util.cpp and doubles as the route-order
// pivot here — the two uses must stay in sync.
esp_netif_t *best;
esp_netif_t *wifi = connected_wifi_netif();
esp_netif_t *ethernet = connected_ethernet_netif();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
best = connected_wifi_netif();
if (best == nullptr)
best = connected_ethernet_netif();
esp_netif_t *best = wifi != nullptr ? wifi : ethernet;
#else
best = connected_ethernet_netif();
if (best == nullptr)
best = connected_wifi_netif();
esp_netif_t *best = ethernet != nullptr ? ethernet : wifi;
#endif
NetworkInterface active = NetworkInterface::NETWORK_INTERFACE_NONE;
if (best != nullptr) {
active = best == ethernet ? NetworkInterface::NETWORK_INTERFACE_ETHERNET : NetworkInterface::NETWORK_INTERFACE_WIFI;
}
if (active != this->active_interface_) {
this->active_interface_ = active;
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
for (auto *listener : this->active_interface_listeners_)
listener->on_active_interface(active);
#endif
}
if (best == nullptr) {
// Forget the last winner: stopping its netif cleared lwIP's default route and
// IDF's manual override suppresses re-election, so reconnect must re-assert it.
@@ -4,12 +4,32 @@
#include "esphome/core/component.h"
#ifdef USE_NETWORK_DEFAULT_ROUTE
#include "esphome/core/helpers.h"
// Forward declaration matching esp_netif's own typedef; avoids pulling esp_netif.h
// into this header.
using esp_netif_t = struct esp_netif_obj;
#endif
namespace esphome::network {
#ifdef USE_NETWORK_DEFAULT_ROUTE
/// The interface holding the default route. Lists only the interfaces loop() arbitrates
/// (ARBITRATED_NETWORK_TYPES in __init__.py); extend both together.
enum class NetworkInterface : uint8_t {
NETWORK_INTERFACE_NONE,
NETWORK_INTERFACE_ETHERNET,
NETWORK_INTERFACE_WIFI,
};
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
/// Notified from the main loop when the interface holding the default route changes.
class ActiveInterfaceListener {
public:
virtual void on_active_interface(NetworkInterface interface) = 0;
};
#endif
#endif
class NetworkComponent final : public Component {
public:
void setup() override;
@@ -20,6 +40,14 @@ class NetworkComponent final : public Component {
#ifdef USE_NETWORK_DEFAULT_ROUTE
void loop() override;
/// The first connected interface in network: priority order; NONE while none is connected.
NetworkInterface get_active_interface() const { return this->active_interface_; }
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
void add_active_interface_listener(ActiveInterfaceListener *listener) {
this->active_interface_listeners_.push_back(listener);
}
#endif
protected:
// Verify-lwIP-route interval for the settled state; keeps the global lwIP core
// mutex off the hot loop path.
@@ -27,6 +55,10 @@ class NetworkComponent final : public Component {
// Last netif this component made the default; avoids redundant esp_netif calls.
esp_netif_t *default_netif_{nullptr};
uint32_t last_route_check_{0};
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
StaticVector<ActiveInterfaceListener *, NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT> active_interface_listeners_;
#endif
NetworkInterface active_interface_{NetworkInterface::NETWORK_INTERFACE_NONE};
#endif
};
} // namespace esphome::network
+4 -2
View File
@@ -1040,11 +1040,13 @@ def run_compile(args, config: ConfigType) -> bool:
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
bootloader = zephyr_data()[KEY_BOOTLOADER]
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes.
# The bootloader resets on a package whose sd_req does not list its SoftDevice,
# so v7 accepts S140 7.0.1, 7.2.0 and 7.3.0 (e.g. Seeed XIAO nRF52840).
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
}
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
_UF2_FAMILY_IDS = {
+79
View File
@@ -0,0 +1,79 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
AUTO_LOAD = ["gpio_expander"]
CODEOWNERS = ["@ssieb", "@ajfriesen"]
DEPENDENCIES = ["i2c"]
DOMAIN = "pca9505"
MULTI_CONF = True
pca9505_ns = cg.esphome_ns.namespace("pca9505")
PCA9505Component = pca9505_ns.class_("PCA9505Component", cg.Component, i2c.I2CDevice)
PCA9505GPIOPin = pca9505_ns.class_(
"PCA9505GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9505Component)
)
CONF_PCA9505 = "pca9505"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value: ConfigType) -> ConfigType:
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
if value[CONF_INPUT] and value[CONF_OUTPUT]:
raise cv.Invalid("Mode must be either input or output")
return value
PCA9505_PIN_SCHEMA = pins.gpio_base_schema(
PCA9505GPIOPin,
cv.int_range(min=0, max=39),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
).extend(
{
cv.Required(CONF_PCA9505): cv.use_id(PCA9505Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCA9505, PCA9505_PIN_SCHEMA)
async def pca9505_pin_to_code(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_PCA9505])
cg.add(var.set_pin(config[CONF_NUMBER]))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var
+165
View File
@@ -0,0 +1,165 @@
#include "pca9505.h"
#include "esphome/core/log.h"
namespace esphome::pca9505 {
// Each register block holds one byte per port. Add the port number to reach a single
// port, or set the auto-increment bit to transfer the whole block in one go.
static constexpr uint8_t INPUT_REG = 0x00;
static constexpr uint8_t OUTPUT_REG = 0x08;
static constexpr uint8_t INVERT_REG = 0x10;
static constexpr uint8_t CONFIG_REG = 0x18;
static constexpr uint8_t MASK_REG = 0x20;
static constexpr uint8_t AUTO_INCREMENT = 0x80;
ESPHOME_LOG_TAG(TAG, "pca9505");
void PCA9505Component::setup() {
// Test to see if device exists
if (!this->read_bank_(0)) {
ESP_LOGE(TAG, "PCA9505 not detected at 0x%02X", this->address_);
this->mark_failed();
return;
}
// No polarity inversion
this->write_all_banks_(INVERT_REG, 0x00);
// All outputs low, before anything is switched to being an output
this->write_all_banks_(OUTPUT_REG, 0x00);
// All inputs at initialization. The part sees a 1 as an input, so this is the
// inverse of config_, which is all zeroes here.
this->write_all_banks_(CONFIG_REG, 0xFF);
// Mask every interrupt. This must be written, not assumed: the chip keeps its
// registers through an MCU reboot, so a previous run may have left pins unmasked.
for (uint8_t &bank : this->mask_) {
bank = 0xFF;
}
this->write_all_banks_(MASK_REG, 0xFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA9505Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
// Don't invalidate cache on read - only invalidate when interrupt fires
this->set_invalidate_on_read_(false);
}
// Nothing to poll until pin_mode() registers an input pin.
this->disable_loop();
}
void IRAM_ATTR PCA9505Component::gpio_intr(PCA9505Component *arg) { arg->enable_loop_soon_any_context(); }
void PCA9505Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
void PCA9505Component::dump_config() {
ESP_LOGCONFIG(TAG, "PCA9505:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
bool PCA9505Component::digital_read_hw(uint8_t pin) { return this->read_bank_(pin / 8); }
bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin / 8] & (1 << (pin % 8)); }
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
if (value) {
this->output_[bank] |= mask;
} else {
this->output_[bank] &= ~mask;
}
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
}
void PCA9505Component::pin_mode(uint8_t pin, gpio::Flags flags) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
if (flags & gpio::FLAG_INPUT) {
this->config_[bank] &= ~mask;
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
this->mask_[bank] &= ~mask;
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags & gpio::FLAG_OUTPUT) {
this->config_[bank] |= mask;
// An output must never raise INT.
this->mask_[bank] |= mask;
}
// Invert as the part sees a 1 as an input
this->write_bank_(CONFIG_REG, bank, ~this->config_[bank]);
// Without an interrupt pin INT is unconnected, so the masks written at setup can stay
if (this->interrupt_pin_ != nullptr) {
this->write_bank_(MASK_REG, bank, this->mask_[bank]);
}
}
bool PCA9505Component::read_bank_(uint8_t bank) {
if (this->is_failed())
return false;
if (this->read_register(INPUT_REG + bank, &this->input_[bank], 1) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to read input register"));
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9505Component::write_bank_(uint8_t reg, uint8_t bank, uint8_t value) {
if (this->is_failed())
return false;
if (this->write_register(reg + bank, &value, 1) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to write register"));
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9505Component::write_all_banks_(uint8_t reg, uint8_t value) {
if (this->is_failed())
return false;
uint8_t values[BANK_COUNT];
for (uint8_t &bank : values) {
bank = value;
}
if (this->write_register(reg | AUTO_INCREMENT, values, sizeof(values)) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to write register"));
return false;
}
this->status_clear_warning();
return true;
}
float PCA9505Component::get_setup_priority() const { return setup_priority::IO; }
void PCA9505GPIOPin::setup() { this->pin_mode(this->flags_); }
void PCA9505GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool PCA9505GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA9505GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA9505GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "%u via PCA9505", this->pin_);
}
} // namespace esphome::pca9505
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::pca9505 {
/// Number of I/O pins on the PCA9505 and PCA9506.
static constexpr uint8_t PIN_COUNT = 40;
/// The pins are grouped into five ports of eight, each with its own set of registers.
static constexpr uint8_t BANK_COUNT = PIN_COUNT / 8;
class PCA9505Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void setup() override;
void loop() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool read_bank_(uint8_t bank);
bool write_bank_(uint8_t reg, uint8_t bank, uint8_t value);
bool write_all_banks_(uint8_t reg, uint8_t value);
/// The pin config per bank - 1 means OUTPUT, 0 means INPUT
uint8_t config_[BANK_COUNT]{};
/// The output state per bank - 1 means HIGH, 0 means LOW
uint8_t output_[BANK_COUNT]{};
/// The input pin states per bank - 1 means HIGH, 0 means LOW
uint8_t input_[BANK_COUNT]{};
/// Which pins may pull INT low - 1 means masked, matching the chip
uint8_t mask_[BANK_COUNT]{};
InternalGPIOPin *interrupt_pin_{nullptr};
};
class PCA9505GPIOPin final : public GPIOPin, public Parented<PCA9505Component> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
gpio::Flags get_flags() const override { return this->flags_; }
protected:
uint8_t pin_{0};
bool inverted_{false};
gpio::Flags flags_{};
};
} // namespace esphome::pca9505
+1 -1
View File
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = cv.ensure_list(
async def to_code(config: list[ConfigType]) -> None:
cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
cg.add_library("wjtje/qr-code-generator-library", "1.7.0")
for entry in config:
var = cg.new_Pvariable(entry[CONF_ID])
+1
View File
@@ -6,4 +6,5 @@ CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
DEPRECATED_COMPONENT = """
The 'qspi_dbi' 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.
"""
+3 -1
View File
@@ -165,8 +165,10 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'qspi_dbi' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'qspi_dbi' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+2 -1
View File
@@ -139,7 +139,8 @@ void QspiDbi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
if (w <= 0 || h <= 0)
return;
if (bitness != display::COLOR_BITNESS_565 || order != this->color_mode_ ||
big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST) ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
} else if (this->draw_from_origin_) {
+2 -1
View File
@@ -8,12 +8,13 @@
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include "esp_lcd_panel_rgb.h"
namespace esphome::qspi_dbi {
constexpr static const char *const TAG = "display.qspi_dbi";
ESPHOME_LOG_TAG(TAG, "display.qspi_dbi");
static const uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13;
@@ -4,4 +4,5 @@ DOMAIN = "rpi_dpi_rgb"
DEPRECATED_COMPONENT = """
The 'rpi_dpi_rgb' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_rgb' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -132,8 +132,10 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'rpi_dpi_rgb' component is deprecated, it is recommended to use 'mipi_rgb' instead."
"The 'rpi_dpi_rgb' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_rgb' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+10 -5
View File
@@ -51,13 +51,19 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (w <= 0 || h <= 0)
return;
// if color mapping is required, pass the buck.
// note that endianness is not considered here - it is assumed to match!
if (bitness != display::COLOR_BITNESS_565) {
// Big endian RGB565 at rotation 0 copies as is; anything else, or a block drawn under a clipping region, goes
// per pixel
if (bitness != display::COLOR_BITNESS_565 || order != display::COLOR_ORDER_RGB || !big_endian ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
return;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
void RpiDpiRgb::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset,
int y_offset, int x_pad) {
x_start += this->offset_x_;
y_start += this->offset_y_;
esp_err_t err = ESP_OK;
@@ -122,8 +128,7 @@ void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
}
auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color));
this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true,
0, 0, 0);
this->write_to_display_(x, y, 1, 1, (const uint8_t *) &pixel, 0, 0, 0);
this->feed_wdt_per_pixel_();
}
+3 -1
View File
@@ -16,7 +16,7 @@
namespace esphome::rpi_dpi_rgb {
constexpr static const char *const TAG = "rpi_dpi_rgb";
ESPHOME_LOG_TAG(TAG, "rpi_dpi_rgb");
class RpiDpiRgb final : public display::Display {
public:
@@ -62,6 +62,8 @@ class RpiDpiRgb final : public display::Display {
void dump_config() override;
protected:
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
void reset_display_() const;
+11 -2
View File
@@ -14,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@glmnet", "@ximex"]
DOMAIN = "rtttl"
CONF_RTTTL = "rtttl"
# The player keeps the song length in 16 bits
MAX_SONG_LENGTH = 65535
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
rtttl_ns = cg.esphome_ns.namespace("rtttl")
@@ -97,16 +99,23 @@ async def to_code(config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
def _static_song(config: ConfigType, value: str) -> str:
# A constant song plays straight from static storage, flash on ESP8266, with no copy per play
return f"esphome::rtttl::StaticSong{{{cg.FlashStringLiteral(value)}}}"
automation.register_apply_action(
"rtttl.play",
cv.maybe_simple_value(
{
cv.GenerateID(CONF_ID): cv.use_id(Rtttl),
cv.Required(CONF_RTTTL): cv.templatable(cv.string),
cv.Required(CONF_RTTTL): cv.templatable(
cv.All(cv.string, cv.ByteLength(max=MAX_SONG_LENGTH))
),
},
key=CONF_RTTTL,
),
automation.ApplyField(CONF_RTTTL, "play", cg.std_string),
automation.ApplyField(CONF_RTTTL, "play", cg.std_string, const_fn=_static_song),
)
automation.register_apply_action(
+82 -24
View File
@@ -1,5 +1,8 @@
#include "rtttl.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -147,14 +150,14 @@ void Rtttl::loop() {
#endif // USE_SPEAKER
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
while (this->position_ < this->rtttl_.length()) {
char c = this->rtttl_[this->position_];
while (this->position_ < this->song_len_) {
char c = this->song_at_(this->position_);
if (c != ',' && c != ' ')
break;
this->position_++;
}
if (this->position_ >= this->rtttl_.length()) {
if (this->position_ >= this->song_len_) {
this->finish_();
return;
}
@@ -169,12 +172,12 @@ void Rtttl::loop() {
this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_;
}
uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]);
uint8_t note_index_in_octave = note_index_from_char(this->song_at_(this->position_));
this->position_++;
// Now, get optional '#' sharp
if (this->rtttl_[this->position_] == '#') {
if (this->song_at_(this->position_) == '#') {
note_index_in_octave++;
this->position_++;
}
@@ -192,7 +195,7 @@ void Rtttl::loop() {
}
// Now, get optional '.' dotted note
if (this->rtttl_[this->position_] == '.') {
if (this->song_at_(this->position_) == '.') {
this->note_duration_ += this->note_duration_ / 2; // Duration +50%
this->position_++;
}
@@ -264,15 +267,68 @@ void Rtttl::loop() {
}
void Rtttl::play(std::string rtttl) {
if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) {
size_t pos = this->rtttl_.find(':');
size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
if (this->is_busy_()) {
return;
}
this->owned_song_ = std::move(rtttl);
this->set_song_(this->owned_song_.data(), this->owned_song_.size());
this->start_();
}
this->rtttl_ = std::move(rtttl);
void Rtttl::play(StaticSong song) {
if (this->is_busy_()) {
return;
}
// Release a previously owned song; the static one needs no storage.
std::string().swap(this->owned_song_);
const char *data = reinterpret_cast<const char *>(song.rtttl);
this->set_song_(data, ESPHOME_strlen_P(data));
this->start_();
}
void Rtttl::set_song_(const char *song, size_t len) {
this->song_ = song;
// Anything past what the 16 bit length holds is dropped; the validator keeps configured songs shorter.
this->song_len_ = static_cast<uint16_t>(std::min<size_t>(len, std::numeric_limits<uint16_t>::max()));
}
bool Rtttl::is_busy_() const {
if (this->state_ == State::STOPPED || this->state_ == State::STOPPING) {
return false;
}
char name[SONG_NAME_LENGTH_LIMIT + 1];
this->song_name_(name, sizeof(name), this->song_find_(':', 0, this->song_len_));
ESP_LOGW(TAG, "Already playing: %s", name);
return true;
}
size_t Rtttl::song_find_(char c, size_t from, size_t end) const {
for (size_t pos = from; pos < end; pos++) {
if (this->song_at_(pos) == c)
return pos;
}
return end;
}
size_t Rtttl::song_find_control_(char key, size_t from, size_t end) const {
size_t pos = from;
while ((pos = this->song_find_(key, pos, end)) < end) {
if (this->song_at_(pos + 1) == '=')
return pos;
pos++;
}
return end;
}
void Rtttl::song_name_(char *buf, size_t size, size_t name_len) const {
size_t len = std::min(name_len, size - 1);
for (size_t pos = 0; pos < len; pos++) {
buf[pos] = this->song_at_(pos);
}
buf[len] = '\0';
}
void Rtttl::start_() {
this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR;
this->default_octave_ = DEFAULT_OCTAVE;
this->note_duration_ = 0;
@@ -281,9 +337,9 @@ void Rtttl::play(std::string rtttl) {
uint16_t num; // Used for: default note-denominator, default octave, BPM
// Get name
this->position_ = this->rtttl_.find(':');
this->position_ = this->song_find_(':', 0, this->song_len_);
if (this->position_ == std::string::npos) {
if (this->position_ == this->song_len_) {
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
return;
}
@@ -292,18 +348,20 @@ void Rtttl::play(std::string rtttl) {
static_cast<unsigned>(SONG_NAME_LENGTH_LIMIT));
return;
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
char name[SONG_NAME_LENGTH_LIMIT + 1];
this->song_name_(name, sizeof(name), this->position_);
ESP_LOGD(TAG, "Playing song %s", name);
size_t name_end_position = this->position_;
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
if (control_end == std::string::npos) {
size_t control_end = this->song_find_(':', name_end_position + 1, this->song_len_);
if (control_end == this->song_len_) {
ESP_LOGE(TAG, "Missing second ':'");
return;
}
// Get default duration
size_t pos = this->rtttl_.find("d=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
size_t pos = this->song_find_control_('d', name_end_position, control_end);
if (pos == control_end) {
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
} else {
this->position_ = pos + 2;
@@ -317,8 +375,8 @@ void Rtttl::play(std::string rtttl) {
}
// Get default octave
pos = this->rtttl_.find("o=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
pos = this->song_find_control_('o', name_end_position, control_end);
if (pos == control_end) {
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
} else {
this->position_ = pos + 2;
@@ -332,8 +390,8 @@ void Rtttl::play(std::string rtttl) {
}
// Get BPM
pos = this->rtttl_.find("b=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
pos = this->song_find_control_('b', name_end_position, control_end);
if (pos == control_end) {
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
} else {
this->position_ = pos + 2;
@@ -387,7 +445,7 @@ void Rtttl::stop() {
}
#endif // USE_SPEAKER
this->position_ = this->rtttl_.length();
this->position_ = this->song_len_;
this->note_duration_ = 0;
}
@@ -411,7 +469,7 @@ void Rtttl::finish_() {
#endif // USE_SPEAKER
// Ensure no more notes are played in case finish_() is called for an error.
this->position_ = this->rtttl_.length();
this->position_ = this->song_len_;
this->note_duration_ = 0;
}
+53 -16
View File
@@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#ifdef USE_OUTPUT
#include "esphome/components/output/float_output.h"
@@ -14,6 +15,10 @@
namespace esphome::rtttl {
namespace testing {
class RtttlParserAccess;
} // namespace testing
inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note)
inline constexpr uint8_t DEFAULT_OCTAVE =
6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`)
@@ -26,6 +31,11 @@ enum class State : uint8_t {
STOPPING,
};
/// A song in storage that outlives playback, such as a literal from generated code; PROGMEM on ESP8266.
struct StaticSong {
ProgmemStr rtttl;
};
class Rtttl final : public Component {
public:
#ifdef USE_OUTPUT
@@ -38,7 +48,10 @@ class Rtttl final : public Component {
void dump_config() override;
void loop() override;
/// Play a song; the string is copied so it may come from anywhere.
void play(std::string rtttl);
/// Play a song from static storage without copying it.
void play(StaticSong song);
void stop();
float get_gain() { return this->gain_; }
@@ -55,11 +68,29 @@ class Rtttl final : public Component {
protected:
inline uint16_t get_integer_() {
uint16_t ret = 0;
while (isdigit(this->rtttl_[this->position_])) {
ret = (ret * 10) + (this->rtttl_[this->position_++] - '0');
while (isdigit(this->song_at_(this->position_))) {
ret = (ret * 10) + (this->song_at_(this->position_++) - '0');
}
return ret;
}
/// The byte of the song at pos, or NUL past its end; the read is flash safe for a PROGMEM song on ESP8266.
char song_at_(size_t pos) const {
if (pos >= this->song_len_)
return '\0';
return static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(this->song_) + pos));
}
/// Index of the first `c` in [from, end), or `end` when absent.
size_t song_find_(char c, size_t from, size_t end) const;
/// Index of `key` followed by '=' in [from, end), or `end` when absent.
size_t song_find_control_(char key, size_t from, size_t end) const;
/// Copies the first name_len bytes of the song, its name, into buf for logging.
void song_name_(char *buf, size_t size, size_t name_len) const;
/// Points the view at a song, clamped to what song_len_ can hold.
void set_song_(const char *song, size_t len);
/// Parses the header and starts playback of the song the view points at.
void start_();
/// Logs and returns true when a song is still playing, so a new one must wait.
bool is_busy_() const;
/**
* @brief Finalizes the playback of the RTTTL string.
*
@@ -70,26 +101,30 @@ class Rtttl final : public Component {
void finish_();
void set_state_(State state);
/// The RTTTL string to play.
std::string rtttl_;
/// The current position in the RTTTL string.
size_t position_{0};
/// The default duration of a note (e.g. 4 for a quarter note).
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
/// The default octave for a note.
uint8_t default_octave_{DEFAULT_OCTAVE};
/// The duration of the current note in milliseconds.
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The current state of the RTTTL player.
State state_{State::STOPPED};
/// The song being played; points at owned_song_ or at static storage.
const char *song_{nullptr};
/// Backing store for play(std::string).
std::string owned_song_;
/// The time in milliseconds since microcontroller boot when the last note was started.
uint32_t last_note_start_time_;
/// The frequency of the current note in Hz.
uint32_t output_freq_{0};
/// The gain of the output.
float gain_{0.6f};
/// Length of the song in bytes; songs are a few hundred bytes, so 16 bits is ample.
uint16_t song_len_{0};
/// The current position in the song.
uint16_t position_{0};
/// The duration of the current note in milliseconds.
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The default duration of a note (e.g. 4 for a quarter note).
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
/// The default octave for a note.
uint8_t default_octave_{DEFAULT_OCTAVE};
/// The current state of the RTTTL player.
State state_{State::STOPPED};
#ifdef USE_OUTPUT
/// The output to write the sound to.
@@ -113,6 +148,8 @@ class Rtttl final : public Component {
/// The callback to call when playback is finished.
CallbackManager<void()> on_finished_playback_callback_;
#endif
friend class testing::RtttlParserAccess;
};
} // namespace esphome::rtttl
+1 -1
View File
@@ -115,7 +115,7 @@ class PNGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_PNG")
cg.add_library("pngle", "1.1.0")
cg.add_library("kikuchan98/pngle", "1.1.0")
class QOIFormat(Format):
@@ -30,6 +30,6 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
}
uint8_t *ImageDecoder::image_buffer_() const { return this->image_->buffer_; }
bool ImageDecoder::image_is_big_endian_() const { return this->image_->is_big_endian_; }
bool ImageDecoder::image_is_big_endian_() const { return this->image_->big_endian_; }
} // namespace esphome::runtime_image
@@ -41,12 +41,11 @@ inline bool is_color_on(const Color &color) {
RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
: Image(nullptr, 0, 0, type, transparency),
: Image(nullptr, 0, 0, type, transparency, is_big_endian),
format_(format),
fixed_width_(fixed_width),
fixed_height_(fixed_height),
placeholder_(placeholder),
is_big_endian_(is_big_endian) {}
placeholder_(placeholder) {}
RuntimeImage::~RuntimeImage() { this->release(); }
@@ -137,7 +136,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
Color mapped_color = color;
this->map_chroma_key(mapped_color);
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
if (this->is_big_endian_) {
if (this->big_endian_) {
this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
} else {
@@ -227,11 +227,6 @@ class RuntimeImage : public image::Image {
// Configuration
bool progressive_display_{false};
/**
* Whether the image is stored in big-endian format.
* This is used to determine how to store 16 bit colors in the buffer.
*/
bool is_big_endian_{false};
/** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */
bool external_buffer_{false};
@@ -15,7 +15,7 @@ class Component; // Forward declaration
namespace runtime_stats {
static const char *const TAG = "runtime_stats";
ESPHOME_LOG_TAG(TAG, "runtime_stats");
class RuntimeStatsCollector final {
public:
+1 -1
View File
@@ -13,7 +13,7 @@
namespace esphome::sdl {
constexpr static const char *const TAG = "sdl";
ESPHOME_LOG_TAG(TAG, "sdl");
class Sdl final : public display::Display, public snapshot::Snapshot {
public:
+109 -2
View File
@@ -14,6 +14,15 @@ static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID is active at a time.
static constexpr uint32_t TIMEOUT_POLL = 1;
static constexpr uint32_t TIMEOUT_SETUP_STEP = 2;
static constexpr uint32_t TIMEOUT_STARTUP = 3;
// Post-command wait times from the datasheet: start execution (4.8.1), stop execution (4.8.2),
// fan cleaning (4.8.22), SHT heater (4.8.23), CO2 conditioning after start on SEN63C/SEN69C (4.8.1)
static constexpr uint32_t START_MEASUREMENT_DELAY = 50;
static constexpr uint32_t STOP_MEASUREMENT_DELAY = 1400;
static constexpr uint32_t FAN_CLEANING_DELAY = 10000;
static constexpr uint32_t SHT_HEATER_DELAY = 20000;
static constexpr uint32_t CO2_CONDITIONING_DELAY = 24000;
static constexpr uint32_t STARTUP_DELAY = 60000; // ms before published values are considered valid
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
@@ -27,6 +36,9 @@ static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
static constexpr uint16_t SEN6X_CMD_STOP_MEASUREMENTS = 0x0104;
static constexpr uint16_t SEN6X_CMD_START_FAN_CLEANING = 0x5607;
static constexpr uint16_t SEN6X_CMD_ACTIVATE_SHT_HEATER = 0x6765;
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
@@ -191,7 +203,7 @@ void SEN6XComponent::finish_setup_() {
return;
}
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
this->on_measurement_started_();
this->initialized_ = true;
ESP_LOGD(TAG, "Initialized");
}
@@ -240,7 +252,7 @@ void SEN6XComponent::dump_config() {
}
void SEN6XComponent::update() {
if (!this->initialized_) {
if (!this->initialized_ || !this->measuring_) {
return;
}
@@ -442,6 +454,101 @@ void SEN6XComponent::parse_and_publish_measurements_() {
this->status_clear_warning();
}
// True while the previous command's execution or settling time has not elapsed
bool SEN6XComponent::command_blocked_() const { return millis() - this->command_started_at_ < this->command_wait_; }
void SEN6XComponent::set_command_wait_(uint32_t wait_ms) {
this->command_started_at_ = millis();
this->command_wait_ = wait_ms;
}
void SEN6XComponent::on_measurement_started_() {
this->measuring_ = true;
this->set_command_wait_(START_MEASUREMENT_DELAY);
// SEN63C/SEN69C condition the CO2 sensor for 24 s after a start; block restarts until then
if (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN69C) {
this->co2_started_at_ = millis();
this->co2_wait_ = CO2_CONDITIONING_DELAY;
}
// Values need the warm-up period again after every start
this->startup_complete_ = false;
this->set_timeout(TIMEOUT_STARTUP, STARTUP_DELAY, [this]() { this->startup_complete_ = true; });
}
void SEN6XComponent::start_measurement() {
if (!this->initialized_ || this->measuring_)
return;
// The CO2 conditioning window only blocks restarting, not other commands
if (this->command_blocked_() || millis() - this->co2_started_at_ < this->co2_wait_) {
ESP_LOGW(TAG, "Device busy");
return;
}
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
this->status_set_warning();
ESP_LOGW(TAG, "Start measurement failed (%d)", this->last_error_);
return;
}
this->on_measurement_started_();
}
void SEN6XComponent::stop_measurement() {
if (!this->initialized_ || !this->measuring_)
return;
if (this->command_blocked_()) {
ESP_LOGW(TAG, "Device busy");
return;
}
// Stop any in-flight polling before the device goes idle
this->cancel_timeout(TIMEOUT_POLL);
if (!this->write_command(SEN6X_CMD_STOP_MEASUREMENTS)) {
this->status_set_warning();
ESP_LOGW(TAG, "Stop measurement failed (%d)", this->last_error_);
return;
}
this->measuring_ = false;
this->set_command_wait_(STOP_MEASUREMENT_DELAY);
}
void SEN6XComponent::start_fan_cleaning() {
if (!this->initialized_)
return;
// The device only accepts this command in idle mode (datasheet section 4.8.22)
if (this->measuring_) {
ESP_LOGW(TAG, "Fan cleaning requires idle mode; stop the measurement first");
return;
}
if (this->command_blocked_()) {
ESP_LOGW(TAG, "Device busy");
return;
}
if (!this->write_command(SEN6X_CMD_START_FAN_CLEANING)) {
this->status_set_warning();
ESP_LOGW(TAG, "Fan cleaning failed (%d)", this->last_error_);
return;
}
this->set_command_wait_(FAN_CLEANING_DELAY);
}
void SEN6XComponent::activate_sht_heater() {
if (!this->initialized_)
return;
// The device only accepts this command in idle mode (datasheet section 4.8.23)
if (this->measuring_) {
ESP_LOGW(TAG, "SHT heater requires idle mode; stop the measurement first");
return;
}
if (this->command_blocked_()) {
ESP_LOGW(TAG, "Device busy");
return;
}
if (!this->write_command(SEN6X_CMD_ACTIVATE_SHT_HEATER)) {
this->status_set_warning();
ESP_LOGW(TAG, "SHT heater failed (%d)", this->last_error_);
return;
}
this->set_command_wait_(SHT_HEATER_DELAY);
}
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
if (product_name == "SEN62")
return SEN62;
+13
View File
@@ -58,6 +58,10 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
NOX_STD_INITIAL,
gain_factor};
}
void start_measurement();
void stop_measurement();
void start_fan_cleaning();
void activate_sht_heater();
protected:
Sen6xType infer_type_from_product_name_(const std::string &product_name);
@@ -65,6 +69,9 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
void finish_setup_();
bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len);
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool command_blocked_() const;
void set_command_wait_(uint32_t wait_ms);
void on_measurement_started_();
void poll_data_ready_();
void read_measurements_();
void parse_and_publish_measurements_();
@@ -74,6 +81,11 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
optional<GasTuning> voc_tuning_params_;
optional<GasTuning> nox_tuning_params_;
Sen6xType sen6x_type_{UNKNOWN};
// Post-command wait windows, stored as start time plus duration so elapsed-time checks survive millis() wrap
uint32_t command_started_at_{0};
uint32_t command_wait_{0};
uint32_t co2_started_at_{0};
uint32_t co2_wait_{0};
uint16_t read_cmd_{0};
uint8_t setup_step_index_{0};
uint8_t firmware_version_major_{0};
@@ -81,6 +93,7 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
uint8_t poll_retries_remaining_{0};
uint8_t read_words_{0};
bool initialized_{false};
bool measuring_{false};
bool startup_complete_{false};
};
+20
View File
@@ -1,3 +1,5 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
@@ -209,3 +211,21 @@ async def to_code(config: ConfigType) -> None:
args.append(std_initial)
args.append(tuning[CONF_GAIN_FACTOR])
cg.add(getattr(var, setter)(*args))
SEN6X_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(SEN6XComponent),
}
)
for _name, _call in (
("sen6x.start_measurement", "start_measurement()"),
("sen6x.stop_measurement", "stop_measurement()"),
("sen6x.start_fan_cleaning", "start_fan_cleaning()"),
("sen6x.activate_sht_heater", "activate_sht_heater()"),
):
automation.register_apply_action(
_name, SEN6X_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@@ -7,7 +7,7 @@
namespace esphome::spi_led_strip {
static const char *const TAG = "spi_led_strip";
ESPHOME_LOG_TAG(TAG, "spi_led_strip");
class SpiLedStrip final : public light::AddressableLight,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
+10 -5
View File
@@ -54,13 +54,19 @@ void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (w <= 0 || h <= 0)
return;
// if color mapping is required, pass the buck.
// note that endianness is not considered here - it is assumed to match!
if (bitness != display::COLOR_BITNESS_565) {
// Big endian RGB565 at rotation 0 copies as is; anything else, or a block drawn under a clipping region, goes
// per pixel
if (bitness != display::COLOR_BITNESS_565 || order != display::COLOR_ORDER_RGB || !big_endian ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
return;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
void ST7701S::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad) {
x_start += this->offset_x_;
y_start += this->offset_y_;
esp_err_t err;
@@ -105,8 +111,7 @@ void ST7701S::draw_pixel_at(int x, int y, Color color) {
}
auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color));
this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true,
0, 0, 0);
this->write_to_display_(x, y, 1, 1, (const uint8_t *) &pixel, 0, 0, 0);
this->feed_wdt_per_pixel_();
}
+4 -1
View File
@@ -8,13 +8,14 @@
#include "esphome/core/component.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
#include "esphome/core/log.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
namespace esphome::st7701s {
constexpr static const char *const TAG = "display.st7701s";
ESPHOME_LOG_TAG(TAG, "display.st7701s");
const uint8_t SW_RESET_CMD = 0x01;
const uint8_t SLEEP_OUT = 0x11;
const uint8_t SDIR_CMD = 0xC7;
@@ -77,6 +78,8 @@ class ST7701S final : public display::Display,
// this will be horribly slow.
protected:
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
void write_command_(uint8_t value);
void write_data_(uint8_t value);
void write_sequence_(uint8_t cmd, size_t len, const uint8_t *bytes);
+1
View File
@@ -7,4 +7,5 @@ st7735_ns = cg.esphome_ns.namespace("st7735")
DEPRECATED_COMPONENT = """
The 'st7735' 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.
"""
+3 -1
View File
@@ -92,8 +92,10 @@ async def setup_st7735(var: MockObj, config: ConfigType) -> None:
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'st7735' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
var = cg.new_Pvariable(
config[CONF_ID],
+1
View File
@@ -7,4 +7,5 @@ st7789v_ns = cg.esphome_ns.namespace("st7789v")
DEPRECATED_COMPONENT = """
The 'st7789v' component is deprecated and no new functionality will be added to it.
PRs should target the newer and more performant 'mipi_spi' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -183,8 +183,10 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'st7789v' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'st7789v' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+14
View File
@@ -17,12 +17,22 @@ CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_ENABLE_ADC = "enable_adc"
CONF_I2C_WATCHDOG_TIMEOUT = "i2c_watchdog_timeout"
sy6970_ns = cg.esphome_ns.namespace("sy6970")
SY6970Component = sy6970_ns.class_(
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
)
SY6970Listener = sy6970_ns.class_("SY6970Listener")
I2CWatchdogTimeout = sy6970_ns.enum("I2CWatchdogTimeout")
# REG07[5:4]; the component kicks the watchdog itself, so the chip's 40S default can stay on
I2C_WATCHDOG_TIMEOUTS = {
"DISABLED": I2CWatchdogTimeout.I2C_WATCHDOG_DISABLED,
"40S": I2CWatchdogTimeout.I2C_WATCHDOG_40S,
"80S": I2CWatchdogTimeout.I2C_WATCHDOG_80S,
"160S": I2CWatchdogTimeout.I2C_WATCHDOG_160S,
}
CONFIG_SCHEMA = (
cv.Schema(
@@ -43,6 +53,9 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
cv.Optional(CONF_I2C_WATCHDOG_TIMEOUT, default="40S"): cv.enum(
I2C_WATCHDOG_TIMEOUTS, upper=True
),
}
)
.extend(cv.polling_component_schema("5s"))
@@ -60,6 +73,7 @@ async def to_code(config: ConfigType) -> None:
config[CONF_PRECHARGE_CURRENT],
config[CONF_CHARGE_ENABLED],
config[CONF_ENABLE_ADC],
config[CONF_I2C_WATCHDOG_TIMEOUT],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

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