mirror of
https://github.com/esphome/esphome.git
synced 2026-01-23 03:19:09 -07:00
Compare commits
39 Commits
no_new_to_
...
wifi_api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bacca21c4 | ||
|
|
425db688e5 | ||
|
|
be5bfe24da | ||
|
|
cfb61bc50a | ||
|
|
547c985672 | ||
|
|
44e624d7a7 | ||
|
|
5779e3e6e4 | ||
|
|
3184717607 | ||
|
|
e8972c65c8 | ||
|
|
71cda05073 | ||
|
|
3dbebb728d | ||
|
|
f938de16af | ||
|
|
ec791063b3 | ||
|
|
fb984cd052 | ||
|
|
85181779d1 | ||
|
|
95b23702e4 | ||
|
|
95eebcd74f | ||
|
|
3c3d5c2fca | ||
|
|
811ac81320 | ||
|
|
f01bd68a4b | ||
|
|
5433c0f707 | ||
|
|
b06cce9eeb | ||
|
|
65bcfee035 | ||
|
|
9261b9ecaa | ||
|
|
6725e6c01e | ||
|
|
effbcece49 | ||
|
|
98a926f37f | ||
|
|
110c173eac | ||
|
|
6008abae62 | ||
|
|
04e102f344 | ||
|
|
bb67b1ca1e | ||
|
|
6d7956a062 | ||
|
|
afbbdd1492 | ||
|
|
b06568c132 | ||
|
|
3c5fc638d5 | ||
|
|
ddb762f8f5 | ||
|
|
4ac7fe84b4 | ||
|
|
d6a41ed51e | ||
|
|
8d1379a275 |
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
|
||||
2
.github/workflows/auto-label-pr.yml
vendored
2
.github/workflows/auto-label-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
|
||||
4
.github/workflows/ci-api-proto.yml
vendored
4
.github/workflows/ci-api-proto.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@@ -43,9 +43,9 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -237,10 +237,10 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
@@ -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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -54,7 +54,7 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Build
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
|
||||
6
.github/workflows/sync-device-classes.yml
vendored
6
.github/workflows/sync-device-classes.yml
vendored
@@ -13,16 +13,16 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
|
||||
@@ -1844,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// filling the TCP send buffer and crowding out important state updates.
|
||||
//
|
||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
||||
//
|
||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
||||
//
|
||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
||||
//
|
||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
||||
// state here would be too late since we'd already be in a degraded state.
|
||||
this->helper_->set_nodelay(!is_log_message);
|
||||
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
|
||||
this->helper_->set_nodelay_for_message(is_log_message);
|
||||
|
||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
|
||||
@@ -120,26 +120,39 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
|
||||
//
|
||||
// For non-log messages (sensor data, state updates): Always disable Nagle
|
||||
// (NODELAY on) for immediate delivery - these are time-sensitive.
|
||||
//
|
||||
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
|
||||
// shared pbufs, but holding data too long waiting for Nagle's timer causes
|
||||
// buffer exhaustion and dropped messages.
|
||||
//
|
||||
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
this->set_nodelay_raw_(false);
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single operation
|
||||
@@ -229,10 +242,18 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
int val = enable ? 1 : 0;
|
||||
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
}
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -25,9 +25,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) {
|
||||
return to_string(std::forward<T>(val)); // NOLINT
|
||||
}
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
|
||||
@@ -13,14 +13,11 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
|
||||
@@ -108,10 +108,14 @@ void ATM90E32Component::update() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||
this->cs_->dump_summary(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
void ATM90E32Component::setup() {
|
||||
this->spi_setup();
|
||||
this->cs_summary_ = this->cs_->dump_summary();
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
|
||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||
uint16_t high_thresh = 0;
|
||||
@@ -159,13 +163,13 @@ void ATM90E32Component::setup() {
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
||||
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
|
||||
o_hash = fnv1_hash_extend(o_hash, cs);
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
||||
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
|
||||
po_hash = fnv1_hash_extend(po_hash, cs);
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
@@ -186,7 +190,7 @@ void ATM90E32Component::setup() {
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
||||
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
|
||||
g_hash = fnv1_hash_extend(g_hash, cs);
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
@@ -217,7 +221,8 @@ void ATM90E32Component::setup() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::log_calibration_status_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
|
||||
bool offset_mismatch = false;
|
||||
bool power_mismatch = false;
|
||||
@@ -568,7 +573,8 @@ float ATM90E32Component::get_chip_temperature_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_gain_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
if (!this->enable_gain_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
|
||||
cs);
|
||||
@@ -668,7 +674,8 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -681,7 +688,8 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
bool success = this->offset_pref_.save(&this->offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -697,7 +705,8 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -713,7 +722,8 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_offset_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
|
||||
@@ -743,7 +753,8 @@ void ATM90E32Component::run_offset_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_power_offset_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
@@ -816,7 +827,8 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_gain_calibrations_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
|
||||
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
|
||||
@@ -870,7 +882,8 @@ void ATM90E32Component::restore_gain_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_offset_calibrations_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->config_offset_phase_[i] = this->offset_phase_[i];
|
||||
|
||||
@@ -912,7 +925,8 @@ void ATM90E32Component::restore_offset_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
|
||||
|
||||
@@ -954,7 +968,8 @@ void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_gain_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
if (!this->using_saved_calibrations_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
@@ -1003,7 +1018,8 @@ void ATM90E32Component::clear_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_offset_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
if (!this->restored_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
@@ -1045,7 +1061,8 @@ void ATM90E32Component::clear_offset_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_power_offset_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
if (!this->restored_power_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
@@ -1120,7 +1137,8 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
|
||||
}
|
||||
|
||||
bool ATM90E32Component::verify_gain_writes_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
bool success = true;
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include "atm90e32_reg.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
@@ -182,6 +184,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
bool verify_gain_writes_();
|
||||
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
||||
void log_calibration_status_();
|
||||
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
|
||||
|
||||
struct ATM90E32Phase {
|
||||
uint16_t voltage_gain_{0};
|
||||
@@ -247,7 +250,6 @@ class ATM90E32Component : public PollingComponent,
|
||||
ESPPreferenceObject offset_pref_;
|
||||
ESPPreferenceObject power_offset_pref_;
|
||||
ESPPreferenceObject gain_calibration_pref_;
|
||||
std::string cs_summary_;
|
||||
|
||||
sensor::Sensor *freq_sensor_{nullptr};
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
|
||||
from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS
|
||||
from esphome.core import HexInt
|
||||
|
||||
CODEOWNERS = ["@nagyrobi"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
@@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None):
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_BINDKEY): cv.bind_key,
|
||||
}
|
||||
)
|
||||
.extend(BLE_DEVICE_SCHEMA)
|
||||
@@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_ble_device(var, config)
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
if bindkey := config.get(CONF_BINDKEY):
|
||||
bindkey_bytes = [
|
||||
HexInt(int(bindkey[index : index + 2], 16))
|
||||
for index in range(0, len(bindkey), 2)
|
||||
]
|
||||
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))
|
||||
|
||||
@@ -3,15 +3,23 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "mbedtls/ccm.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
|
||||
static const char *const TAG = "bthome_mithermometer";
|
||||
static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
|
||||
static constexpr size_t BTHOME_NONCE_SIZE = 13;
|
||||
static constexpr size_t BTHOME_MIC_SIZE = 4;
|
||||
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
|
||||
|
||||
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
|
||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||
@@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
||||
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
|
||||
if (this->has_bindkey_) {
|
||||
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
@@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
|
||||
return matched;
|
||||
}
|
||||
|
||||
void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
|
||||
if (bindkey.size() != sizeof(this->bindkey_)) {
|
||||
ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
|
||||
return;
|
||||
}
|
||||
std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
|
||||
this->has_bindkey_ = true;
|
||||
}
|
||||
|
||||
bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
|
||||
std::vector<uint8_t> &payload) const {
|
||||
if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
|
||||
ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
|
||||
payload.resize(ciphertext_size);
|
||||
|
||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||
mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
|
||||
}
|
||||
|
||||
std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
|
||||
memcpy(nonce.data(), mac.data(), mac.size());
|
||||
nonce[6] = 0xD2;
|
||||
nonce[7] = 0xFC;
|
||||
nonce[8] = data[0];
|
||||
memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
|
||||
|
||||
const uint8_t *ciphertext = data.data() + 1;
|
||||
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
|
||||
|
||||
mbedtls_ccm_context ctx;
|
||||
mbedtls_ccm_init(&ctx);
|
||||
|
||||
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
|
||||
if (ret) {
|
||||
ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
|
||||
mbedtls_ccm_free(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
|
||||
payload.data(), mic, BTHOME_MIC_SIZE);
|
||||
mbedtls_ccm_free(&ctx);
|
||||
if (ret) {
|
||||
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
||||
@@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
||||
return false;
|
||||
}
|
||||
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
if (is_encrypted) {
|
||||
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
|
||||
uint64_t source_address = device.address_uint64();
|
||||
bool address_matches = source_address == this->address_;
|
||||
if (!is_encrypted && mac_included && data.size() >= 7) {
|
||||
uint64_t advertised_address = 0;
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
advertised_address = (advertised_address << 8) | data[1 + i];
|
||||
}
|
||||
address_matches = address_matches || advertised_address == this->address_;
|
||||
}
|
||||
|
||||
if (is_encrypted && !this->has_bindkey_) {
|
||||
if (address_matches) {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
|
||||
device.address_str_to(addr_buf));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t payload_index = 1;
|
||||
uint64_t source_address = device.address_uint64();
|
||||
if (!is_encrypted && this->has_bindkey_) {
|
||||
if (address_matches) {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
|
||||
device.address_str_to(addr_buf));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> decrypted_payload;
|
||||
const uint8_t *payload = nullptr;
|
||||
size_t payload_size = 0;
|
||||
|
||||
if (is_encrypted) {
|
||||
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
|
||||
return false;
|
||||
}
|
||||
payload = decrypted_payload.data();
|
||||
payload_size = decrypted_payload.size();
|
||||
} else {
|
||||
payload = data.data() + 1;
|
||||
payload_size = data.size() - 1;
|
||||
}
|
||||
|
||||
if (mac_included) {
|
||||
if (data.size() < 7) {
|
||||
if (payload_size < 6) {
|
||||
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
||||
return false;
|
||||
}
|
||||
source_address = 0;
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
source_address = (source_address << 8) | data[1 + i];
|
||||
source_address = (source_address << 8) | payload[i];
|
||||
}
|
||||
payload_index = 7;
|
||||
payload += 6;
|
||||
payload_size -= 6;
|
||||
}
|
||||
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
if (source_address != this->address_) {
|
||||
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload_index >= data.size()) {
|
||||
if (payload_size == 0) {
|
||||
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reported = false;
|
||||
size_t offset = payload_index;
|
||||
size_t offset = 0;
|
||||
uint8_t last_type = 0;
|
||||
|
||||
while (offset < data.size()) {
|
||||
const uint8_t obj_type = data[offset++];
|
||||
while (offset < payload_size) {
|
||||
const uint8_t obj_type = payload[offset++];
|
||||
size_t value_length = 0;
|
||||
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
||||
|
||||
if (has_length_byte) {
|
||||
if (offset >= data.size()) {
|
||||
if (offset >= payload_size) {
|
||||
break;
|
||||
}
|
||||
value_length = data[offset++];
|
||||
value_length = payload[offset++];
|
||||
} else {
|
||||
if (!get_bthome_value_length(obj_type, value_length)) {
|
||||
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
||||
@@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
||||
break;
|
||||
}
|
||||
|
||||
if (offset + value_length > data.size()) {
|
||||
if (offset + value_length > payload_size) {
|
||||
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t *value = &data[offset];
|
||||
const uint8_t *value = &payload[offset];
|
||||
offset += value_length;
|
||||
|
||||
if (obj_type < last_type) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -14,6 +16,7 @@ namespace bthome_mithermometer {
|
||||
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
public:
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
void set_bindkey(std::initializer_list<uint8_t> bindkey);
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||
@@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
|
||||
protected:
|
||||
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
const esp32_ble_tracker::ESPBTDevice &device);
|
||||
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
|
||||
std::vector<uint8_t> &payload) const;
|
||||
|
||||
uint64_t address_{0};
|
||||
optional<uint8_t> last_packet_id_{};
|
||||
bool has_bindkey_{false};
|
||||
uint8_t bindkey_[16];
|
||||
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
|
||||
@@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
delayMicroseconds(500);
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
delayMicroseconds(1000);
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
delayMicroseconds(1000);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -48,7 +48,7 @@ class ESPBTUUID {
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const; // NOLINT
|
||||
std::string to_string() const;
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fingerprint_grow.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -532,14 +533,21 @@ void FingerprintGrowComponent::sensor_sleep_() {
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::dump_config() {
|
||||
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
if (this->has_sensing_pin_) {
|
||||
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
|
||||
}
|
||||
if (this->has_power_pin_) {
|
||||
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"GROW_FINGERPRINT_READER:\n"
|
||||
" System Identifier Code: 0x%.4X\n"
|
||||
" Touch Sensing Pin: %s\n"
|
||||
" Sensor Power Pin: %s",
|
||||
this->system_identifier_code_,
|
||||
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
|
||||
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
|
||||
} else {
|
||||
|
||||
@@ -3,13 +3,44 @@
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <map>
|
||||
#include "ir_sender_esphome.h"
|
||||
#include "HeatpumpIRFactory.h"
|
||||
#include <IRSender.h>
|
||||
#include <HeatpumpIRFactory.h>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace heatpumpir {
|
||||
|
||||
// IRSenderESPHome - bridge between ESPHome's remote_transmitter and HeatpumpIR library
|
||||
// Defined here (not in a header) to isolate HeatpumpIR's headers from the rest of ESPHome,
|
||||
// as they define conflicting symbols like millis() in the global namespace.
|
||||
class IRSenderESPHome : public IRSender {
|
||||
public:
|
||||
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()) {}
|
||||
|
||||
void setFrequency(int frequency) override { // NOLINT(readability-identifier-naming)
|
||||
auto *data = this->transmit_.get_data();
|
||||
data->set_carrier_frequency(1000 * frequency);
|
||||
}
|
||||
|
||||
void space(int space_length) override {
|
||||
if (space_length) {
|
||||
auto *data = this->transmit_.get_data();
|
||||
data->space(space_length);
|
||||
} else {
|
||||
this->transmit_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
void mark(int mark_length) override {
|
||||
auto *data = this->transmit_.get_data();
|
||||
data->mark(mark_length);
|
||||
}
|
||||
|
||||
protected:
|
||||
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
|
||||
};
|
||||
|
||||
static const char *const TAG = "heatpumpir.climate";
|
||||
|
||||
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#include "ir_sender_esphome.h"
|
||||
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
namespace esphome {
|
||||
namespace heatpumpir {
|
||||
|
||||
void IRSenderESPHome::setFrequency(int frequency) { // NOLINT(readability-identifier-naming)
|
||||
auto *data = transmit_.get_data();
|
||||
data->set_carrier_frequency(1000 * frequency);
|
||||
}
|
||||
|
||||
// Send an IR 'mark' symbol, i.e. transmitter ON
|
||||
void IRSenderESPHome::mark(int mark_length) {
|
||||
auto *data = transmit_.get_data();
|
||||
data->mark(mark_length);
|
||||
}
|
||||
|
||||
// Send an IR 'space' symbol, i.e. transmitter OFF
|
||||
void IRSenderESPHome::space(int space_length) {
|
||||
if (space_length) {
|
||||
auto *data = transmit_.get_data();
|
||||
data->space(space_length);
|
||||
} else {
|
||||
transmit_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace heatpumpir
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include <IRSender.h> // arduino-heatpump library
|
||||
|
||||
namespace esphome {
|
||||
namespace heatpumpir {
|
||||
|
||||
class IRSenderESPHome : public IRSender {
|
||||
public:
|
||||
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
|
||||
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
|
||||
void space(int space_length) override;
|
||||
void mark(int mark_length) override;
|
||||
|
||||
protected:
|
||||
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
|
||||
};
|
||||
|
||||
} // namespace heatpumpir
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
@@ -5,8 +5,6 @@
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#include "esphome/components/remote_receiver/remote_receiver.h"
|
||||
|
||||
namespace esphome::ir_rf_proxy {
|
||||
|
||||
|
||||
@@ -55,3 +55,44 @@ DriverChip(
|
||||
(0x35,), (0xFE,),
|
||||
],
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
"M5STACK-TAB5-V2",
|
||||
height=1280,
|
||||
width=720,
|
||||
hsync_back_porch=40,
|
||||
hsync_pulse_width=2,
|
||||
hsync_front_porch=40,
|
||||
vsync_back_porch=8,
|
||||
vsync_pulse_width=2,
|
||||
vsync_front_porch=220,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="960Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0x60, 0x71, 0x23, 0xa2),
|
||||
(0x60, 0x71, 0x23, 0xa3),
|
||||
(0x60, 0x71, 0x23, 0xa4),
|
||||
(0xA4, 0x31),
|
||||
(0xD7, 0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80),
|
||||
(0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09),
|
||||
(0xA3, 0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF),
|
||||
(0xA6, 0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xA7, 0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44),
|
||||
(0xAC, 0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01),
|
||||
(0xAD, 0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF),
|
||||
(0xAE, 0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00),
|
||||
(0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, 0xC0, 0x00),
|
||||
(0xE8, 0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E),
|
||||
(0x75, 0x03, 0x04),
|
||||
(0xE7, 0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77),
|
||||
(0xEA, 0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C),
|
||||
(0xB0, 0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43),
|
||||
(0xb7, 0x00, 0x00, 0x73, 0x73),
|
||||
(0xBF, 0xA6, 0xAA),
|
||||
(0xA9, 0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03),
|
||||
(0xC8, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
|
||||
(0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include <span>
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_rgb {
|
||||
@@ -343,19 +345,27 @@ int MipiRgb::get_height() {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string get_pin_name(GPIOPin *pin) {
|
||||
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||
if (pin == nullptr)
|
||||
return "None";
|
||||
return pin->dump_summary();
|
||||
pin->dump_summary(buffer.data(), buffer.size());
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) {
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (uint8_t i = start; i != end; i++) {
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
|
||||
}
|
||||
}
|
||||
|
||||
void MipiRgb::dump_config() {
|
||||
char reset_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char de_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char pclk_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char hsync_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char vsync_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"MIPI_RGB LCD"
|
||||
"\n Model: %s"
|
||||
@@ -379,9 +389,9 @@ void MipiRgb::dump_config() {
|
||||
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
|
||||
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
|
||||
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
|
||||
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_, reset_buf),
|
||||
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf),
|
||||
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf));
|
||||
|
||||
this->dump_pins_(8, 13, "Blue", 0);
|
||||
this->dump_pins_(13, 16, "Green", 0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "rpi_dpi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -134,8 +135,11 @@ void RpiDpiRgb::dump_config() {
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||
for (size_t i = 0; i != data_pin_count; i++)
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (size_t i = 0; i != data_pin_count; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
}
|
||||
|
||||
void RpiDpiRgb::reset_display_() const {
|
||||
|
||||
@@ -445,22 +445,18 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
|
||||
ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range)
|
||||
: min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {}
|
||||
optional<float> ClampFilter::new_value(float value) {
|
||||
if (std::isfinite(value)) {
|
||||
if (std::isfinite(this->min_) && value < this->min_) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
} else {
|
||||
return this->min_;
|
||||
}
|
||||
if (std::isfinite(this->min_) && !(value >= this->min_)) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
}
|
||||
return this->min_;
|
||||
}
|
||||
|
||||
if (std::isfinite(this->max_) && value > this->max_) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
} else {
|
||||
return this->max_;
|
||||
}
|
||||
if (std::isfinite(this->max_) && !(value <= this->max_)) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
}
|
||||
return this->max_;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "st7701s.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -183,8 +184,11 @@ void ST7701S::dump_config() {
|
||||
LOG_PIN(" DE Pin: ", this->de_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||
for (size_t i = 0; i != data_pin_count; i++)
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (size_t i = 0; i != data_pin_count; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
// Unsigned subtraction handles wraparound correctly, then cast to signed
|
||||
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
|
||||
if (diff >= -1 && diff <= 1) {
|
||||
// Time is already synchronized, but still call callbacks so components
|
||||
// waiting for time sync (e.g., uptime timestamp sensor) can initialize
|
||||
this->time_sync_callback_.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ struct Timer {
|
||||
}
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const { // NOLINT
|
||||
std::string to_string() const {
|
||||
char buffer[TO_STR_BUFFER_SIZE];
|
||||
return this->to_str(buffer);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -565,6 +565,11 @@ void WiFiComponent::start() {
|
||||
void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
// Clear error flag here because restart_adapter() enters COOLDOWN state,
|
||||
// and check_connecting_finished() is called after cooldown without going
|
||||
// through start_connecting() first. Without this clear, stale errors would
|
||||
// trigger spurious "failed (callback)" logs. The canonical clear location
|
||||
// is in start_connecting(); this is the only exception to that pattern.
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
@@ -618,8 +623,6 @@ void WiFiComponent::loop() {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
// Clear error flag before reconnecting so first attempt is not seen as immediate failure
|
||||
this->error_from_callback_ = false;
|
||||
this->retry_connect();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
@@ -743,16 +746,32 @@ void WiFiComponent::setup_ap_config_() {
|
||||
return;
|
||||
|
||||
if (this->ap_.get_ssid().empty()) {
|
||||
std::string name = App.get_name();
|
||||
if (name.length() > 32) {
|
||||
// Build AP SSID from app name without heap allocation
|
||||
// WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7
|
||||
static constexpr size_t AP_SSID_MAX_LEN = 32;
|
||||
static constexpr size_t AP_SSID_PREFIX_LEN = 25;
|
||||
static constexpr size_t AP_SSID_SUFFIX_LEN = 7;
|
||||
|
||||
const std::string &app_name = App.get_name();
|
||||
const char *name_ptr = app_name.c_str();
|
||||
size_t name_len = app_name.length();
|
||||
|
||||
if (name_len <= AP_SSID_MAX_LEN) {
|
||||
// Name fits, use directly
|
||||
this->ap_.set_ssid(name_ptr);
|
||||
} else {
|
||||
// Name too long, need to truncate into stack buffer
|
||||
char ssid_buf[AP_SSID_MAX_LEN + 1];
|
||||
if (App.is_name_add_mac_suffix_enabled()) {
|
||||
// Keep first 25 chars and last 7 chars (MAC suffix), remove middle
|
||||
name.erase(25, name.length() - 32);
|
||||
memcpy(ssid_buf, name_ptr, AP_SSID_PREFIX_LEN);
|
||||
memcpy(ssid_buf + AP_SSID_PREFIX_LEN, name_ptr + name_len - AP_SSID_SUFFIX_LEN, AP_SSID_SUFFIX_LEN);
|
||||
} else {
|
||||
name.resize(32);
|
||||
memcpy(ssid_buf, name_ptr, AP_SSID_MAX_LEN);
|
||||
}
|
||||
ssid_buf[AP_SSID_MAX_LEN] = '\0';
|
||||
this->ap_.set_ssid(ssid_buf);
|
||||
}
|
||||
this->ap_.set_ssid(name);
|
||||
}
|
||||
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
|
||||
|
||||
@@ -963,6 +982,12 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
|
||||
#endif
|
||||
|
||||
// Clear any stale error from previous connection attempt.
|
||||
// This is the canonical location for clearing the flag since all connection
|
||||
// attempts go through start_connecting(). The only other clear is in
|
||||
// restart_adapter() which enters COOLDOWN without calling start_connecting().
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
// Enter cooldown to allow WiFi hardware to stabilize
|
||||
@@ -1068,7 +1093,6 @@ void WiFiComponent::enable() {
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->error_from_callback_ = false;
|
||||
this->state_ = WIFI_COMPONENT_STATE_OFF;
|
||||
this->start();
|
||||
}
|
||||
@@ -1329,11 +1353,6 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||
this->num_retried_ = 0;
|
||||
// Ensure next connection attempt does not inherit error state
|
||||
// so when WiFi disconnects later we start fresh and don't see
|
||||
// the first connection as a failure.
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (this->has_ap()) {
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (this->is_captive_portal_active_()) {
|
||||
@@ -1844,8 +1863,6 @@ void WiFiComponent::retry_connect() {
|
||||
this->advance_to_next_target_or_increment_retry_();
|
||||
}
|
||||
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
yield();
|
||||
// Check if we have a valid target before building params
|
||||
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||
@@ -2171,7 +2188,6 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
this->roaming_state_ = RoamingState::CONNECTING;
|
||||
|
||||
// Connect directly - wifi_sta_connect_ handles disconnect internally
|
||||
this->error_from_callback_ = false;
|
||||
this->start_connecting(roam_params);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools==80.10.1", "wheel>=0.43,<0.46"]
|
||||
requires = ["setuptools==80.10.1", "wheel>=0.43,<0.47"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
|
||||
@@ -752,47 +752,6 @@ def lint_no_sprintf(fname, match):
|
||||
)
|
||||
|
||||
|
||||
@lint_re_check(
|
||||
# Match std::to_string() or unqualified to_string() calls
|
||||
# The esphome namespace has "using std::to_string;" so unqualified calls resolve to std::to_string
|
||||
# Use negative lookbehind to avoid matching:
|
||||
# - Function definitions: "const char *to_string(" or "std::string to_string("
|
||||
# - Method definitions: "Class::to_string("
|
||||
# - Method calls: ".to_string(" or "->to_string("
|
||||
# - Other identifiers: "_to_string("
|
||||
r"(?<![*&.\w>:])to_string\s*\(" + CPP_RE_EOL,
|
||||
include=cpp_include,
|
||||
exclude=[
|
||||
# Vendored library
|
||||
"esphome/components/http_request/httplib.h",
|
||||
# Deprecated helpers that return std::string
|
||||
"esphome/core/helpers.cpp",
|
||||
# The using declaration itself
|
||||
"esphome/core/helpers.h",
|
||||
# Test fixtures - not production embedded code
|
||||
"tests/integration/fixtures/*",
|
||||
],
|
||||
)
|
||||
def lint_no_std_to_string(fname, match):
|
||||
return (
|
||||
f"{highlight('std::to_string()')} allocates heap memory. On long-running embedded "
|
||||
f"devices, repeated heap allocations fragment memory over time.\n"
|
||||
f"Please use {highlight('snprintf()')} with a stack buffer instead.\n"
|
||||
f"\n"
|
||||
f"Buffer sizes and format specifiers:\n"
|
||||
f" uint8_t/int8_t: 4 chars - %u / %d (or PRIu8/PRId8)\n"
|
||||
f" uint16_t/int16_t: 6 chars - %u / %d (or PRIu16/PRId16)\n"
|
||||
f" uint32_t/int32_t: 11 chars - %" + "PRIu32 / %" + "PRId32\n"
|
||||
" uint64_t/int64_t: 21 chars - %" + "PRIu64 / %" + "PRId64\n"
|
||||
f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n"
|
||||
f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n"
|
||||
f"\n"
|
||||
f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n"
|
||||
f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n'
|
||||
f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)"
|
||||
)
|
||||
|
||||
|
||||
@lint_content_find_check(
|
||||
"ESP_LOG",
|
||||
include=["*.h", "*.tcc"],
|
||||
|
||||
@@ -3,6 +3,7 @@ esp32_ble_tracker:
|
||||
sensor:
|
||||
- platform: bthome_mithermometer
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
bindkey: eef418daf699a0c188f3bfd17e4565d9
|
||||
temperature:
|
||||
name: "BTHome Temperature"
|
||||
humidity:
|
||||
|
||||
18
tests/components/ir_rf_proxy/common-rx.yaml
Normal file
18
tests/components/ir_rf_proxy/common-rx.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
remote_receiver:
|
||||
id: ir_receiver
|
||||
pin: ${rx_pin}
|
||||
|
||||
# Test various hardware types with transmitter/receiver using infrared platform
|
||||
infrared:
|
||||
# Infrared receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_rx
|
||||
name: "IR Receiver"
|
||||
remote_receiver_id: ir_receiver
|
||||
|
||||
# RF 900MHz receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_900_rx
|
||||
name: "RF 900 Receiver"
|
||||
frequency: 900 MHz
|
||||
remote_receiver_id: ir_receiver
|
||||
19
tests/components/ir_rf_proxy/common-tx.yaml
Normal file
19
tests/components/ir_rf_proxy/common-tx.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
remote_transmitter:
|
||||
id: ir_transmitter
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
# Test various hardware types with transmitter/receiver using infrared platform
|
||||
infrared:
|
||||
# Infrared transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_tx
|
||||
name: "IR Transmitter"
|
||||
remote_transmitter_id: ir_transmitter
|
||||
|
||||
# RF 433MHz transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_433_tx
|
||||
name: "RF 433 Transmitter"
|
||||
frequency: 433 MHz
|
||||
remote_transmitter_id: ir_transmitter
|
||||
@@ -1,42 +1,7 @@
|
||||
network:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
remote_transmitter:
|
||||
id: ir_transmitter
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
remote_receiver:
|
||||
id: ir_receiver
|
||||
pin: ${rx_pin}
|
||||
|
||||
# Test various hardware types with transmitter/receiver using infrared platform
|
||||
infrared:
|
||||
# Infrared transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_tx
|
||||
name: "IR Transmitter"
|
||||
remote_transmitter_id: ir_transmitter
|
||||
|
||||
# Infrared receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_rx
|
||||
name: "IR Receiver"
|
||||
remote_receiver_id: ir_receiver
|
||||
|
||||
# RF 433MHz transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_433_tx
|
||||
name: "RF 433 Transmitter"
|
||||
frequency: 433 MHz
|
||||
remote_transmitter_id: ir_transmitter
|
||||
|
||||
# RF 900MHz receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_900_rx
|
||||
name: "RF 900 Receiver"
|
||||
frequency: 900 MHz
|
||||
remote_receiver_id: ir_receiver
|
||||
|
||||
7
tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml
Normal file
7
tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml
Normal file
7
tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
tx: !include common-tx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
tx: !include common-tx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
tx: !include common-tx.yaml
|
||||
8
tests/components/ir_rf_proxy/test.bk72xx-ard.yaml
Normal file
8
tests/components/ir_rf_proxy/test.bk72xx-ard.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
@@ -2,4 +2,7 @@ substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
|
||||
@@ -2,4 +2,7 @@ substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
|
||||
@@ -2,4 +2,7 @@ substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
|
||||
@@ -117,6 +117,7 @@ sensor:
|
||||
- 10.0 -> 12.1
|
||||
- 13.0 -> 14.0
|
||||
- clamp:
|
||||
# Infinity and NaN will be clamped (NaN -> min_value, +Infinity -> max_value, -Infinity -> min_value)
|
||||
max_value: 10.0
|
||||
min_value: -10.0
|
||||
- debounce: 0.1s
|
||||
|
||||
Reference in New Issue
Block a user