Compare commits

..

64 Commits

Author SHA1 Message Date
Jonathan Swoboda
42811edeb4 Merge pull request #12293 from esphome/bump-2025.11.4
2025.11.4
2025-12-04 22:54:55 -05:00
Jonathan Swoboda
8f20abebf6 Bump version to 2025.11.4 2025-12-04 21:52:48 -05:00
J. Nick Koston
7077488dc7 [scheduler] Fix use-after-free when cancelling timeouts from non-main-loop threads (#12288) 2025-12-04 21:52:48 -05:00
Jesse Hills
ef34239064 [CI] Trigger generic version notifier job on release (#12292) 2025-12-04 21:52:48 -05:00
Jonathan Swoboda
44148c0c6b [esp32_hosted] Fix build and bump IDF component version to 2.7.0 (#12282)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:52:48 -05:00
Jonathan Swoboda
1b53fcf634 [es8311] Remove MIN and MAX from mic_gain enum options (#12281)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:52:48 -05:00
Clyde Stubbs
b18e3d943a [config] Provide path for has_at_most_one_of messages (#12277) 2025-12-04 21:52:48 -05:00
Jonathan Swoboda
f0673f6304 [ld2420] Add missing USE_SELECT ifdefs (#12275)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:52:48 -05:00
Clyde Stubbs
320ba30d50 [esp32] Add build flag to suppress noexecstack message (#12272) 2025-12-04 21:52:48 -05:00
Jonathan Swoboda
cfd88376b9 Merge pull request #12266 from esphome/bump-2025.11.3
2025.11.3
2025-12-03 11:36:57 -05:00
Jonathan Swoboda
577a6b2941 Bump version to 2025.11.3 2025-12-03 10:50:28 -05:00
Jonathan Swoboda
de68b56c4a [rtl87xx] Fix FreeRTOS version for RTL8720C boards (#12261)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
Jonathan Swoboda
ccd23e692b [analog_threshold] Fix oscillation when using invert filter (#12251)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
Jonathan Swoboda
1f5a44be3d [rtl87xx] Fix AsyncTCP compilation by upgrading FreeRTOS to 8.2.3 (#12230)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
Jonathan Swoboda
1d1e47c757 [core] Fix clean all windows (#12217)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-03 10:50:28 -05:00
3fbed1fa79 [ade7953] Apply voltage_gain setting to both channels (#12180) 2025-12-03 10:50:28 -05:00
Jonathan Swoboda
5c71520635 [mopeka_pro_check] Fix negative temperatures (#12198)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
J. Nick Koston
9d6c81ec23 [hlk_fm22x] Fix Action::play method signatures (#12192) 2025-12-03 10:50:28 -05:00
Clyde Stubbs
73fa9230e6 [helpers] Add conversion from FixedVector to std::vector (#12179) 2025-12-03 10:50:28 -05:00
J. Nick Koston
48caff13c9 [espnow] Initialize LwIP stack when running without WiFi component (#12169) 2025-12-03 10:50:28 -05:00
J. Nick Koston
71bb94524e [usb_uart] Wake main loop immediately when USB data arrives (#12148) 2025-12-03 10:50:28 -05:00
Clyde Stubbs
a3199792c6 [build] Don't clear pio cache unless requested (#11966) 2025-12-03 10:50:28 -05:00
Jonathan Swoboda
50c1720c16 Merge pull request #12149 from esphome/bump-2025.11.2
2025.11.2
2025-11-27 18:19:05 -05:00
Jonathan Swoboda
4115dd7222 Bump version to 2025.11.2 2025-11-27 17:23:28 -05:00
J. Nick Koston
d5e2543751 [scheduler] Fix use-after-move crash in heap operations (#12124) 2025-11-27 17:23:28 -05:00
Clyde Stubbs
b4b34aee13 [wifi] Restore blocking setup until connected for RP2040 (#12142) 2025-11-27 17:23:28 -05:00
Jonathan Swoboda
6645994700 [esp32] Fix hosted update when there is no wifi (#12123) 2025-11-27 17:23:28 -05:00
Clyde Stubbs
ae140f52e3 [lvgl] Fix position of errors in widget config (#12111)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-27 17:23:28 -05:00
Clyde Stubbs
46ae6d35a2 [lvgl] Allow multiple widgets per grid cell (#12091) 2025-11-27 17:23:27 -05:00
J. Nick Koston
278f12fb99 [script] Fix script.wait hanging when triggered from on_boot (#12102) 2025-11-27 17:23:27 -05:00
Jonathan Swoboda
acdcd56395 [esp32] Fix platformio flash size print (#12099)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-27 17:23:27 -05:00
Edward Firmo
9289fc36f7 [nextion] Do not set alternative baud rate when not specified or <= 0 (#12097) 2025-11-27 17:23:27 -05:00
Jonathan Swoboda
3775b54554 Merge pull request #12086 from esphome/bump-2025.11.1
2025.11.1
2025-11-24 17:29:53 -05:00
Jonathan Swoboda
9186144dcd Bump version to 2025.11.1 2025-11-24 16:24:38 -05:00
Jesse Hills
25bcd0ea25 [online_image] Fix some large PNGs causing watchdog timeout (#12025)
Co-authored-by: guillempages <guillempages@users.noreply.github.com>
2025-11-24 16:24:38 -05:00
J. Nick Koston
50d08a2eba [esp_ldo,mipi_dsi,mipi_rgb] Fix dangling pointer bugs in mark_failed() (#12077) 2025-11-24 16:24:38 -05:00
J. Nick Koston
3a7a0c66ab [script][wait_until] Fix FIFO ordering and reentrancy bugs (#12049)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 16:24:38 -05:00
Jonathan Swoboda
83525b7a92 [core] Add support for passing yaml files to clean-all (#12039) 2025-11-24 16:24:38 -05:00
Jonathan Swoboda
f31f023c89 [esp32] Fix C2 builds (#12050) 2025-11-24 16:24:37 -05:00
J. Nick Koston
f8efefffaa [cst816][http_request] Fix status_set_error() dangling pointer bugs (#12033) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
d698083ede [jsn_sr04t] Fix model AJ_SR04M (#11992) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
11ba6440d7 [cst816][packet_transport][udp][wake_on_lan] Fix error messages (#12019) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
89ee37a2d5 [ltr501][ltr_als_ps] Rename enum to avoid collision with lwip defines (#12017) 2025-11-24 16:24:37 -05:00
J. Nick Koston
45b8c1e267 [network] Fix IPAddress constructor causing comparison failures and garbage output (#12005) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
fbe091f167 [graph] Fix legend border (#12000) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
625172e07d Merge pull request #12004 from esphome/bump-2025.11.0
2025.11.0
2025-11-19 17:37:42 -05:00
Jonathan Swoboda
1e9c7d3c6d Bump version to 2025.11.0 2025-11-19 16:02:52 -05:00
Jonathan Swoboda
c2bc7b3cdc Merge pull request #12003 from esphome/bump-2025.11.0b5
2025.11.0b5
2025-11-19 15:06:44 -05:00
Jonathan Swoboda
c75abfb894 Bump version to 2025.11.0b5 2025-11-19 14:17:03 -05:00
Jesse Hills
1157b4aee8 [epaper_spi] Add basic 7.3in-Spectra-E6 model (#12001) 2025-11-19 14:17:03 -05:00
J. Nick Koston
71dc2d374d [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) 2025-11-19 14:17:03 -05:00
Jonathan Swoboda
0a224f919b [wifi] Fix positive RSSI values on 8266 (#11994) 2025-11-19 14:17:03 -05:00
Jonathan Swoboda
7ef4b4f3d9 [text_sensor] Fix infinite loop in substitute filter (#11989)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-19 14:17:03 -05:00
J. Nick Koston
13b875c763 [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) 2025-11-19 14:17:03 -05:00
Jesse Hills
dfd614c00c Merge pull request #11980 from esphome/bump-2025.11.0b4
2025.11.0b4
2025-11-19 13:22:09 +13:00
Jesse Hills
2681a14d05 Bump version to 2025.11.0b4 2025-11-19 09:17:33 +13:00
J. Nick Koston
f436f6ee2e [wifi] Fix captive portal unusable when WiFi credentials are wrong (#11965) 2025-11-19 09:17:33 +13:00
Jonathan Swoboda
f18bc62690 [sfa30] Fix negative temperature values (#11973) 2025-11-19 09:17:33 +13:00
J. Nick Koston
6db73df649 [scheduler] Add defensive nullptr checks and explicit locking requirements (#11974) 2025-11-19 09:17:33 +13:00
Jonathan Swoboda
93215f1737 [esp32] Fix Arduino build on some ESP32 S2 boards (#11972) 2025-11-19 09:17:33 +13:00
Clyde Stubbs
70aa94b8a4 [lvgl] Apply scale to spinbox value (#11946) 2025-11-19 09:17:33 +13:00
strange_v
e8998a79c7 [mipi_rgb] Fix GUITION-4848S040 colors (#11709) 2025-11-19 09:17:33 +13:00
Jonathan Swoboda
3b25fdbc5f [core] Add support for setting environment variables (#11953) 2025-11-19 09:17:33 +13:00
J. Nick Koston
6c8577678c [captive_portal] Warn when enabled without WiFi AP configured (#11856) 2025-11-19 09:17:33 +13:00
134 changed files with 1650 additions and 1097 deletions

View File

@@ -22,7 +22,7 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate a token
id: generate-token

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0

View File

@@ -43,7 +43,7 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -36,7 +36,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -91,7 +91,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -132,7 +132,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -183,7 +183,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -237,7 +237,7 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python 3.13
id: python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
@@ -273,7 +273,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -321,7 +321,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -400,7 +400,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -489,7 +489,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -577,7 +577,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -662,7 +662,7 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -688,7 +688,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.base_ref }}
@@ -840,7 +840,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -908,7 +908,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,7 +60,7 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
@@ -92,7 +92,7 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
@@ -219,10 +219,19 @@ jobs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
github-token: ${{ steps.generate-token.outputs.token }}
script: |
let description = "ESPHome";
if (context.eventName == "release") {
@@ -245,10 +254,19 @@ jobs:
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
@@ -259,3 +277,34 @@ jobs:
version: "${{ needs.init.outputs.tag }}",
}
})
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

View File

@@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Checkout Home Assistant
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: home-assistant/core
path: lib/home-assistant

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.4
hooks:
# Run the linter.
- id: ruff

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.12.0-dev
PROJECT_NUMBER = 2025.11.4
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -1319,7 +1319,7 @@ def parse_args(argv):
"clean-all", help="Clean all build and platform files."
)
parser_clean_all.add_argument(
"configuration", help="Your YAML configuration directory.", nargs="*"
"configuration", help="Your YAML file or configuration directory.", nargs="*"
)
parser_dashboard = subparsers.add_parser(

View File

@@ -25,7 +25,8 @@ void ADE7953::setup() {
this->ade_write_8(PGA_V_8, pga_v_);
this->ade_write_8(PGA_IA_8, pga_ia_);
this->ade_write_8(PGA_IB_8, pga_ib_);
this->ade_write_32(AVGAIN_32, vgain_);
this->ade_write_32(AVGAIN_32, avgain_);
this->ade_write_32(BVGAIN_32, bvgain_);
this->ade_write_32(AIGAIN_32, aigain_);
this->ade_write_32(BIGAIN_32, bigain_);
this->ade_write_32(AWGAIN_32, awgain_);
@@ -34,7 +35,8 @@ void ADE7953::setup() {
this->ade_read_8(PGA_V_8, &pga_v_);
this->ade_read_8(PGA_IA_8, &pga_ia_);
this->ade_read_8(PGA_IB_8, &pga_ib_);
this->ade_read_32(AVGAIN_32, &vgain_);
this->ade_read_32(AVGAIN_32, &avgain_);
this->ade_read_32(BVGAIN_32, &bvgain_);
this->ade_read_32(AIGAIN_32, &aigain_);
this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_);
@@ -63,13 +65,14 @@ void ADE7953::dump_config() {
" PGA_V_8: 0x%X\n"
" PGA_IA_8: 0x%X\n"
" PGA_IB_8: 0x%X\n"
" VGAIN_32: 0x%08jX\n"
" AVGAIN_32: 0x%08jX\n"
" BVGAIN_32: 0x%08jX\n"
" AIGAIN_32: 0x%08jX\n"
" BIGAIN_32: 0x%08jX\n"
" AWGAIN_32: 0x%08jX\n"
" BWGAIN_32: 0x%08jX",
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_,
(uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_,
(uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
}
#define ADE_PUBLISH_(name, val, factor) \

View File

@@ -46,7 +46,12 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; }
// Set input gains
void set_vgain(uint32_t vgain) { vgain_ = vgain; }
void set_vgain(uint32_t vgain) {
// Datasheet says: "to avoid discrepancies in other registers,
// if AVGAIN is set then BVGAIN should be set to the same value."
avgain_ = vgain;
bvgain_ = vgain;
}
void set_aigain(uint32_t aigain) { aigain_ = aigain; }
void set_bigain(uint32_t bigain) { bigain_ = bigain; }
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
@@ -100,7 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint8_t pga_v_;
uint8_t pga_ia_;
uint8_t pga_ib_;
uint32_t vgain_;
uint32_t avgain_;
uint32_t bvgain_;
uint32_t aigain_;
uint32_t bigain_;
uint32_t awgain_;

View File

@@ -12,10 +12,11 @@ void AnalogThresholdBinarySensor::setup() {
// TRUE state is defined to be when sensor is >= threshold
// so when undefined sensor value initialize to FALSE
if (std::isnan(sensor_value)) {
this->raw_state_ = false;
this->publish_initial_state(false);
} else {
this->publish_initial_state(sensor_value >=
(this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f;
this->publish_initial_state(this->raw_state_);
}
}
@@ -25,8 +26,10 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
this->sensor_->add_on_state_callback([this](float sensor_value) {
// if there is an invalid sensor reading, ignore the change and keep the current state
if (!std::isnan(sensor_value)) {
this->publish_state(sensor_value >=
(this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
// Use raw_state_ for hysteresis logic, not this->state which is post-filter
this->raw_state_ =
sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value());
this->publish_state(this->raw_state_);
}
});
}

View File

@@ -20,6 +20,7 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
};
} // namespace analog_threshold

View File

@@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000;
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) {
auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) {
this->helper_ =
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else {

View File

@@ -527,7 +527,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
const auto &psk = this->ctx_.get_psk();
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED);

View File

@@ -9,8 +9,9 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -40,8 +41,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;

View File

@@ -227,8 +227,8 @@ void APIServer::dump_config() {
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
}
#else
@@ -493,7 +493,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGW(TAG, "Key set in YAML");
return false;
#else
auto &old_psk = this->noise_ctx_.get_psk();
auto &old_psk = this->noise_ctx_->get_psk();
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
ESP_LOGW(TAG, "New PSK matches old");
return true;

View File

@@ -54,8 +54,8 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
@@ -228,7 +228,7 @@ class APIServer : public Component, public Controller {
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE
APINoiseContext noise_ctx_;
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
};

View File

@@ -1,11 +1,12 @@
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
static const char *const TAG = "binary_sensor.automation";
void MultiClickTrigger::on_state_(bool state) {
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events
if (state == this->last_state_) {
return;
@@ -66,7 +67,7 @@ void MultiClickTrigger::on_state_(bool state) {
*this->at_index_ = *this->at_index_ + 1;
}
void MultiClickTrigger::schedule_cooldown_() {
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
this->is_in_cooldown_ = true;
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
@@ -78,7 +79,7 @@ void MultiClickTrigger::schedule_cooldown_() {
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
}
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
if (min_length == 0) {
this->is_valid_ = true;
return;
@@ -89,19 +90,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
this->is_valid_ = true;
});
}
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout("is_not_valid", max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
}
void MultiClickTrigger::cancel() {
void binary_sensor::MultiClickTrigger::cancel() {
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
this->is_valid_ = false;
this->schedule_cooldown_();
}
void MultiClickTrigger::trigger_() {
void binary_sensor::MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset();
this->cancel_timeout("trigger");
@@ -117,4 +118,5 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
return length >= min_length && length <= max_length;
}
}
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -9,7 +9,8 @@
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
struct MultiClickTriggerEvent {
bool state;
@@ -171,4 +172,5 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
BinarySensor *sensor_;
};
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -3,7 +3,9 @@
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
static const char *const TAG = "binary_sensor";
@@ -61,4 +63,6 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
}
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -6,7 +6,9 @@
#include <initializer_list>
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
class BinarySensor;
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
@@ -68,4 +70,5 @@ class BinarySensorInitiallyOff : public BinarySensor {
bool has_state() const override { return true; }
};
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -2,7 +2,9 @@
#include "binary_sensor.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
static const char *const TAG = "sensor.filter";
@@ -130,4 +132,6 @@ optional<bool> SettleFilter::new_value(bool value) {
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -4,7 +4,9 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
class BinarySensor;
@@ -137,4 +139,6 @@ class SettleFilter : public Filter, public Component {
bool steady_{true};
};
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -70,9 +70,6 @@ void BME68xBSEC2Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_,
this->bme68x_status_);
if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) {
ESP_LOGE(TAG, "No sensors, add at least one sensor to the config");
}
}
if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) {

View File

@@ -4,7 +4,8 @@
#include "esphome/core/automation.h"
#include "cover.h"
namespace esphome::cover {
namespace esphome {
namespace cover {
template<typename... Ts> class OpenAction : public Action<Ts...> {
public:
@@ -130,4 +131,5 @@ class CoverClosedTrigger : public Trigger<> {
}
};
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/core/log.h"
namespace esphome::cover {
namespace esphome {
namespace cover {
static const char *const TAG = "cover";
@@ -211,4 +212,5 @@ void CoverRestoreState::apply(Cover *cover) {
cover->publish_state();
}
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -7,7 +7,8 @@
#include "cover_traits.h"
namespace esphome::cover {
namespace esphome {
namespace cover {
const extern float COVER_OPEN;
const extern float COVER_CLOSED;
@@ -156,4 +157,5 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
ESPPreferenceObject rtc_;
};
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -1,6 +1,7 @@
#pragma once
namespace esphome::cover {
namespace esphome {
namespace cover {
class CoverTraits {
public:
@@ -25,4 +26,5 @@ class CoverTraits {
bool supports_stop_{false};
};
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -19,8 +19,9 @@ void CST816Touchscreen::continue_setup_() {
case CST816T_CHIP_ID:
break;
default:
ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_);
this->status_set_error("Unknown chip ID");
this->mark_failed();
this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str());
return;
}
this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION);

View File

@@ -3,10 +3,10 @@
namespace esphome {
namespace dashboard_import {
static const char *g_package_import_url = ""; // NOLINT
static std::string g_package_import_url; // NOLINT
const char *get_package_import_url() { return g_package_import_url; }
void set_package_import_url(const char *url) { g_package_import_url = url; }
const std::string &get_package_import_url() { return g_package_import_url; }
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
} // namespace dashboard_import
} // namespace esphome

View File

@@ -1,10 +1,12 @@
#pragma once
#include <string>
namespace esphome {
namespace dashboard_import {
const char *get_package_import_url();
void set_package_import_url(const char *url);
const std::string &get_package_import_url();
void set_package_import_url(std::string url);
} // namespace dashboard_import
} // namespace esphome

View File

@@ -102,7 +102,7 @@ def customise_schema(config):
"""
config = cv.Schema(
{
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"),
},
extra=cv.ALLOW_EXTRA,
)(config)

View File

@@ -32,11 +32,15 @@ class SpectraE6(EpaperModel):
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6.extend(
"Seeed-reTerminal-E1002",
spectra_e6_7p3 = spectra_e6.extend(
"7.3in-Spectra-E6",
width=800,
height=480,
data_rate="20MHz",
)
spectra_e6_7p3.extend(
"Seeed-reTerminal-E1002",
cs_pin=10,
dc_pin=11,
reset_pin=12,

View File

@@ -22,7 +22,6 @@ ES8311_BITS_PER_SAMPLE_ENUM = {
es8311_mic_gain = es8311_ns.enum("ES8311MicGain")
ES8311_MIC_GAIN_ENUM = {
"MIN": es8311_mic_gain.ES8311_MIC_GAIN_MIN,
"0DB": es8311_mic_gain.ES8311_MIC_GAIN_0DB,
"6DB": es8311_mic_gain.ES8311_MIC_GAIN_6DB,
"12DB": es8311_mic_gain.ES8311_MIC_GAIN_12DB,
@@ -31,7 +30,6 @@ ES8311_MIC_GAIN_ENUM = {
"30DB": es8311_mic_gain.ES8311_MIC_GAIN_30DB,
"36DB": es8311_mic_gain.ES8311_MIC_GAIN_36DB,
"42DB": es8311_mic_gain.ES8311_MIC_GAIN_42DB,
"MAX": es8311_mic_gain.ES8311_MIC_GAIN_MAX,
}

View File

@@ -854,8 +854,13 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_build_flag("-Wl,-z,noexecstack")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
@@ -883,6 +888,12 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",

View File

@@ -0,0 +1,9 @@
Import("env") # noqa: F821
# Remove custom_sdkconfig from the board config as it causes
# pioarduino to enable some strange hybrid build mode that breaks IDF
board = env.BoardConfig()
if "espidf.custom_sdkconfig" in board:
del board._manifest["espidf"]["custom_sdkconfig"]
if not board._manifest["espidf"]:
del board._manifest["espidf"]

View File

@@ -634,13 +634,11 @@ void ESP32BLE::dump_config() {
io_capability_s = "invalid";
break;
}
char mac_s[18];
format_mac_addr_upper(mac_address, mac_s);
ESP_LOGCONFIG(TAG,
"BLE:\n"
" MAC address: %s\n"
" IO Capability: %s",
mac_s, io_capability_s);
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
} else {
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
}

View File

@@ -93,9 +93,9 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.7.0")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")

View File

@@ -22,6 +22,11 @@ constexpr size_t CHUNK_SIZE = 1500;
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// if wifi is not present, connect to the coprocessor
#ifndef USE_WIFI
esp_hosted_connect_to_slave(); // NOLINT
#endif
// get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {

View File

@@ -14,8 +14,8 @@ void EspLdo::setup() {
config.flags.adjustable = this->adjustable_;
auto err = esp_ldo_acquire_channel(&config, &this->handle_);
if (err != ESP_OK) {
auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
this->mark_failed(msg.c_str());
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
this->mark_failed("Failed to acquire LDO channel");
} else {
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_);
}

View File

@@ -10,6 +10,7 @@
#include <esp_event.h>
#include <esp_mac.h>
#include <esp_netif.h>
#include <esp_now.h>
#include <esp_random.h>
#include <esp_wifi.h>
@@ -157,6 +158,12 @@ bool ESPNowComponent::is_wifi_enabled() {
}
void ESPNowComponent::setup() {
#ifndef USE_WIFI
// Initialize LwIP stack for wake_loop_threadsafe() socket support
// When WiFi component is present, it handles esp_netif_init()
ESP_ERROR_CHECK(esp_netif_init());
#endif
if (this->enable_on_boot_) {
this->enable_();
} else {

View File

@@ -36,6 +36,7 @@ from esphome.const import (
CONF_WEIGHT,
)
from esphome.core import CORE, HexInt
from esphome.helpers import cpp_string_escape
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -49,6 +50,7 @@ font_ns = cg.esphome_ns.namespace("font")
Font = font_ns.class_("Font")
Glyph = font_ns.class_("Glyph")
GlyphData = font_ns.struct("GlyphData")
CONF_BPP = "bpp"
CONF_EXTRAS = "extras"
@@ -461,7 +463,7 @@ FONT_SCHEMA = cv.Schema(
)
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph),
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
},
)
@@ -486,8 +488,6 @@ class GlyphInfo:
def glyph_to_glyphinfo(glyph, font, size, bpp):
# Convert to 32 bit unicode codepoint
glyph = ord(glyph)
scale = 256 // (1 << bpp)
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
@@ -583,15 +583,22 @@ async def to_code(config):
# Create the glyph table that points to data in the above array.
glyph_initializer = [
[
x.glyph,
prog_arr + (y - len(x.bitmap_data)),
x.advance,
x.offset_x,
x.offset_y,
x.width,
x.height,
]
cg.StructInitializer(
GlyphData,
(
"a_char",
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
),
(
"data",
cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
),
("advance", x.advance),
("offset_x", x.offset_x),
("offset_y", x.offset_y),
("width", x.width),
("height", x.height),
)
for (x, y) in zip(
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
)

View File

@@ -6,245 +6,133 @@
namespace esphome {
namespace font {
static const char *const TAG = "font";
#ifdef USE_LVGL_FONT
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(unicode_letter);
if (gd == nullptr) {
return nullptr;
const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; }
// Compare the char at the string position with this char.
// Return true if this char is less than or equal the other.
bool Glyph::compare_to(const uint8_t *str) const {
// 1 -> this->char_
// 2 -> str
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return true;
if (str[i] == '\0')
return false;
if (this->glyph_data_->a_char[i] > str[i])
return false;
if (this->glyph_data_->a_char[i] < str[i])
return true;
}
return gd->data;
// this should not happen
return false;
}
int Glyph::match_length(const uint8_t *str) const {
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return i;
if (str[i] != this->glyph_data_->a_char[i])
return 0;
}
// this should not happen
return 0;
}
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*x1 = this->glyph_data_->offset_x;
*y1 = this->glyph_data_->offset_y;
*width = this->glyph_data_->width;
*height = this->glyph_data_->height;
}
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
auto *fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(unicode_letter);
if (gd == nullptr) {
return false;
}
dsc->adv_w = gd->advance;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line;
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->get_bpp();
return true;
}
const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) {
if (unicode_letter == this->last_letter_ && this->last_letter_ != 0)
return this->last_data_;
auto *glyph = this->find_glyph(unicode_letter);
if (glyph == nullptr) {
return nullptr;
}
this->last_data_ = glyph;
this->last_letter_ = unicode_letter;
return glyph;
}
#endif
/**
* Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string.
* If successful, return the codepoint and set the length to the number of bytes read.
* If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to
* 0.
*
* @param utf8_str The input string
* @param length Pointer to length storage
* @return The extracted code point
*/
static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
// Safely cast to uint8_t* for correct bitwise operations on bytes
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
uint32_t code_point = 0;
uint8_t c1 = *current++;
// check for end of string
if (c1 == 0) {
*length = 0;
return 0;
}
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
if (c1 < 0x80) {
// Valid ASCII byte.
code_point = c1;
// Optimization: No need to check for continuation bytes.
}
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
else if ((c1 & 0xE0) == 0xC0) {
uint8_t c2 = *current++;
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
if ((c2 & 0xC0) != 0x80) {
*length = 0;
return 0;
}
code_point = (c1 & 0x1F) << 6;
code_point |= (c2 & 0x3F);
// Error Check 2: Overlong check (2-byte must be > 0x7F)
if (code_point <= 0x7F) {
*length = 0;
return 0;
}
}
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
else if ((c1 & 0xF0) == 0xE0) {
uint8_t c2 = *current++;
uint8_t c3 = *current++;
// Error Check 1: Check continuation bytes
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
*length = 0;
return 0;
}
code_point = (c1 & 0x0F) << 12;
code_point |= (c2 & 0x3F) << 6;
code_point |= (c3 & 0x3F);
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
// Also check for surrogates (0xD800-0xDFFF)
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
*length = 0;
return 0;
}
}
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
else if ((c1 & 0xF8) == 0xF0) {
uint8_t c2 = *current++;
uint8_t c3 = *current++;
uint8_t c4 = *current++;
// Error Check 1: Check continuation bytes
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
*length = 0;
return 0;
}
code_point = (c1 & 0x07) << 18;
code_point |= (c2 & 0x3F) << 12;
code_point |= (c3 & 0x3F) << 6;
code_point |= (c4 & 0x3F);
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
// Also check for valid Unicode range (must be <= 0x10FFFF)
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
*length = 0;
return 0;
}
}
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
else {
*length = 0;
return 0;
}
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
return code_point;
}
Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
uint8_t bpp)
: glyphs_(ConstVector(data, data_nr)),
baseline_(baseline),
: baseline_(baseline),
height_(height),
descender_(descender),
linegap_(height - baseline - descender),
xheight_(xheight),
capheight_(capheight),
bpp_(bpp) {
#ifdef USE_LVGL_FONT
this->lv_font_.dsc = this;
this->lv_font_.line_height = this->get_height();
this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline();
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
this->lv_font_.underline_position = -1;
this->lv_font_.underline_thickness = 1;
#endif
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);
}
const Glyph *Font::find_glyph(uint32_t codepoint) const {
int Font::match_next_glyph(const uint8_t *str, int *match_length) {
int lo = 0;
int hi = this->glyphs_.size() - 1;
while (lo != hi) {
int mid = (lo + hi + 1) / 2;
if (this->glyphs_[mid].is_less_or_equal(codepoint)) {
if (this->glyphs_[mid].compare_to(str)) {
lo = mid;
} else {
hi = mid - 1;
}
}
auto *result = &this->glyphs_[lo];
if (result->code_point == codepoint)
return result;
return nullptr;
*match_length = this->glyphs_[lo].match_length(str);
if (*match_length <= 0)
return -1;
return lo;
}
#ifdef USE_DISPLAY
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
int i = 0;
int min_x = 0;
bool has_char = false;
int x = 0;
for (;;) {
size_t length;
auto code_point = extract_unicode_codepoint(str, &length);
if (length == 0)
break;
str += length;
auto *glyph = this->find_glyph(code_point);
if (glyph == nullptr) {
while (str[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
if (!this->glyphs_.empty())
x += this->glyphs_[0].advance;
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->advance;
i++;
continue;
}
const Glyph &glyph = this->glyphs_[glyph_n];
if (!has_char) {
min_x = glyph->offset_x;
min_x = glyph.glyph_data_->offset_x;
} else {
min_x = std::min(min_x, x + glyph->offset_x);
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph->advance;
x += glyph.glyph_data_->advance;
i += match_length;
has_char = true;
}
*x_offset = min_x;
*width = x - min_x;
}
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) {
int i = 0;
int x_at = x_start;
for (;;) {
size_t length;
auto code_point = extract_unicode_codepoint(text, &length);
if (length == 0)
break;
text += length;
auto *glyph = this->find_glyph(code_point);
if (glyph == nullptr) {
int scan_x1, scan_y1, scan_width, scan_height;
while (text[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
if (!this->glyphs_.empty()) {
uint8_t glyph_width = this->glyphs_[0].advance;
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!this->get_glyphs().empty()) {
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
i++;
continue;
}
const uint8_t *data = glyph->data;
const int max_x = x_at + glyph->offset_x + glyph->width;
const int max_y = y_start + glyph->offset_y + glyph->height;
const Glyph &glyph = this->get_glyphs()[glyph_n];
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
const uint8_t *data = glyph.glyph_data_->data;
const int max_x = x_at + scan_x1 + scan_width;
const int max_y = y_start + scan_y1 + scan_height;
uint8_t bitmask = 0;
uint8_t pixel_data = 0;
@@ -257,10 +145,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
auto b_g = (float) background.g;
auto b_b = (float) background.b;
auto b_w = (float) background.w;
for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) {
for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) {
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
uint8_t pixel = 0;
for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) {
for (int bit_num = 0; bit_num != this->bpp_; bit_num++) {
if (bitmask == 0) {
pixel_data = progmem_read_byte(data++);
bitmask = 0x80;
@@ -280,9 +168,12 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
}
}
}
x_at += glyph->advance;
x_at += glyph.glyph_data_->advance;
i += match_length;
}
}
#endif
} // namespace font
} // namespace esphome

View File

@@ -6,30 +6,14 @@
#ifdef USE_DISPLAY
#include "esphome/components/display/display.h"
#endif
#ifdef USE_LVGL_FONT
#include <lvgl.h>
#endif
namespace esphome {
namespace font {
class Font;
class Glyph {
public:
constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
int height)
: code_point(code_point),
data(data),
advance(advance),
offset_x(offset_x),
offset_y(offset_y),
width(width),
height(height) {}
bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; }
const uint32_t code_point;
struct GlyphData {
const uint8_t *a_char;
const uint8_t *data;
int advance;
int offset_x;
@@ -38,6 +22,26 @@ class Glyph {
int height;
};
class Glyph {
public:
Glyph(const GlyphData *data) : glyph_data_(data) {}
const uint8_t *get_char() const;
bool compare_to(const uint8_t *str) const;
int match_length(const uint8_t *str) const;
void scan_area(int *x1, int *y1, int *width, int *height) const;
const GlyphData *get_glyph_data() const { return this->glyph_data_; }
protected:
friend Font;
const GlyphData *glyph_data_;
};
class Font
#ifdef USE_DISPLAY
: public display::BaseFont
@@ -46,8 +50,8 @@ class Font
public:
/** Construct the font with the given glyphs.
*
* @param data A list of glyphs, must be sorted lexicographically.
* @param data_nr The number of glyphs
* @param data A vector of glyphs, must be sorted lexicographically.
* @param data_nr The number of glyphs in data.
* @param baseline The y-offset from the top of the text to the baseline.
* @param height The y-offset from the top of the text to the bottom.
* @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
@@ -55,10 +59,10 @@ class Font
* @param capheight The height of capital letters, usually measured at the "X" glyph.
* @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
*/
Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
uint8_t bpp = 1);
const Glyph *find_glyph(uint32_t codepoint) const;
int match_next_glyph(const uint8_t *str, int *match_length);
#ifdef USE_DISPLAY
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
@@ -73,14 +77,11 @@ class Font
inline int get_xheight() { return this->xheight_; }
inline int get_capheight() { return this->capheight_; }
inline int get_bpp() { return this->bpp_; }
#ifdef USE_LVGL_FONT
const lv_font_t *get_lv_font() const { return &this->lv_font_; }
#endif
const ConstVector<Glyph> &get_glyphs() const { return glyphs_; }
const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
ConstVector<Glyph> glyphs_;
std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
int descender_;
@@ -88,14 +89,6 @@ class Font
int xheight_;
int capheight_;
uint8_t bpp_; // bits per pixel
#ifdef USE_LVGL_FONT
lv_font_t lv_font_{};
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
const Glyph *get_glyph_data_(uint32_t unicode_letter);
uint32_t last_letter_{};
const Glyph *last_data_{};
#endif
};
} // namespace font

View File

@@ -337,7 +337,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of
return;
/// Plot border
if (this->border_) {
if (legend_->border_) {
int w = legend_->width_;
int h = legend_->height_;
buff->horizontal_line(x_offset, y_offset, w, color);

View File

@@ -189,7 +189,7 @@ template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public P
TEMPLATABLE_VALUE(std::string, name)
TEMPLATABLE_VALUE(uint8_t, direction)
void play(Ts... x) override {
void play(const Ts &...x) override {
auto name = this->name_.value(x...);
auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...);
this->parent_->enroll_face(name, direction);
@@ -200,7 +200,7 @@ template<typename... Ts> class DeleteAction : public Action<Ts...>, public Paren
public:
TEMPLATABLE_VALUE(int16_t, face_id)
void play(Ts... x) override {
void play(const Ts &...x) override {
auto face_id = this->face_id_.value(x...);
this->parent_->delete_face(face_id);
}
@@ -208,17 +208,17 @@ template<typename... Ts> class DeleteAction : public Action<Ts...>, public Paren
template<typename... Ts> class DeleteAllAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(Ts... x) override { this->parent_->delete_all_faces(); }
void play(const Ts &...x) override { this->parent_->delete_all_faces(); }
};
template<typename... Ts> class ScanAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(Ts... x) override { this->parent_->scan_face(); }
void play(const Ts &...x) override { this->parent_->scan_face(); }
};
template<typename... Ts> class ResetAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(Ts... x) override { this->parent_->reset(); }
void play(const Ts &...x) override { this->parent_->reset(); }
};
} // namespace esphome::hlk_fm22x

View File

@@ -49,18 +49,18 @@ void HttpRequestUpdate::update_task(void *params) {
auto container = this_update->request_parent_->get(this_update->source_url_);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str());
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); });
this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); });
UPDATE_RETURN;
}
RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length);
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); });
this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); });
container->end();
UPDATE_RETURN;
}
@@ -121,9 +121,9 @@ void HttpRequestUpdate::update_task(void *params) {
}
if (!valid) {
std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str());
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); });
this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); });
UPDATE_RETURN;
}

View File

@@ -10,7 +10,7 @@ namespace jsn_sr04t {
static const char *const TAG = "jsn_sr04t.sensor";
void Jsnsr04tComponent::update() {
this->write_byte(0x55);
this->write_byte((this->model_ == AJ_SR04M) ? 0x01 : 0x55);
ESP_LOGV(TAG, "Request read out from sensor");
}
@@ -31,19 +31,10 @@ void Jsnsr04tComponent::loop() {
}
void Jsnsr04tComponent::check_buffer_() {
uint8_t checksum = 0;
switch (this->model_) {
case JSN_SR04T:
checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
break;
case AJ_SR04M:
checksum = this->buffer_[1] + this->buffer_[2];
break;
}
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
if (this->buffer_[3] == checksum) {
uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]);
if (distance > 250) {
if (distance > ((this->model_ == AJ_SR04M) ? 200 : 250)) {
float meters = distance / 1000.0f;
ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters);
this->publish_state(meters);

View File

@@ -13,6 +13,8 @@ namespace esphome {
namespace ld2410 {
static const char *const TAG = "ld2410";
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
@@ -179,15 +181,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
}
void LD2410Component::dump_config() {
char mac_s[18];
char version_s[20];
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
ld24xx::format_version_str(this->version_, version_s);
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGCONFIG(TAG,
"LD2410:\n"
" Firmware version: %s\n"
" MAC address: %s",
version_s, mac_str);
version.c_str(), mac_str.c_str());
#ifdef USE_BINARY_SENSOR
ESP_LOGCONFIG(TAG, "Binary Sensors:");
LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
@@ -446,12 +448,12 @@ bool LD2410Component::handle_ack_data_() {
case CMD_QUERY_VERSION: {
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
char version_s[20];
ld24xx::format_version_str(this->version_, version_s);
ESP_LOGV(TAG, "Firmware version: %s", version_s);
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(version_s);
this->version_text_sensor_->publish_state(version);
}
#endif
break;
@@ -504,9 +506,9 @@ bool LD2410Component::handle_ack_data_() {
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
}
char mac_s[18];
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
ESP_LOGV(TAG, "MAC address: %s", mac_str);
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(mac_str);

View File

@@ -14,6 +14,8 @@ namespace esphome {
namespace ld2412 {
static const char *const TAG = "ld2412";
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
@@ -198,15 +200,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
}
void LD2412Component::dump_config() {
char mac_s[18];
char version_s[20];
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
ld24xx::format_version_str(this->version_, version_s);
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGCONFIG(TAG,
"LD2412:\n"
" Firmware version: %s\n"
" MAC address: %s",
version_s, mac_str);
version.c_str(), mac_str.c_str());
#ifdef USE_BINARY_SENSOR
ESP_LOGCONFIG(TAG, "Binary Sensors:");
LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus",
@@ -490,12 +492,12 @@ bool LD2412Component::handle_ack_data_() {
case CMD_QUERY_VERSION: {
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
char version_s[20];
ld24xx::format_version_str(this->version_, version_s);
ESP_LOGV(TAG, "Firmware version: %s", version_s);
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(version_s);
this->version_text_sensor_->publish_state(version);
}
#endif
break;
@@ -542,9 +544,9 @@ bool LD2412Component::handle_ack_data_() {
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
}
char mac_s[18];
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
ESP_LOGV(TAG, "MAC address: %s", mac_str);
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(mac_str);

View File

@@ -205,8 +205,10 @@ void LD2420Component::dump_config() {
LOG_BUTTON(" ", "Factory Reset:", this->factory_reset_button_);
LOG_BUTTON(" ", "Restart Module:", this->restart_module_button_);
#endif
#ifdef USE_SELECT
ESP_LOGCONFIG(TAG, "Select:");
LOG_SELECT(" ", "Operating Mode", this->operating_selector_);
#endif
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
}
@@ -238,12 +240,20 @@ void LD2420Component::setup() {
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
this->set_operating_mode(OP_SIMPLE_MODE_STRING);
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr) {
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
}
#endif
this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
} else {
this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr) {
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
}
#endif
}
#ifdef USE_NUMBER
this->init_gate_config_numbers();
@@ -383,8 +393,12 @@ void LD2420Component::set_operating_mode(const char *state) {
// If unsupported firmware ignore mode select
if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) {
this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state);
// Entering Auto Calibrate we need to clear the privoiuos data collection
this->operating_selector_->publish_state(state);
// Entering Auto Calibrate we need to clear the previous data collection
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr) {
this->operating_selector_->publish_state(state);
}
#endif
if (current_operating_mode == OP_CALIBRATE_MODE) {
this->set_calibration_(true);
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
@@ -404,7 +418,11 @@ void LD2420Component::set_operating_mode(const char *state) {
}
} else {
this->current_operating_mode = OP_SIMPLE_MODE;
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr) {
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
}
#endif
}
}

View File

@@ -17,6 +17,8 @@ namespace esphome {
namespace ld2450 {
static const char *const TAG = "ld2450";
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
@@ -190,15 +192,15 @@ void LD2450Component::setup() {
}
void LD2450Component::dump_config() {
char mac_s[18];
char version_s[20];
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
ld24xx::format_version_str(this->version_, version_s);
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGCONFIG(TAG,
"LD2450:\n"
" Firmware version: %s\n"
" MAC address: %s",
version_s, mac_str);
version.c_str(), mac_str.c_str());
#ifdef USE_BINARY_SENSOR
ESP_LOGCONFIG(TAG, "Binary Sensors:");
LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
@@ -640,12 +642,12 @@ bool LD2450Component::handle_ack_data_() {
case CMD_QUERY_VERSION: {
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
char version_s[20];
ld24xx::format_version_str(this->version_, version_s);
ESP_LOGV(TAG, "Firmware version: %s", version_s);
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(version_s);
this->version_text_sensor_->publish_state(version);
}
#endif
break;
@@ -661,9 +663,9 @@ bool LD2450Component::handle_ack_data_() {
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
}
char mac_s[18];
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
ESP_LOGV(TAG, "MAC address: %s", mac_str);
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(mac_str);

View File

@@ -1,12 +1,11 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <memory>
#include <span>
#ifdef USE_SENSOR
#include "esphome/core/helpers.h"
#include "esphome/components/sensor/sensor.h"
#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \
@@ -40,27 +39,6 @@
namespace esphome {
namespace ld24xx {
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
// Helper function to format MAC address with stack allocation
// Returns pointer to UNKNOWN_MAC constant or formatted buffer
// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator)
inline const char *format_mac_str(const uint8_t *mac_address, std::span<char, 18> buffer) {
if (mac_address_is_valid(mac_address)) {
format_mac_addr_upper(mac_address, buffer.data());
return buffer.data();
}
return UNKNOWN_MAC;
}
// Helper function to format firmware version with stack allocation
// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety)
inline void format_version_str(const uint8_t *version, std::span<char, 20> buffer) {
snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3],
version[2]);
}
#ifdef USE_SENSOR
// Helper class to store a sensor with a deduplicator & publish state only when the value changes
template<typename T> class SensorWithDedup {

View File

@@ -174,7 +174,7 @@ void LTRAlsPs501Component::loop() {
break;
case State::WAITING_FOR_DATA:
if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) {
if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) {
tries = 0;
ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms",
get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time));
@@ -379,18 +379,18 @@ void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time)
}
}
DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) {
LtrDataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) {
AlsPsStatusRegister als_status{0};
als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
if (!als_status.als_new_data)
return DataAvail::NO_DATA;
return LtrDataAvail::LTR_NO_DATA;
ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain));
if (data.gain != als_status.gain) {
ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain));
return DataAvail::BAD_DATA;
return LtrDataAvail::LTR_BAD_DATA;
}
data.gain = als_status.gain;
return DataAvail::DATA_OK;
return LtrDataAvail::LTR_DATA_OK;
}
void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) {

View File

@@ -11,7 +11,7 @@
namespace esphome {
namespace ltr501 {
enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK };
enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK };
enum LtrType : uint8_t {
LTR_TYPE_UNKNOWN = 0,
@@ -106,7 +106,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice {
void configure_als_();
void configure_integration_time_(IntegrationTime501 time);
void configure_gain_(AlsGain501 gain);
DataAvail is_als_data_ready_(AlsReadings &data);
LtrDataAvail is_als_data_ready_(AlsReadings &data);
void read_sensor_data_(AlsReadings &data);
bool are_adjustments_required_(AlsReadings &data);
void apply_lux_calculation_(AlsReadings &data);

View File

@@ -165,7 +165,7 @@ void LTRAlsPsComponent::loop() {
break;
case State::WAITING_FOR_DATA:
if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) {
if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) {
tries = 0;
ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
get_itime_ms(this->als_readings_.integration_time));
@@ -376,23 +376,23 @@ void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) {
}
}
DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) {
LtrDataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) {
AlsPsStatusRegister als_status{0};
als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
if (!als_status.als_new_data)
return DataAvail::NO_DATA;
return LtrDataAvail::LTR_NO_DATA;
if (als_status.data_invalid) {
ESP_LOGW(TAG, "Data available but not valid");
return DataAvail::BAD_DATA;
return LtrDataAvail::LTR_BAD_DATA;
}
ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain));
if (data.gain != als_status.gain) {
ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain));
return DataAvail::BAD_DATA;
return LtrDataAvail::LTR_BAD_DATA;
}
return DataAvail::DATA_OK;
return LtrDataAvail::LTR_DATA_OK;
}
void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) {

View File

@@ -11,7 +11,7 @@
namespace esphome {
namespace ltr_als_ps {
enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK };
enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK };
enum LtrType : uint8_t {
LTR_TYPE_UNKNOWN = 0,
@@ -106,7 +106,7 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice {
void configure_als_();
void configure_integration_time_(IntegrationTime time);
void configure_gain_(AlsGain gain);
DataAvail is_als_data_ready_(AlsReadings &data);
LtrDataAvail is_als_data_ready_(AlsReadings &data);
void read_sensor_data_(AlsReadings &data);
bool are_adjustments_required_(AlsReadings &data);
void apply_lux_calculation_(AlsReadings &data);

View File

@@ -52,7 +52,15 @@ from .schemas import (
from .styles import add_top_layer, styles_to_code, theme_to_code
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import add_on_boot_triggers, generate_triggers
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
from .types import (
FontEngine,
IdleTrigger,
PlainTrigger,
lv_font_t,
lv_group_t,
lv_style_t,
lvgl_ns,
)
from .widgets import (
LvScrActType,
Widget,
@@ -236,6 +244,7 @@ async def to_code(configs):
cg.add_global(lvgl_ns.using)
for font in helpers.esphome_fonts_used:
await cg.get_variable(font)
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
default_font = config_0[df.CONF_DEFAULT_FONT]
if not lvalid.is_lv_font(default_font):
add_define(
@@ -247,8 +256,7 @@ async def to_code(configs):
type=lv_font_t.operator("ptr").operator("const"),
)
cg.new_variable(
globfont_id,
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
globfont_id, MockObj(await lvalid.lv_font.process(default_font))
)
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:

View File

@@ -0,0 +1,76 @@
#include "lvgl_esphome.h"
#ifdef USE_LVGL_FONT
namespace esphome {
namespace lvgl {
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return nullptr;
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
return gd->data;
}
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return false;
dsc->adv_w = gd->advance;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->bpp;
return true;
}
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
this->bpp = esp_font->get_bpp();
this->lv_font_.dsc = this;
this->lv_font_.line_height = this->height = esp_font->get_height();
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
this->lv_font_.underline_position = -1;
this->lv_font_.underline_thickness = 1;
}
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
if (unicode_letter == last_letter_)
return this->last_data_;
uint8_t unicode[5];
memset(unicode, 0, sizeof unicode);
if (unicode_letter > 0xFFFF) {
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[3] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7FF) {
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[2] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7F) {
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
unicode[1] = 0x80 + (unicode_letter & 0x3F);
} else {
unicode[0] = unicode_letter;
}
int match_length;
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
if (glyph_n < 0)
return nullptr;
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
this->last_letter_ = unicode_letter;
return this->last_data_;
}
} // namespace lvgl
} // namespace esphome
#endif // USES_LVGL_FONT

View File

@@ -36,6 +36,8 @@ from .defines import (
)
from .lv_validation import padding, size
CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell"
cell_alignments = LV_CELL_ALIGNMENTS.one_of
grid_alignments = LV_GRID_ALIGNMENTS.one_of
flex_alignments = LV_FLEX_ALIGNMENTS.one_of
@@ -220,6 +222,7 @@ class GridLayout(Layout):
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
cv.Optional(CONF_PAD_ROW): padding,
cv.Optional(CONF_PAD_COLUMN): padding,
cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean,
},
{
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
@@ -263,6 +266,7 @@ class GridLayout(Layout):
# should be guaranteed to be a dict at this point
assert isinstance(layout, dict)
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False)
rows = len(layout[CONF_GRID_ROWS])
columns = len(layout[CONF_GRID_COLUMNS])
used_cells = [[None] * columns for _ in range(rows)]
@@ -299,7 +303,10 @@ class GridLayout(Layout):
f"exceeds grid size {rows}x{columns}",
[CONF_WIDGETS, index],
)
if used_cells[row + i][column + j] is not None:
if (
not allow_multiple
and used_cells[row + i][column + j] is not None
):
raise cv.Invalid(
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
[CONF_WIDGETS, index],

View File

@@ -493,7 +493,6 @@ class LvFont(LValidator):
return LV_FONTS
if is_lv_font(value):
return lv_builtin_font(value)
add_lv_use("font")
fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval)
return requires_component("font")(fontval)
@@ -503,9 +502,7 @@ class LvFont(LValidator):
async def process(self, value, args=()):
if is_lv_font(value):
return literal(f"&lv_font_{value}")
if isinstance(value, str):
return literal(f"{value}")
return await super().process(value, args)
return literal(f"{value}_engine->get_lv_font()")
lv_font = LvFont()

View File

@@ -50,14 +50,6 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
#endif // LV_COLOR_DEPTH
#ifdef USE_LVGL_FONT
inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) {
lv_obj_set_style_text_font(obj, font->get_lv_font(), part);
}
inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
lv_style_set_text_font(style, font->get_lv_font());
}
#endif
#ifdef USE_LVGL_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
@@ -142,6 +134,24 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
protected:
std::function<void(Ts...)> lamb_;
};
#ifdef USE_LVGL_FONT
class FontEngine {
public:
FontEngine(font::Font *esp_font);
const lv_font_t *get_lv_font();
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
uint16_t baseline{};
uint16_t height{};
uint8_t bpp{};
protected:
font::Font *font_{};
uint32_t last_letter_{};
const font::GlyphData *last_data_{};
lv_font_t lv_font_{};
};
#endif // USE_LVGL_FONT
#ifdef USE_LVGL_ANIMIMG
void lv_animimg_stop(lv_obj_t *obj);
#endif // USE_LVGL_ANIMIMG

View File

@@ -1,6 +1,7 @@
from esphome import config_validation as cv
from esphome.automation import Trigger, validate_automation
from esphome.components.time import RealTimeClock
from esphome.config_validation import prepend_path
from esphome.const import (
CONF_ARGS,
CONF_FORMAT,
@@ -422,7 +423,10 @@ def any_widget_schema(extras=None):
def validator(value):
if isinstance(value, dict):
# Convert to list
is_dict = True
value = [{k: v} for k, v in value.items()]
else:
is_dict = False
if not isinstance(value, list):
raise cv.Invalid("Expected a list of widgets")
result = []
@@ -443,7 +447,9 @@ def any_widget_schema(extras=None):
)
# Apply custom validation
value = widget_type.validate(value or {})
result.append({key: container_validator(value)})
path = [key] if is_dict else [index, key]
with prepend_path(path):
result.append({key: container_validator(value)})
return result
return validator

View File

@@ -45,6 +45,7 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t")
lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
lv_key_t = cg.global_ns.enum("lv_key_t")
FontEngine = lvgl_ns.class_("FontEngine")
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
DrawEndTrigger = esphome_ns.class_(
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)

View File

@@ -119,7 +119,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
bool has_psk = api::global_api_server->get_noise_ctx().has_psk();
bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
#endif
@@ -135,7 +135,8 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#ifdef USE_DASHBOARD_IMPORT
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url())});
txt_records.push_back(
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
#endif
}
#endif // USE_API

View File

@@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel
xSemaphoreGiveFromISR(sem, &need_yield);
return (need_yield == pdTRUE);
}
void MIPI_DSI::smark_failed(const char *message, esp_err_t err) {
ESP_LOGE(TAG, "%s: %s", message, esp_err_to_name(err));
this->mark_failed(message);
}
void MIPI_DSI::setup() {
ESP_LOGCONFIG(TAG, "Running Setup");

View File

@@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display {
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
void smark_failed(const char *message, esp_err_t err) {
auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err));
this->mark_failed(str.c_str());
}
void smark_failed(const char *message, esp_err_t err);
void update() override;

View File

@@ -164,8 +164,8 @@ void MipiRgb::common_setup_() {
if (err == ESP_OK)
err = esp_lcd_panel_init(this->handle_);
if (err != ESP_OK) {
auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err));
this->mark_failed(msg.c_str());
ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err));
this->mark_failed("lcd setup failed");
}
ESP_LOGCONFIG(TAG, "MipiRgb setup complete");
}

View File

@@ -116,7 +116,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
// Get temperature of sensor
if (this->temperature_ != nullptr) {
uint8_t temp_in_c = this->parse_temperature_(manu_data.data);
int8_t temp_in_c = this->parse_temperature_(manu_data.data);
this->temperature_->publish_state(temp_in_c);
}
@@ -145,7 +145,7 @@ uint32_t MopekaProCheck::parse_distance_(const std::vector<uint8_t> &message) {
(MOPEKA_LPG_COEF[0] + MOPEKA_LPG_COEF[1] * raw_t + MOPEKA_LPG_COEF[2] * raw_t * raw_t));
}
uint8_t MopekaProCheck::parse_temperature_(const std::vector<uint8_t> &message) { return (message[2] & 0x7F) - 40; }
int8_t MopekaProCheck::parse_temperature_(const std::vector<uint8_t> &message) { return (message[2] & 0x7F) - 40; }
SensorReadQuality MopekaProCheck::parse_read_quality_(const std::vector<uint8_t> &message) {
// Since a 8 bit value is being shifted and truncated to 2 bits all possible values are defined as enumeration

View File

@@ -61,7 +61,7 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi
uint8_t parse_battery_level_(const std::vector<uint8_t> &message);
uint32_t parse_distance_(const std::vector<uint8_t> &message);
uint8_t parse_temperature_(const std::vector<uint8_t> &message);
int8_t parse_temperature_(const std::vector<uint8_t> &message);
SensorReadQuality parse_read_quality_(const std::vector<uint8_t> &message);
};

View File

@@ -140,7 +140,7 @@ void MQTTClientComponent::send_device_info_() {
#endif
#ifdef USE_API_NOISE
root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] =
root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
#endif
},

View File

@@ -81,7 +81,12 @@ struct IPAddress {
ip_addr_.type = IPADDR_TYPE_V6;
}
#endif /* LWIP_IPV6 */
IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); }
IPAddress(esp_ip4_addr_t *other_ip) {
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t));
#if LWIP_IPV6
ip_addr_.type = IPADDR_TYPE_V4;
#endif
}
IPAddress(esp_ip_addr_t *other_ip) {
#if LWIP_IPV6
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_));
@@ -118,10 +123,10 @@ struct IPAddress {
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
#endif
bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() { return IP_IS_V4(&ip_addr_); }
bool is_ip6() { return IP_IS_V6(&ip_addr_); }
bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }

View File

@@ -174,6 +174,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// Check if baud rate is supported
this->original_baud_rate_ = this->parent_->get_baud_rate();
if (baud_rate <= 0) {
baud_rate = this->original_baud_rate_;
}
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
// Define the configuration for the HTTP client

View File

@@ -177,6 +177,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// Check if baud rate is supported
this->original_baud_rate_ = this->parent_->get_baud_rate();
if (baud_rate <= 0) {
baud_rate = this->original_baud_rate_;
}
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
// Define the configuration for the HTTP client

View File

@@ -103,7 +103,6 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
CONF_DFU = "dfu"
CONF_DCDC = "dcdc"
CONF_REG0 = "reg0"
CONF_UICR_ERASE = "uicr_erase"
@@ -122,7 +121,6 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
),
cv.Optional(CONF_DCDC, default=True): cv.boolean,
cv.Optional(CONF_REG0): cv.Schema(
{
cv.Required(CONF_VOLTAGE): cv.All(
@@ -198,7 +196,6 @@ async def to_code(config: ConfigType) -> None:
if dfu_config := config.get(CONF_DFU):
CORE.add_job(_dfu_to_code, dfu_config)
zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC])
if reg0_config := config.get(CONF_REG0):
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])

View File

@@ -1,7 +1,8 @@
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number.automation";
@@ -51,4 +52,5 @@ void ValueRangeTrigger::on_state_(float state) {
this->rtc_.save(&in_range);
}
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome::number {
namespace esphome {
namespace number {
class NumberStateTrigger : public Trigger<float> {
public:
@@ -90,4 +91,5 @@ template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...>
float max_{NAN};
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -3,7 +3,8 @@
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number";
@@ -42,4 +43,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
this->state_callback_.add(std::move(callback));
}
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "number_call.h"
#include "number_traits.h"
namespace esphome::number {
namespace esphome {
namespace number {
class Number;
void log_number(const char *tag, const char *prefix, const char *type, Number *obj);
@@ -52,4 +53,5 @@ class Number : public EntityBase {
CallbackManager<void(float)> state_callback_;
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -2,7 +2,8 @@
#include "number.h"
#include "esphome/core/log.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number";
@@ -124,4 +125,5 @@ void NumberCall::perform() {
this->parent_->control(target_value);
}
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome::number {
namespace esphome {
namespace number {
class Number;
@@ -43,4 +44,5 @@ class NumberCall {
bool cycle_;
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -1,8 +1,10 @@
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number";
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -3,7 +3,8 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome::number {
namespace esphome {
namespace number {
enum NumberMode : uint8_t {
NUMBER_MODE_AUTO = 0,
@@ -34,4 +35,5 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas
NumberMode mode_{NUMBER_MODE_AUTO};
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -2,6 +2,7 @@
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color);
// Feed watchdog periodically to avoid triggering during long decode operations.
// Feed every 1024 pixels to balance efficiency and responsiveness.
uint32_t pixels = w * h;
decoder->increment_pixels_decoded(pixels);
if ((decoder->get_pixels_decoded() % 1024) < pixels) {
App.feed_wdt();
}
}
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {

View File

@@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder {
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
uint32_t get_pixels_decoded() const { return this->pixels_decoded_; }
protected:
RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_;
uint32_t pixels_decoded_{0};
};
} // namespace online_image

View File

@@ -195,8 +195,8 @@ static void add(std::vector<uint8_t> &vec, const char *str) {
void PacketTransport::setup() {
this->name_ = App.get_name().c_str();
if (strlen(this->name_) > 255) {
this->mark_failed();
this->status_set_error("Device name exceeds 255 chars");
this->mark_failed();
return;
}
this->resend_ping_key_ = this->ping_pong_enable_;

View File

@@ -6,10 +6,13 @@
# in schema.py file in this directory.
from esphome import pins
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components.libretiny.const import (
COMPONENT_RTL87XX,
FAMILY_RTL8710B,
KEY_COMPONENT_DATA,
KEY_FAMILY,
KEY_LIBRETINY,
LibreTinyComponent,
)
@@ -45,6 +48,11 @@ CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
# Use FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake required by AsyncTCP 3.4.3+
# https://github.com/esphome/esphome/issues/10220
# Only for RTL8710B (ambz) - RTL8720C (ambz2) requires FreeRTOS 10.x
if CORE.data[KEY_LIBRETINY][KEY_FAMILY] == FAMILY_RTL8710B:
cg.add_platformio_option("custom_versions.freertos", "8.2.3")
return await libretiny.component_to_code(config)

View File

@@ -1,8 +1,8 @@
#pragma once
#include <list>
#include <memory>
#include <tuple>
#include <forward_list>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
@@ -278,7 +278,12 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
void setup() override {
// Start with loop disabled - only enable when there's work to do
this->disable_loop();
// IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
// called before our setup() (e.g., from on_boot trigger at same priority level)
// and we must not undo its enable_loop() call
if (this->num_running_ == 0) {
this->disable_loop();
}
}
void play_complex(const Ts &...x) override {
@@ -290,10 +295,10 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
}
// Store parameters for later execution
this->param_queue_.emplace_front(x...);
// Enable loop now that we have work to do
this->param_queue_.emplace_back(x...);
// Enable loop now that we have work to do - don't call loop() synchronously!
// Let the event loop call it to avoid reentrancy issues
this->enable_loop();
this->loop();
}
void loop() override {
@@ -303,13 +308,17 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
if (this->script_->is_running())
return;
while (!this->param_queue_.empty()) {
// Only process ONE queued item per loop iteration
// Processing all items in a while loop causes infinite loops because
// play_next_() can trigger more items to be queued
if (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
this->param_queue_.pop_front();
} else {
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
}
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
}
void play(const Ts &...x) override { /* ignore - see play_complex */
@@ -326,7 +335,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
}
C *script_;
std::forward_list<std::tuple<Ts...>> param_queue_;
std::list<std::tuple<Ts...>> param_queue_;
};
} // namespace script

View File

@@ -66,10 +66,14 @@ SubstituteFilter::SubstituteFilter(const std::initializer_list<Substitution> &su
: substitutions_(substitutions) {}
optional<std::string> SubstituteFilter::new_value(std::string value) {
std::size_t pos;
for (const auto &sub : this->substitutions_) {
while ((pos = value.find(sub.from)) != std::string::npos)
std::size_t pos = 0;
while ((pos = value.find(sub.from, pos)) != std::string::npos) {
value.replace(pos, sub.from.size(), sub.to);
// Advance past the replacement to avoid infinite loop when
// the replacement contains the search pattern (e.g., f -> foo)
pos += sub.to.size();
}
}
return value;
}

View File

@@ -21,8 +21,8 @@ void UDPComponent::setup() {
if (this->should_broadcast_) {
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (this->broadcast_socket_ == nullptr) {
this->mark_failed();
this->status_set_error("Could not create socket");
this->mark_failed();
return;
}
int enable = 1;
@@ -41,15 +41,15 @@ void UDPComponent::setup() {
if (this->should_listen_) {
this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (this->listen_socket_ == nullptr) {
this->mark_failed();
this->status_set_error("Could not create socket");
this->mark_failed();
return;
}
auto err = this->listen_socket_->setblocking(false);
if (err < 0) {
ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno);
this->mark_failed();
this->status_set_error("Unable to set nonblocking");
this->mark_failed();
return;
}
int enable = 1;
@@ -73,8 +73,8 @@ void UDPComponent::setup() {
err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq));
if (err < 0) {
ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno);
this->mark_failed();
this->status_set_error("Failed to set IP_ADD_MEMBERSHIP");
this->mark_failed();
return;
}
}
@@ -82,8 +82,8 @@ void UDPComponent::setup() {
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
this->status_set_error("Unable to bind socket");
this->mark_failed();
return;
}
}

View File

@@ -1,4 +1,5 @@
import esphome.codegen as cg
from esphome.components import socket
from esphome.components.uart import (
CONF_DATA_BITS,
CONF_PARITY,
@@ -17,7 +18,7 @@ from esphome.const import (
)
from esphome.cpp_types import Component
AUTO_LOAD = ["uart", "usb_host", "bytebuffer"]
AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"]
CODEOWNERS = ["@clydebarrow"]
usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
@@ -116,6 +117,10 @@ CONFIG_SCHEMA = cv.ensure_list(
async def to_code(config):
# Enable wake_loop_threadsafe for low-latency USB data processing
# The USB task queues data events that need immediate processing
socket.require_wake_loop_threadsafe()
for device in config:
var = await register_usb_client(device)
for index, channel in enumerate(device[CONF_CHANNELS]):

View File

@@ -2,6 +2,7 @@
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "usb_uart.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/uart/uart_debugger.h"
#include <cinttypes>
@@ -262,6 +263,11 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
// Wake main loop immediately to process USB data instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
// On success, restart input immediately from USB task for performance

View File

@@ -67,8 +67,8 @@ void WakeOnLanButton::setup() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (this->broadcast_socket_ == nullptr) {
this->mark_failed();
this->status_set_error("Could not create socket");
this->mark_failed();
return;
}
int enable = 1;

View File

@@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_
}
} // namespace
void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
// CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions
//
// The race condition occurs because close() initiates lwIP teardown while
// the TCP/IP thread can still receive packets, causing assertions when
// recv_tcp() sees partially-torn-down state.
//
// By shutting down receive first, we tell lwIP to stop accepting new data BEFORE
// the teardown begins, eliminating the race window. We only shutdown RD (not RDWR)
// to allow the FIN packet to be sent cleanly during close().
//
// Note: This function may be called with an already-closed socket if the network
// stack closed it. In that case, shutdown() will fail but close() is safe to call.
//
// See: https://github.com/esphome/esphome-webserver/issues/163
// Attempt shutdown - ignore errors as socket may already be closed
shutdown(sockfd, SHUT_RD);
// Always close - safe even if socket is already closed by network stack
close(sockfd);
}
void AsyncWebServer::end() {
if (this->server_) {
httpd_stop(this->server_);
@@ -115,6 +138,8 @@ void AsyncWebServer::begin() {
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
// Enable LRU purging if requested (e.g., by captive portal to handle probe bursts)
config.lru_purge_enable = this->lru_purge_enable_;
// Use custom close function that shuts down before closing to prevent lwIP race conditions
config.close_fn = AsyncWebServer::safe_close_with_shutdown;
if (httpd_start(&this->server_, &config) == ESP_OK) {
const httpd_uri_t handler_get = {
.uri = "",
@@ -505,17 +530,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
void AsyncEventSourceResponse::destroy(void *ptr) {
auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
if (fd > 0) {
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
// Immediately shut down the socket to prevent lwIP from delivering more data
// This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack
// tries to deliver queued data after the session is marked as dead
// See: https://github.com/esphome/esphome/issues/11936
shutdown(fd, SHUT_RDWR);
// Note: We don't close() the socket - httpd owns it and will close it
}
// Session will be cleaned up in the main loop to avoid race conditions
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
// Mark as dead - will be cleaned up in the main loop
// Note: We don't delete or remove from set here to avoid race conditions
// httpd will call our custom close_fn (safe_close_with_shutdown) which handles
// shutdown() before close() to prevent lwIP race conditions
}
// helper for allowing only unique entries in the queue

View File

@@ -209,6 +209,7 @@ class AsyncWebServer {
static esp_err_t request_handler(httpd_req_t *r);
static esp_err_t request_post_handler(httpd_req_t *r);
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd);
#ifdef USE_WEBSERVER_OTA
esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type);
#endif

View File

@@ -485,14 +485,11 @@ async def to_code(config):
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
if config[CONF_FAST_CONNECT]:
cg.add_define("USE_WIFI_FAST_CONNECT")
# passive_scan defaults to false in C++ - only set if true
if config[CONF_PASSIVE_SCAN]:
cg.add(var.set_passive_scan(True))
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
if CONF_OUTPUT_POWER in config:
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)

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