Merge branch 'esphome:dev' into lsm6ds_test_001

This commit is contained in:
NonaSuomy authored and GitHub committed 2026-04-11 14:26:16 -04:00
commit e1486e229b
117 files changed
+2228 -520

No files matched your search

+2 -2
View File
@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -4,6 +4,7 @@ module.exports = {
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
TOO_BIG_MARKER: '<!-- too-big-request -->',
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
MANAGED_LABELS: [
'new-component',
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
return { labels, deprecatedInfo };
}
// Strategy: Detect when maintainers cannot modify the PR branch
function detectMaintainerAccess(context) {
const pr = context.payload.pull_request;
// Only relevant for cross-repo PRs (forks)
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
return null;
}
if (pr.maintainer_can_modify) {
return null;
}
const isOrgFork = pr.head.repo.owner.type === 'Organization';
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
return { isOrgFork, orgName: pr.head.repo.owner.login };
}
// Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context) {
const labels = new Set();
@@ -329,5 +347,6 @@ module.exports = {
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectMaintainerAccess,
detectRequirements
};
+11 -5
View File
@@ -12,9 +12,10 @@ const {
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectMaintainerAccess,
detectRequirements
} = require('./detectors');
const { handleReviews } = require('./reviews');
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
const { applyLabels, removeOldLabels } = require('./labels');
// Fetch API data
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
codeOwnerLabels,
testLabels,
checkboxLabels,
deprecatedResult
deprecatedResult,
maintainerAccess
] = await Promise.all([
detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData),
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
detectCodeOwner(github, context, changedFiles),
detectTests(changedFiles),
detectPRTemplateCheckboxes(context),
detectDeprecatedComponents(github, context, changedFiles)
detectDeprecatedComponents(github, context, changedFiles),
detectMaintainerAccess(context)
]);
// Extract deprecated component info
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
// Handle reviews and org fork comment
await Promise.all([
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
handleMaintainerAccessComment(github, context, maintainerAccess)
]);
// Apply labels
await applyLabels(github, context, finalLabels);
+60 -2
View File
@@ -2,7 +2,8 @@ const {
BOT_COMMENT_MARKER,
CODEOWNERS_MARKER,
TOO_BIG_MARKER,
DEPRECATED_COMPONENT_MARKER
DEPRECATED_COMPONENT_MARKER,
ORG_FORK_MARKER
} = require('./constants');
// Generate review messages
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
}
}
// Handle maintainer access warning comment
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
if (!maintainerAccess) {
return;
}
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
const prAuthor = context.payload.pull_request.user.login;
// Check if we already posted the warning (iterate pages to exit early)
let existingComment;
for await (const { data: comments } of github.paginate.iterator(
github.rest.issues.listComments,
{ owner, repo, issue_number: pr_number }
)) {
existingComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body && comment.body.includes(ORG_FORK_MARKER)
);
if (existingComment) {
break;
}
}
if (existingComment) {
console.log('Maintainer access warning comment already exists, skipping');
return;
}
let body;
if (maintainerAccess.isOrgFork) {
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
`Hey there @${prAuthor},\n` +
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
} else {
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
`Hey there @${prAuthor},\n` +
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body
});
console.log('Created maintainer access warning comment');
}
module.exports = {
handleReviews
handleReviews,
handleMaintainerAccessComment
};
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
- name: Auto Label PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+3 -3
View File
@@ -47,7 +47,7 @@ jobs:
fi
- if: failure()
name: Review PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.pulls.createReview({
@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: generated-proto-files
path: |
@@ -70,7 +70,7 @@ jobs:
esphome/components/api/api_pb2_service.*
- if: success()
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
let reviews = await github.rest.pulls.listReviews({
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
name: Request changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.pulls.createReview({
@@ -55,7 +55,7 @@ jobs:
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
let reviews = await github.rest.pulls.listReviews({
+6 -4
View File
@@ -868,7 +868,8 @@ jobs:
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
-t "$platform" \
--base-only 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--output-env \
@@ -903,7 +904,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -954,7 +955,8 @@ jobs:
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
-t "$platform" \
--base-only 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--output-env \
@@ -967,7 +969,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -34,7 +34,7 @@ jobs:
CODEOWNERS
- name: Check codeowner approval and update label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
@@ -33,7 +33,7 @@ jobs:
ref: ${{ github.event.pull_request.base.sha }}
- name: Request reviews from component codeowners
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Add external component comment
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify codeowners for component issues
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const {
+4 -4
View File
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -229,7 +229,7 @@ jobs:
repositories: home-assistant-addon
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
@@ -264,7 +264,7 @@ jobs:
repositories: esphome-schema
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
@@ -295,7 +295,7 @@ jobs:
repositories: version-notifier
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+13 -14
View File
@@ -2,30 +2,29 @@ name: Status check labels
on:
pull_request:
types: [labeled, unlabeled]
types: [opened, reopened, labeled, unlabeled, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check:
name: Check ${{ matrix.label }}
name: Check blocking labels
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
label:
- needs-docs
- merge-after-release
- chained-pr
steps:
- name: Check for ${{ matrix.label }} label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- name: Check for blocking labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
if (hasLabel) {
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
const labelNames = labels.map(l => l.name);
const found = blockingLabels.filter(bl => labelNames.includes(bl));
if (found.length > 0) {
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
}
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.9
rev: v0.15.10
hooks:
# Run the linter.
- id: ruff
+1 -1
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 = 2026.4.0-dev
PROJECT_NUMBER = 2026.5.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
@@ -8,6 +8,9 @@ namespace ade7953_base {
static const char *const TAG = "ade7953";
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
static const float ADE_POWER_FACTOR = 154.0f;
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
@@ -18,7 +21,12 @@ void ADE7953::setup() {
// The chip might take up to 100ms to initialise
this->set_timeout(100, [this]() {
// this->ade_write_8(0x0010, 0x04);
// Lock communication interface (SPI or I2C)
uint16_t config_v = CONFIG_DEFAULT;
this->ade_read_16(CONFIG_16, &config_v);
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
this->ade_write_16(CONFIG_16, config_v);
// Configure optimum settings according to datasheet
this->ade_write_8(0x00FE, 0xAD);
this->ade_write_16(0x0120, 0x0030);
// Set gains
+17 -13
View File
@@ -9,31 +9,35 @@
namespace esphome {
namespace ade7953_base {
static const uint8_t PGA_V_8 =
static constexpr uint8_t PGA_V_8 =
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
static const uint8_t PGA_IA_8 =
static constexpr uint8_t PGA_IA_8 =
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
static const uint8_t PGA_IB_8 =
static constexpr uint8_t PGA_IB_8 =
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
static const uint32_t AIGAIN_32 =
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
static constexpr uint16_t AIGAIN_32 =
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static const uint32_t AWGAIN_32 =
static constexpr uint16_t AVGAIN_32 =
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static constexpr uint16_t AWGAIN_32 =
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
static const uint32_t AVARGAIN_32 =
static constexpr uint16_t AVARGAIN_32 =
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
static const uint32_t AVAGAIN_32 =
static constexpr uint16_t AVAGAIN_32 =
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
static const uint32_t BIGAIN_32 =
static constexpr uint16_t BIGAIN_32 =
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static const uint32_t BWGAIN_32 =
static constexpr uint16_t BVGAIN_32 =
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static constexpr uint16_t BWGAIN_32 =
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
static const uint32_t BVARGAIN_32 =
static constexpr uint16_t BVARGAIN_32 =
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
static const uint32_t BVAGAIN_32 =
static constexpr uint16_t BVAGAIN_32 =
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
class ADE7953 : public PollingComponent, public sensor::Sensor {
@@ -7,6 +7,9 @@ namespace ade7953_spi {
static const char *const TAG = "ade7953";
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
void AdE7953Spi::setup() {
this->spi_setup();
ade7953_base::ADE7953::setup();
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
this->write_byte16(reg);
this->transfer_byte(0);
this->write_byte16(value);
if (reg == ade7953_base::CONFIG_16) {
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
}
this->disable();
return false;
}
+1 -1
View File
@@ -12,7 +12,7 @@ namespace esphome {
namespace ade7953_spi {
class AdE7953Spi : public ade7953_base::ADE7953,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> {
public:
void setup() override;
+1
View File
@@ -1625,6 +1625,7 @@ message BluetoothLEAdvertisementResponse {
}
message BluetoothLERawAdvertisement {
option (inline_encode) = true;
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3 [(max_value) = 4];
+55 -64
View File
@@ -52,11 +52,11 @@
namespace esphome::api {
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
// Maximum messages to read per loop iteration to prevent starving other components.
// This is a balance between API responsiveness and allowing other components to run.
// Since each message could contain multiple protobuf messages when using packet batching,
// this limits the number of messages processed, not the number of TCP packets.
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10;
static constexpr uint8_t MAX_PING_RETRIES = 60;
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
@@ -220,10 +220,17 @@ void APIConnection::loop() {
}
const uint32_t now = App.get_loop_component_start_time();
// Check if socket has data ready before attempting to read
if (this->helper_->is_socket_ready()) {
// Check if socket has data ready before attempting to read.
// Also try reading if we hit the message limit last time — LWIP's rcvevent
// (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple
// messages share a TCP segment, the last message's data stays in LWIP's
// lastdata cache after rcvevent hits 0, making is_socket_ready() return false
// even though data remains.
if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) {
this->flags_.may_have_remaining_data = false;
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
uint8_t message_count = 0;
for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
@@ -245,6 +252,11 @@ void APIConnection::loop() {
return;
}
}
// If we hit the limit, there may be more data remaining in LWIP's
// lastdata cache that rcvevent doesn't account for.
if (message_count == MAX_MESSAGES_PER_LOOP) {
this->flags_.may_have_remaining_data = true;
}
}
// Process deferred batch if scheduled and timer has expired
@@ -315,6 +327,8 @@ void APIConnection::process_active_iterator_() {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
} else {
this->finalize_iterator_sync_();
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
@@ -322,21 +336,27 @@ void APIConnection::process_active_iterator_() {
} else { // INITIAL_STATE
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
this->finalize_iterator_sync_();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
}
}
void APIConnection::finalize_iterator_sync_() {
// Flush any remaining batched messages immediately so clients
// receive completion responses (e.g. ListEntitiesDoneResponse)
// without waiting for the batch timer.
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
}
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
@@ -406,7 +426,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
}
uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
@@ -2005,48 +2025,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
}
// Encodes a message to the buffer and returns the total number of bytes used,
// including header and footer overhead. Returns 0 if the message doesn't fit.
uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
const uint8_t footer_size = conn->helper_->frame_footer_size();
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
// Calculate total size with padding for buffer allocation
size_t total_calculated_size = calculated_size + header_padding + footer_size;
// Check if it fits
if (total_calculated_size > remaining_size)
return 0; // Doesn't fit
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
size_t to_add;
if (conn->flags_.batch_first_message) {
// First message - buffer already prepared by caller, just clear flag
conn->flags_.batch_first_message = false;
to_add = calculated_size;
} else {
// Batch message second or later
// Reserve for full message, resize to include footer gap + header padding + payload
to_add = total_calculated_size;
}
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
// Return total size (header + payload + footer)
return static_cast<uint16_t>(total_calculated_size);
// Noinline version for cold paths — single shared copy
uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size) {
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
@@ -2114,6 +2098,13 @@ void APIConnection::process_batch_() {
return;
}
// Ensure TCP_NODELAY is on before draining overflow and writing batch data.
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
// If Nagle is still on when we try to drain, LWIP holds data in the
// Nagle buffer, the TCP send buffer stays full, and the overflow
// buffer can never drain — blocking the batch write indefinitely.
this->helper_->set_nodelay_for_message(false);
// Try to clear buffer first
if (!this->try_to_clear_buffer(true)) {
// Can't write now, we'll try again later
@@ -2173,17 +2164,15 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
"MessageInfo must remain trivially destructible with this placement-new approach");
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
const uint8_t frame_overhead = header_padding + footer_size;
// Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
size_t items_processed = 0;
uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
// Track where each message's header padding begins in the buffer
// For plaintext: this is where the 6-byte header padding starts
// For noise: this is where the 7-byte header padding starts
// The actual message data follows after the header padding
// Track where each message's header begins in the buffer
// First message: offset 0 (max padding, may have unused leading bytes)
// Subsequent messages: offset points to exact header start (no gaps)
uint32_t current_offset = 0;
// Process items and encode directly to buffer (up to our limit)
@@ -2199,13 +2188,14 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
}
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead;
// payload_size = header_size + proto_payload_size + footer_size
uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size;
// Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible,
// as ensured by the static_assert in its definition.
new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size);
new (&message_info[items_processed++])
MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_);
// After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
if (items_processed == 1) {
remaining_size = MAX_BATCH_PACKET_SIZE;
@@ -2255,6 +2245,7 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
bool batch_first) {
this->flags_.batch_first_message = batch_first;
this->batch_message_type_ = item.message_type;
#ifdef USE_EVENT
// Events need aux_data_index to look up event type from entity
if (item.message_type == EventResponse::MESSAGE_TYPE) {
+58 -8
View File
@@ -411,16 +411,59 @@ class APIConnection final : public APIServerConnectionBase {
// Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
// Non-template buffer management for batch encoding
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size);
// Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes.
// ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites.
static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn,
const void *msg, APIConnection *conn,
uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
const uint8_t footer_size = conn->helper_->frame_footer_size();
// Thin template wrapper — computes size, delegates buffer work to non-template helper
// First message uses max padding (already in buffer), subsequent use exact header size
size_t to_add;
if (conn->flags_.batch_first_message) {
conn->flags_.batch_first_message = false;
conn->batch_header_size_ = conn->helper_->frame_header_padding();
to_add = calculated_size;
} else {
conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_);
to_add = calculated_size + conn->batch_header_size_ + footer_size;
}
// Check if it fits (using actual header size, not max padding)
uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size;
if (total_calculated_size > remaining_size)
return 0;
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return total_calculated_size;
}
// Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages).
// All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion.
static uint16_t encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size);
// Thin template wrapper — uses noinline encode_to_buffer_slow since
// encode_message_to_buffer callers are cold paths (zero-payload control messages).
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
// batch_message_type_ is already set by dispatch_message_ before reaching here.
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
}
@@ -619,6 +662,7 @@ class APIConnection final : public APIServerConnectionBase {
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
void finalize_iterator_sync_();
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
@@ -727,6 +771,7 @@ class APIConnection final : public APIServerConnectionBase {
uint8_t batch_scheduled : 1;
uint8_t batch_first_message : 1; // For batch buffer allocation
uint8_t should_try_send_immediately : 1; // True after initial states are sent
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
#ifdef HAS_PROTO_MESSAGE_DUMP
uint8_t log_only_mode : 1;
#endif
@@ -735,9 +780,14 @@ class APIConnection final : public APIServerConnectionBase {
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// 1-byte type to fill padding
// 1-byte types to fill remaining space before next 4-byte boundary
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint8_t batch_message_type_{0}; // Current message type during batch encoding
// Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary
// Actual header size used by encode_to_buffer for the current message.
// Read by process_batch_multi_ to pass into MessageInfo.
uint8_t batch_header_size_{0};
uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
// Message will use 8 more bytes than the minimum size, and typical
+49 -29
View File
@@ -100,10 +100,17 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
#ifdef HELPER_LOG_PACKETS
void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) {
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
}
#endif
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
@@ -111,45 +118,58 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
return APIError::OK;
}
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Single-buffer write path: wraps in iovec and delegates.
APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) {
struct iovec iov = {const_cast<void *>(data), len};
APIError err = this->write_raw_iov_(&iov, 1, len, sent);
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
// Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent
if (err == APIError::OK)
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
#endif
return err;
}
uint16_t skip = 0;
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) [[unlikely]] {
// Handles partial writes, errors, and overflow buffering.
// Called when the inline fast path couldn't complete the write,
// or directly from cold paths (handshake, error handling).
APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) {
if (sent <= 0) {
if (sent == WRITE_NOT_ATTEMPTED) {
// Cold path: no write attempted yet, drain overflow and try
if (!this->overflow_buf_.empty()) {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
if (this->overflow_buf_.empty()) {
sent = this->write_iov_to_socket_(iov, iovcnt);
if (sent == static_cast<ssize_t>(total_write_len))
return APIError::OK;
// Partial write or -1: fall through to error check / enqueue below
} else {
// Overflow backlog remains after drain; skip socket write, enqueue everything
sent = 0;
}
}
// WRITE_FAILED (-1): fast path or retry write returned -1, check errno
if (sent == WRITE_FAILED) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
skip = static_cast<uint16_t>(sent);
sent = 0; // Treat WOULD_BLOCK as zero bytes sent
}
}
// Full write completed (possible when called directly, not via write_raw_fast_buf_)
if (sent == static_cast<ssize_t>(total_write_len))
return APIError::OK;
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
+67 -23
View File
@@ -49,12 +49,17 @@ struct ReadPacketBuffer {
};
// Packed message info structure to minimize memory usage
// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
// The noise wire format encodes types as 16-bit, but the high byte is always 0.
// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
struct MessageInfo {
uint16_t offset; // Offset in buffer where message starts
uint16_t payload_size; // Size of the message payload
uint8_t message_type; // Message type (0-255)
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
};
enum class APIError : uint16_t {
@@ -161,23 +166,39 @@ class APIFrameHelper {
this->nodelay_counter_ = 0;
}
}
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize buffer to include footer space if needed (e.g. Noise MAC)
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
// Write multiple protobuf messages in a single operation
// messages contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
// Write a single protobuf message - the hot path (87-100% of all writes).
// Caller must ensure state is DATA before calling.
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single batched operation.
// Caller must ensure state is DATA and messages is not empty.
// messages contains (message_type, offset, length) for each message in the buffer.
// The buffer contains all messages with appropriate padding before each.
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
// Get the frame header padding required by this protocol
// Get the maximum frame header padding required by this protocol (worst case)
uint8_t frame_header_padding() const { return frame_header_padding_; }
// Get the actual frame header size for a specific message.
// For noise: always returns frame_header_padding_ (fixed 7-byte header).
// For plaintext: computes actual size from varint lengths (3-6 bytes).
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
// added, this should become a virtual method.
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
return this->frame_footer_size_
? this->frame_header_padding_
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
#elif defined(USE_API_NOISE)
return this->frame_header_padding_;
#else // USE_API_PLAINTEXT only
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
#endif
}
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read
// Check if socket has buffered data ready to read.
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
// or track that they stopped early and retry without this check.
// See Socket::ready() for details.
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
// Release excess memory from internal buffers after initial sync
void release_buffers() {
@@ -196,18 +217,41 @@ class APIFrameHelper {
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Sentinel values for the sent parameter in write_raw_ methods
static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
APIError check_socket_write_err_(int err) {
if (err == EWOULDBLOCK || err == EAGAIN)
return APIError::WOULD_BLOCK;
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
// Dispatch to write() or writev() based on iovec count
inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
}
// Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
// These inline the fast path (overflow empty + full write) and tail-call the out-of-line
// slow path only on failure/partial write.
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent = this->socket_->write(data, len);
if (sent == static_cast<ssize_t>(len)) [[likely]] {
#ifdef HELPER_LOG_PACKETS
this->log_packet_sending_(data, len);
#endif
return APIError::OK;
}
// sent is -1 (WRITE_FAILED) or partial write count
return this->write_raw_buf_(data, len, sent);
}
return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
}
// Out-of-line write paths: handle partial writes, errors, overflow buffering
// sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
ssize_t sent = WRITE_NOT_ATTEMPTED);
#ifdef HELPER_LOG_PACKETS
void log_packet_sending_(const void *data, uint16_t len);
#endif
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Convert a noise error code to a readable error
@@ -464,65 +457,83 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
// Encrypt a single noise message in place and return the encrypted frame length.
// Returns APIError::OK on success.
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out) {
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + payload_size, 4 + payload_size + this->frame_footer_size_);
int err = noise_cipherstate_encrypt(this->send_cipher_, &mbuf);
APIError aerr =
this->handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
}
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
// Resize buffer to include footer space for Noise MAC
if (this->frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
uint16_t payload_size =
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
uint8_t *buf_start = buffer.get_buffer()->data();
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
if (aerr != APIError::OK)
return aerr;
return this->write_raw_fast_buf_(buf_start, encrypted_len);
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
assert(!messages.empty());
#endif
// Noise messages are already contiguous in the buffer:
// HEADER_PADDING (7) exactly matches the fixed header size, and
// footer space (16) is consumed by the encryption MAC.
uint8_t *buffer_data = buffer.get_buffer()->data();
// Stack-allocated iovec array - no heap allocation
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint8_t *write_start = buffer_data + messages[0].offset;
uint16_t total_write_len = 0;
// We need to encrypt each message in place
for (const auto &msg : messages) {
// The buffer already has padding at offset
uint8_t *buf_start = buffer_data + msg.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
// The buffer should already have been sized appropriately
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size,
4 + msg.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, msg.payload_size, msg.message_type, encrypted_len);
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted message
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
iovs.push_back({buf_start, msg_len});
total_write_len += msg_len;
total_write_len += encrypted_len;
}
// Send all encrypted messages in one writev call
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
return this->write_raw_fast_buf_(write_start, total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -531,16 +542,16 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
if (len == 0) {
return this->write_raw_buf_(header, 3);
}
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_(iov, 2, 3 + len); // Header + data
return this->write_raw_iov_(iov, 2, 3 + len);
}
/** Initiate the data structures for the handshake.
@@ -606,7 +617,7 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
if (aerr != APIError::OK)
return aerr;
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
@@ -9,19 +9,22 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
frame_header_padding_ = HEADER_PADDING;
}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -33,6 +36,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError state_action_handshake_write_();
APIError try_read_frame_();
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const LogString *reason);
@@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Initialize the frame helper, returns OK if successful.
@@ -205,7 +198,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
// Make sure to tell the remote that we don't
// understand the indicator byte so it knows
// we do not support it.
struct iovec iov[1];
// The \x00 first byte is the marker for plaintext.
//
// The remote will know how to handle the indicator byte,
@@ -220,14 +212,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
char msg[INDICATOR_MSG_SIZE];
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
iov[0].iov_base = (void *) msg;
this->write_raw_buf_(msg, INDICATOR_MSG_SIZE);
#else
static const char MSG[] = "\x00"
"Bad indicator byte";
iov[0].iov_base = (void *) MSG;
this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE);
#endif
iov[0].iov_len = INDICATOR_MSG_SIZE;
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
}
return aerr;
}
@@ -237,73 +227,101 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
// Encode a 16-bit varint (1-3 bytes) using pre-computed length.
ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) {
if (varint_len >= 2) {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
if (varint_len == 3) {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
}
*p = static_cast<uint8_t>(value);
}
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
if (varint_len == 2) {
*p++ = static_cast<uint8_t>(value | 0x80);
*p = static_cast<uint8_t>(value >> 7);
} else {
*p = value;
}
}
// Write plaintext header into pre-allocated padding before payload.
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
// actual header size for contiguous batch messages).
// Returns the total header length (indicator + varints).
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
uint8_t message_type, uint8_t padding_size) {
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
uint8_t type_varint_len = ProtoSize::varint8(message_type);
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// The header is right-justified within the padding so it sits immediately before payload.
//
// Single/first message (padding_size = HEADER_PADDING = 6):
// Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload
// Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload
// Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload
//
// Batch messages 2+ (padding_size = actual header size, no unused bytes):
// Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload
// Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload
#ifdef ESPHOME_DEBUG_API
assert(padding_size >= total_header_len);
#endif
uint32_t header_offset = padding_size - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer using pre-computed lengths
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
return total_header_len;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING);
uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING);
return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len,
static_cast<uint16_t>(header_len + payload_size));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
}
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
assert(!messages.empty());
#endif
uint8_t *buffer_data = buffer.get_buffer()->data();
// Stack-allocated iovec array - no heap allocation
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint16_t total_write_len = 0;
// First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes.
// Subsequent messages were encoded with exact header sizes (header_size = actual header len).
// write_plaintext_header right-justifies the header within header_size bytes of padding.
const auto &first = messages[0];
uint8_t *first_start = buffer_data + first.offset;
uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING);
uint8_t *write_start = first_start + HEADER_PADDING - header_len;
uint16_t total_len = header_len + first.payload_size;
for (const auto &msg : messages) {
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
? 1
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
// [4-5] - Message type varint (2 bytes, for types 128-16383)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = buffer_data + msg.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
// Add iovec for this message (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
iovs.push_back({buf_start + header_offset, msg_len});
total_write_len += msg_len;
for (size_t i = 1; i < messages.size(); i++) {
const auto &msg = messages[i];
header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size);
total_len += header_len + msg.payload_size;
}
// Send all messages in one writev call
return write_raw_(iovs.data(), iovs.size(), total_write_len);
return this->write_raw_fast_buf_(write_start, total_len);
}
} // namespace esphome::api
@@ -7,18 +7,21 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper {
public:
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
frame_header_padding_ = HEADER_PADDING;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
+1
View File
@@ -22,6 +22,7 @@ extend google.protobuf.MessageOptions {
optional bool log = 1039 [default=true];
optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
optional bool inline_encode = 1042 [default=false];
}
extend google.protobuf.FieldOptions {
+22 -25
View File
@@ -2328,40 +2328,37 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
}
return true;
}
uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi));
if (this->address_type) {
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type);
}
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len));
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len);
return pos;
}
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint64_force(1, this->address);
size += ProtoSize::calc_sint32_force(1, this->rssi);
size += this->address_type ? 2 : 0;
size += 2 + this->data_len;
return size;
}
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
for (uint16_t i = 0; i < this->advertisements_len; i++) {
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]);
auto &sub_msg = this->advertisements[i];
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 10);
uint8_t *len_pos = pos;
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
if (sub_msg.address_type) {
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address_type);
}
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(sub_msg.data_len));
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.data, sub_msg.data_len);
*len_pos = static_cast<uint8_t>(pos - len_pos - 1);
}
return pos;
}
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t size = 0;
for (uint16_t i = 0; i < this->advertisements_len; i++) {
size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size());
auto &sub_msg = this->advertisements[i];
size += 2;
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
size += sub_msg.address_type ? 2 : 0;
size += 2 + sub_msg.data_len;
}
return size;
}
-2
View File
@@ -1888,8 +1888,6 @@ class BluetoothLERawAdvertisement final : public ProtoMessage {
uint32_t address_type{0};
uint8_t data[62]{};
uint8_t data_len{0};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
+17
View File
@@ -352,6 +352,12 @@ class ProtoEncode {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = b;
}
/// Reserve one byte for later backpatch (e.g., sub-message length).
/// Advances pos past the reserved byte without writing a value.
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
pos++;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
const void *data, size_t len) {
@@ -645,6 +651,17 @@ class ProtoSize {
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
// Varint encoded length for a 16-bit value (1, 2, or 3 bytes).
// Fully inline — no slow path call for values >= 128.
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value) {
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
}
// Varint encoded length for an 8-bit value (1 or 2 bytes).
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) {
return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2;
}
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
@@ -61,6 +61,15 @@ void BedJetClimate::dump_config() {
}
void BedJetClimate::setup() {
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
this->set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
"M2",
"M3",
});
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
@@ -42,21 +42,14 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
climate::CLIMATE_MODE_DRY,
});
// It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE,
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST,
});
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
"M2",
"M3",
});
// Custom fan modes and presets are set once in setup(), stored on Climate base class,
// and wired automatically via get_traits()
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);
+17 -1
View File
@@ -102,6 +102,8 @@ CC1101Component::CC1101Component() {
memset(this->pa_table_, 0, sizeof(this->pa_table_));
}
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
void CC1101Component::setup() {
this->spi_setup();
this->cs_->digital_write(true);
@@ -148,7 +150,12 @@ void CC1101Component::setup() {
// Defer pin mode setup until after all components have completed setup()
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
if (this->gdo0_pin_ != nullptr) {
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
this->defer([this]() {
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
});
}
}
@@ -160,6 +167,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
}
void CC1101Component::loop() {
this->disable_loop();
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
return;
@@ -240,6 +248,7 @@ void CC1101Component::begin_tx() {
this->write_(Register::PKTCTRL0, 0x32);
ESP_LOGV(TAG, "Beginning TX sequence");
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->detach_interrupt();
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
@@ -669,6 +678,13 @@ void CC1101Component::set_packet_mode(bool value) {
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
if (this->gdo0_pin_ != nullptr) {
if (value) {
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
} else {
this->gdo0_pin_->detach_interrupt();
}
}
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::IOCFG0);
+1
View File
@@ -93,6 +93,7 @@ class CC1101Component : public Component,
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
+16 -5
View File
@@ -484,6 +484,11 @@ void Climate::publish_state() {
ClimateTraits Climate::get_traits() {
auto traits = this->traits();
// Wire custom mode pointers from Climate-owned storage
if (this->supported_custom_fan_modes_)
traits.set_supported_custom_fan_modes_(this->supported_custom_fan_modes_);
if (this->supported_custom_presets_)
traits.set_supported_custom_presets_(this->supported_custom_presets_);
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
if (!std::isnan(this->visual_min_temperature_override_)) {
traits.set_visual_min_temperature(this->visual_min_temperature_override_);
@@ -681,9 +686,8 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) {
}
bool Climate::set_custom_fan_mode_(const char *mode, size_t len) {
auto traits = this->get_traits();
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode,
traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode());
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, this->find_custom_fan_mode_(mode, len),
this->has_custom_fan_mode());
}
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
@@ -691,8 +695,7 @@ void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
bool Climate::set_custom_preset_(const char *preset, size_t len) {
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len),
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, this->find_custom_preset_(preset, len),
this->has_custom_preset());
}
@@ -703,6 +706,10 @@ const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
if (this->supported_custom_fan_modes_) {
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
}
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
}
@@ -711,6 +718,10 @@ const char *Climate::find_custom_preset_(const char *custom_preset) {
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
if (this->supported_custom_presets_) {
return vector_find(*this->supported_custom_presets_, custom_preset, len);
}
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
return this->get_traits().find_custom_preset_(custom_preset, len);
}
+44 -3
View File
@@ -1,5 +1,6 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
@@ -234,6 +235,28 @@ class Climate : public EntityBase {
void set_visual_max_humidity_override(float visual_max_humidity_override);
#endif
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->ensure_custom_fan_modes_().assign(modes.begin(), modes.end());
}
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->ensure_custom_fan_modes_() = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->ensure_custom_fan_modes_().assign(modes, modes + N);
}
/// Set the supported custom presets (stored on Climate, referenced by ClimateTraits).
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->ensure_custom_presets_().assign(presets.begin(), presets.end());
}
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->ensure_custom_presets_() = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->ensure_custom_presets_().assign(presets, presets + N);
}
/// Check if a custom fan mode is currently active.
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
@@ -336,13 +359,14 @@ class Climate : public EntityBase {
* called from publish_state()
*/
void save_state_(const ClimateTraits &traits);
void save_state_() { this->save_state_(this->traits()); }
void save_state_() { this->save_state_(this->get_traits()); }
void dump_traits_(const char *tag);
LazyCallbackManager<void(Climate &)> state_callback_{};
LazyCallbackManager<void(ClimateCall &)> control_callback_{};
ESPPreferenceObject rtc_;
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN};
float visual_max_temperature_override_{NAN};
@@ -353,16 +377,33 @@ class Climate : public EntityBase {
#endif
private:
/// Lazy-allocate custom mode vectors (never freed — entity lives forever).
std::vector<const char *> &ensure_custom_fan_modes_() {
if (!this->supported_custom_fan_modes_) {
this->supported_custom_fan_modes_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_custom_fan_modes_;
}
std::vector<const char *> &ensure_custom_presets_() {
if (!this->supported_custom_presets_) {
this->supported_custom_presets_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_custom_presets_;
}
std::vector<const char *> *supported_custom_fan_modes_{nullptr};
std::vector<const char *> *supported_custom_presets_{nullptr};
/** The active custom fan mode (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
* Points to an entry in supported_custom_fan_modes_ or nullptr.
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
*/
const char *custom_fan_mode_{nullptr};
/** The active custom preset (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_presets_ or nullptr.
* Points to an entry in supported_custom_presets_ or nullptr.
* Use get_custom_preset() to read, set_custom_preset_() to modify.
*/
const char *custom_preset_{nullptr};
@@ -2,6 +2,33 @@
namespace esphome::climate {
// Compat: shared empty vector for getters when no custom modes are set.
// Remove in 2026.11.0 when deprecated ClimateTraits setters are removed
// and getters can return const vector * instead of const vector &.
static const std::vector<const char *> EMPTY_CUSTOM_MODES; // NOLINT
const std::vector<const char *> &ClimateTraits::get_supported_custom_fan_modes() const {
if (this->supported_custom_fan_modes_) {
return *this->supported_custom_fan_modes_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
if (!this->compat_custom_fan_modes_.empty()) {
return this->compat_custom_fan_modes_;
}
return EMPTY_CUSTOM_MODES;
}
const std::vector<const char *> &ClimateTraits::get_supported_custom_presets() const {
if (this->supported_custom_presets_) {
return *this->supported_custom_presets_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
if (!this->compat_custom_presets_.empty()) {
return this->compat_custom_presets_;
}
return EMPTY_CUSTOM_MODES;
}
int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const {
return step_to_accuracy_decimals(this->visual_target_temperature_step_);
}
+67 -23
View File
@@ -147,27 +147,45 @@ class ClimateTraits {
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
if (!this->supported_fan_modes_.empty()) {
return true;
}
// Same precedence as get_supported_custom_fan_modes() getter
if (this->supported_custom_fan_modes_) {
return !this->supported_custom_fan_modes_->empty();
}
return !this->compat_custom_fan_modes_.empty(); // Compat: remove in 2026.11.0
}
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->supported_custom_fan_modes_ = modes;
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
this->compat_custom_fan_modes_ = modes;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->supported_custom_fan_modes_ = modes;
this->compat_custom_fan_modes_ = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N);
// Remove before 2026.11.0
template<size_t N>
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->compat_custom_fan_modes_.assign(modes, modes + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &get_supported_custom_fan_modes() const;
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
return (this->supported_custom_fan_modes_ &&
vector_contains(*this->supported_custom_fan_modes_, custom_fan_mode)) ||
vector_contains(this->compat_custom_fan_modes_, custom_fan_mode); // Compat: remove in 2026.11.0
}
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
@@ -179,23 +197,32 @@ class ClimateTraits {
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->supported_custom_presets_ = presets;
this->compat_custom_presets_ = presets;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->supported_custom_presets_ = presets;
this->compat_custom_presets_ = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N);
// Remove before 2026.11.0
template<size_t N>
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(const char *const (&presets)[N]) {
this->compat_custom_presets_.assign(presets, presets + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &get_supported_custom_presets() const;
bool supports_custom_preset(const char *custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset);
return (this->supported_custom_presets_ && vector_contains(*this->supported_custom_presets_, custom_preset)) ||
vector_contains(this->compat_custom_presets_, custom_preset); // Compat: remove in 2026.11.0
}
bool supports_custom_preset(const std::string &custom_preset) const {
return this->supports_custom_preset(custom_preset.c_str());
@@ -258,13 +285,25 @@ class ClimateTraits {
}
}
/// Set custom mode pointers (only Climate::get_traits() should call these).
void set_supported_custom_fan_modes_(const std::vector<const char *> *modes) {
this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_presets_(const std::vector<const char *> *presets) {
this->supported_custom_presets_ = presets;
}
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
if (this->supported_custom_fan_modes_) {
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
}
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
return vector_find(this->compat_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
@@ -273,7 +312,11 @@ class ClimateTraits {
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
if (this->supported_custom_presets_) {
return vector_find(*this->supported_custom_presets_, custom_preset, len);
}
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
return vector_find(this->compat_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};
@@ -289,16 +332,17 @@ class ClimateTraits {
climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_;
/** Custom mode storage using const char* pointers to eliminate std::string overhead.
/** Custom mode storage - pointers to vectors owned by the Climate base class.
*
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
* - Static const data: static const char* MODE = "Eco";
*
* Climate class setters validate pointers are from these vectors before storing.
* ClimateTraits does not own this data; Climate stores the vectors and
* get_traits() wires these pointers automatically.
*/
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
const std::vector<const char *> *supported_custom_fan_modes_{nullptr};
const std::vector<const char *> *supported_custom_presets_{nullptr};
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
// Remove in 2026.11.0.
std::vector<const char *> compat_custom_fan_modes_;
std::vector<const char *> compat_custom_presets_;
};
} // namespace esphome::climate
+8 -1
View File
@@ -7,6 +7,12 @@ namespace copy {
static const char *const TAG = "copy.fan";
void CopyFan::setup() {
// Copy preset modes once from source fan — stored on Fan base class
auto source_traits = source_->get_traits();
if (source_traits.supports_preset_modes()) {
this->set_supported_preset_modes(source_traits.supported_preset_modes());
}
source_->add_on_state_callback([this]() {
this->copy_state_from_source_();
this->publish_state();
@@ -39,7 +45,8 @@ fan::FanTraits CopyFan::get_traits() {
traits.set_speed(base.supports_speed());
traits.set_supported_speed_count(base.supported_speed_count());
traits.set_direction(base.supports_direction());
traits.set_supported_preset_modes(base.supported_preset_modes());
// Preset modes are set once in setup() and wired via wire_preset_modes_()
this->wire_preset_modes_(traits);
return traits;
}
+15 -3
View File
@@ -16,6 +16,19 @@ class DemoClimate : public climate::Climate, public Component {
public:
void set_type(DemoClimateType type) { type_ = type; }
void setup() override {
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
switch (type_) {
case DemoClimateType::TYPE_1:
break;
case DemoClimateType::TYPE_2:
this->set_supported_custom_fan_modes({"Auto Low", "Auto High"});
this->set_supported_custom_presets({"My Preset"});
break;
case DemoClimateType::TYPE_3:
this->set_supported_custom_fan_modes({"Auto Low", "Auto High"});
break;
}
// Set initial state
switch (type_) {
case DemoClimateType::TYPE_1:
this->current_temperature = 20.0;
@@ -105,14 +118,13 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_FAN_DIFFUSE,
climate::CLIMATE_FAN_QUIET,
});
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
// Custom fan modes and presets are set once in setup()
traits.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL,
});
traits.set_supported_custom_presets({"My Preset"});
break;
case DemoClimateType::TYPE_3:
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
@@ -123,7 +135,7 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_HEAT_COOL,
});
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
// Custom fan modes are set once in setup()
traits.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_HORIZONTAL,
@@ -43,3 +43,11 @@ wave_4_26.extend(
},
},
)
ssd1677.extend(
"waveshare-3.97in",
width=800,
height=480,
mirror_x=True,
)
+47 -8
View File
@@ -9,6 +9,22 @@ namespace fan {
static const char *const TAG = "fan";
// Compat: shared empty vector for getter when no preset modes are set.
// Remove in 2026.11.0 when deprecated FanTraits setters are removed
// and getter can return const vector * instead of const vector &.
static const std::vector<const char *> EMPTY_PRESET_MODES; // NOLINT
const std::vector<const char *> &FanTraits::supported_preset_modes() const {
if (this->preset_modes_) {
return *this->preset_modes_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0 (change return to const vector *).
if (!this->compat_preset_modes_.empty()) {
return this->compat_preset_modes_;
}
return EMPTY_PRESET_MODES;
}
// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN");
@@ -148,6 +164,18 @@ const char *Fan::find_preset_mode_(const char *preset_mode) {
}
const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
if (preset_mode == nullptr || len == 0) {
return nullptr;
}
if (this->supported_preset_modes_) {
for (const char *mode : *this->supported_preset_modes_) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode;
}
}
return nullptr;
}
// Fallback for deprecated path: external components may set modes on FanTraits directly
return this->get_traits().find_preset_mode(preset_mode, len);
}
@@ -261,8 +289,6 @@ void Fan::save_state_() {
return;
}
auto traits = this->get_traits();
FanRestoreState state{};
state.state = this->state;
state.oscillating = this->oscillating;
@@ -271,12 +297,25 @@ void Fan::save_state_() {
state.preset_mode = FanRestoreState::NO_PRESET;
if (this->has_preset_mode()) {
const auto &preset_modes = traits.supported_preset_modes();
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
for (size_t i = 0; i < preset_modes.size(); i++) {
if (preset_modes[i] == this->preset_mode_) {
state.preset_mode = i;
break;
if (this->supported_preset_modes_) {
// New path: search Fan-owned vector directly
for (size_t i = 0; i < this->supported_preset_modes_->size(); i++) {
if ((*this->supported_preset_modes_)[i] == this->preset_mode_) {
state.preset_mode = i;
break;
}
}
} else {
// Compat: fall back to traits for deprecated path. Remove in 2026.11.0.
// Pointer comparison works because preset_mode_ and the compat vector both
// hold pointers to string literals in .rodata (stable addresses).
auto traits = this->get_traits();
const auto &preset_modes = traits.supported_preset_modes();
for (size_t i = 0; i < preset_modes.size(); i++) {
if (preset_modes[i] == this->preset_mode_) {
state.preset_mode = i;
break;
}
}
}
}
+24
View File
@@ -130,6 +130,14 @@ class Fan : public EntityBase {
virtual FanTraits get_traits() = 0;
/// Set the supported preset modes (stored on Fan, referenced by FanTraits via pointer).
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
this->ensure_preset_modes_().assign(preset_modes.begin(), preset_modes.end());
}
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
this->ensure_preset_modes_() = preset_modes;
}
/// Set the restore mode of this fan.
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -167,11 +175,27 @@ class Fan : public EntityBase {
const char *find_preset_mode_(const char *preset_mode);
const char *find_preset_mode_(const char *preset_mode, size_t len);
/// Wire the Fan-owned preset modes pointer into the given traits object.
void wire_preset_modes_(FanTraits &traits) {
if (this->supported_preset_modes_) {
traits.set_supported_preset_modes_(this->supported_preset_modes_);
}
}
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_;
private:
/// Lazy-allocate preset modes vector (never freed — entity lives forever).
std::vector<const char *> &ensure_preset_modes_() {
if (!this->supported_preset_modes_) {
this->supported_preset_modes_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_preset_modes_;
}
std::vector<const char *> *supported_preset_modes_{nullptr};
const char *preset_mode_{nullptr};
};
+38 -11
View File
@@ -3,12 +3,17 @@
#include <cstring>
#include <vector>
#include <initializer_list>
#include "esphome/core/helpers.h"
namespace esphome {
namespace fan {
class Fan; // Forward declaration
class FanTraits {
friend class Fan; // Allow Fan to access protected pointer setter
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
@@ -30,42 +35,64 @@ class FanTraits {
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan.
const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan (from initializer list).
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &supported_preset_modes() const;
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
this->preset_modes_ = preset_modes;
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
this->compat_preset_modes_ = preset_modes;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
this->compat_preset_modes_ = preset_modes;
}
/// Set the preset modes supported by the fan (from vector).
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
/// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
bool supports_preset_modes() const {
// Same precedence as supported_preset_modes() getter
if (this->preset_modes_) {
return !this->preset_modes_->empty();
}
return !this->compat_preset_modes_.empty();
}
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
const char *find_preset_mode(const char *preset_mode) const {
return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *find_preset_mode(const char *preset_mode, size_t len) const {
if (preset_mode == nullptr || len == 0)
if (preset_mode == nullptr || len == 0) {
return nullptr;
for (const char *mode : this->preset_modes_) {
}
// Check pointer-based storage (new path) then compat owned vector (deprecated path)
const auto &modes = this->preset_modes_ ? *this->preset_modes_ : this->compat_preset_modes_;
for (const char *mode : modes) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode; // Return pointer from traits
return mode;
}
}
return nullptr;
}
protected:
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
void set_supported_preset_modes_(const std::vector<const char *> *preset_modes) {
this->preset_modes_ = preset_modes;
}
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
int speed_count_{};
std::vector<const char *> preset_modes_{};
const std::vector<const char *> *preset_modes_{nullptr};
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
// Remove in 2026.11.0.
std::vector<const char *> compat_preset_modes_;
};
} // namespace fan
+1 -1
View File
@@ -7,7 +7,7 @@ namespace gdk101 {
static const char *const TAG = "gdk101";
static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5;
static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10;
static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 30;
static constexpr uint32_t RESET_INTERVAL_ID = 0;
static constexpr uint32_t RESET_INTERVAL_MS = 1000;
@@ -30,7 +30,6 @@ fan::FanCall HBridgeFan::brake() {
void HBridgeFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_();
if (restore.has_value()) {
+5 -3
View File
@@ -20,11 +20,14 @@ class HBridgeFan : public Component, public fan::Fan {
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
void setup() override;
void dump_config() override;
fan::FanTraits get_traits() override { return this->traits_; }
fan::FanTraits get_traits() override {
this->wire_preset_modes_(this->traits_);
return this->traits_;
}
fan::FanCall brake();
@@ -36,7 +39,6 @@ class HBridgeFan : public Component, public fan::Fan {
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
void control(const fan::FanCall &call) override;
void write_state_();
+1 -1
View File
@@ -8,7 +8,7 @@ from .. import hbridge_ns
CODEOWNERS = ["@DotNetDann"]
HBridgeLightOutput = hbridge_ns.class_(
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
"HBridgeLightOutput", cg.Component, light.LightOutput
)
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
@@ -1,20 +1,17 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/log.h"
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace hbridge {
// Using PollingComponent as the updates are more consistent and reduces flickering
class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
class HBridgeLightOutput : public Component, public light::LightOutput {
public:
HBridgeLightOutput() : PollingComponent(1) {}
void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; }
void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; }
void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; }
void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
@@ -24,16 +21,16 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
return traits;
}
void setup() override { this->forward_direction_ = false; }
void setup() override { this->disable_loop(); }
void update() override {
// This method runs around 60 times per second
// We cannot do the PWM ourselves so we are reliant on the hardware PWM
if (!this->forward_direction_) { // First LED Direction
void loop() override {
// Only called when both channels are active — alternate H-bridge direction
// each iteration to multiplex cold and warm white.
if (!this->forward_direction_) {
this->pina_pin_->set_level(this->pina_duty_);
this->pinb_pin_->set_level(0);
this->forward_direction_ = true;
} else { // Second LED Direction
} else {
this->pina_pin_->set_level(0);
this->pinb_pin_->set_level(this->pinb_duty_);
this->forward_direction_ = false;
@@ -43,15 +40,32 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void write_state(light::LightState *state) override {
state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false);
float new_pina, new_pinb;
state->current_values_as_cwww(&new_pina, &new_pinb, false);
this->pina_duty_ = new_pina;
this->pinb_duty_ = new_pinb;
if (new_pina != 0.0f && new_pinb != 0.0f) {
// Both channels active — need loop to alternate H-bridge direction
this->high_freq_.start();
this->enable_loop();
} else {
// Zero or one channel active — drive pins directly, no multiplexing needed
this->high_freq_.stop();
this->disable_loop();
this->pina_pin_->set_level(new_pina);
this->pinb_pin_->set_level(new_pinb);
}
}
protected:
output::FloatOutput *pina_pin_;
output::FloatOutput *pinb_pin_;
float pina_duty_ = 0;
float pinb_duty_ = 0;
bool forward_direction_ = false;
float pina_duty_{0};
float pinb_duty_{0};
bool forward_direction_{false};
HighFrequencyLoopRequester high_freq_;
};
} // namespace hbridge
+4
View File
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
@@ -24,6 +25,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(MCP23016),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -35,6 +37,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value):
+14
View File
@@ -24,11 +24,22 @@ void MCP23016::setup() {
// all pins input
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); }
void MCP23016::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
@@ -37,6 +48,9 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
this->update_reg_(pin, true, MCP23016_IODIR1);
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg_(pin, false, MCP23016_IODIR1);
}
+4
View File
@@ -35,7 +35,10 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
float get_setup_priority() const override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -51,6 +54,7 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x0000};
InternalGPIOPin *interrupt_pin_{nullptr};
};
class MCP23016GPIOPin : public GPIOPin {
@@ -451,6 +451,8 @@ async def to_code(config):
ota.request_ota_state_listeners()
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1")
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
@@ -29,14 +29,6 @@ void VADModel::log_model_config() {
bool StreamingModel::load_model_() {
RAMAllocator<uint8_t> arena_allocator;
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->var_arena_ == nullptr) {
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
if (this->var_arena_ == nullptr) {
@@ -53,6 +45,26 @@ bool StreamingModel::load_model_() {
return false;
}
// Probe for the actual required tensor arena size if not yet determined
if (!this->tensor_arena_size_probed_) {
size_t probed_size = this->probe_arena_size_();
if (probed_size > 0) {
ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size);
this->tensor_arena_size_ = probed_size;
} else {
ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_);
}
this->tensor_arena_size_probed_ = true;
}
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->interpreter_ == nullptr) {
this->interpreter_ =
make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_,
@@ -94,6 +106,70 @@ bool StreamingModel::load_model_() {
return true;
}
size_t StreamingModel::probe_arena_size_() {
RAMAllocator<uint8_t> arena_allocator;
// Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different
// versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct,
// and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16
// bytes.
size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15,
(this->tensor_arena_size_ * 2 + 15) & ~15};
for (size_t attempt_size : attempt_sizes) {
uint8_t *probe_arena = arena_allocator.allocate(attempt_size);
if (probe_arena == nullptr) {
continue;
}
// Verify the model works at all with this arena size
auto probe_interpreter = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_);
if (probe_interpreter->AllocateTensors() != kTfLiteOk) {
probe_interpreter.reset();
arena_allocator.deallocate(probe_arena, attempt_size);
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
continue;
}
// Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment).
// If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds.
size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15;
probe_interpreter.reset();
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
size_t upper = attempt_size;
while (lower < upper) {
auto test_interpreter = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_);
bool ok = test_interpreter->AllocateTensors() == kTfLiteOk;
test_interpreter.reset();
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
if (ok) {
// Found a working size smaller than the full arena
upper = lower + 16; // Pad by 16 bytes to be safe for future allocations
break;
}
// Try the midpoint between current attempt and full size
lower = ((lower + upper) / 2 + 15) & ~15;
}
arena_allocator.deallocate(probe_arena, attempt_size);
return upper;
}
return 0;
}
void StreamingModel::unload_model() {
this->interpreter_.reset();
@@ -63,6 +63,10 @@ class StreamingModel {
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
/// @return True if successful, false otherwise
bool load_model_();
/// @brief Probes the actual required tensor arena size by trial allocation.
/// Tries the manifest size first, then 2x if that fails.
/// @return The required arena size rounded up to 16-byte alignment, or 0 on failure.
size_t probe_arena_size_();
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
@@ -70,6 +74,7 @@ class StreamingModel {
bool loaded_{false};
bool enabled_{true};
bool tensor_arena_size_probed_{false};
bool unprocessed_probability_status_{false};
uint8_t current_stride_step_{0};
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
+2 -2
View File
@@ -168,8 +168,8 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
if (capabilities.supportEcoPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
if (capabilities.supportFrostProtectionPreset())
traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION});
// Frost protection custom preset is handled by AirConditioner directly
// since custom presets are stored on the Climate base class
}
} // namespace ac
+22 -5
View File
@@ -24,6 +24,25 @@ template<typename T> void update_property(T &property, const T &value, bool &fla
}
void AirConditioner::on_status_change() {
// Add frost protection custom preset once when autoconf completes
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
// Read existing presets (set by codegen), append frost protection, write back
const auto &existing = this->get_traits().get_supported_custom_presets();
bool found = false;
for (const char *p : existing) {
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
found = true;
break;
}
}
if (!found) {
std::vector<const char *> merged(existing.begin(), existing.end());
merged.push_back(Constants::FREEZE_PROTECTION);
this->set_supported_custom_presets(merged);
}
this->frost_protection_set_ = true;
}
bool need_publish = false;
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
@@ -91,17 +110,15 @@ ClimateTraits AirConditioner::traits() {
traits.set_supported_modes(this->supported_modes_);
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supported_presets(this->supported_presets_);
if (!this->supported_custom_presets_.empty())
traits.set_supported_custom_presets(this->supported_custom_presets_);
if (!this->supported_custom_fan_modes_.empty())
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
// Custom fan modes and presets are stored on Climate base class and wired via get_traits()
/* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
Converters::to_climate_traits(traits, this->base_.getCapabilities());
}
if (!traits.get_supported_modes().empty())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
if (!traits.get_supported_swing_modes().empty())
+3 -4
View File
@@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->set_supported_custom_presets(presets); }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->set_supported_custom_fan_modes(modes); }
protected:
void control(const ClimateCall &call) override;
@@ -55,8 +55,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
std::vector<const char *> supported_custom_presets_{};
std::vector<const char *> supported_custom_fan_modes_{};
bool frost_protection_set_{false};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
+9 -1
View File
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
@@ -25,7 +26,12 @@ PCA6416AGPIOPin = pca6416a_ns.class_(
CONF_PCA6416A = "pca6416a"
CONFIG_SCHEMA = (
cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)})
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
@@ -35,6 +41,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value):
+18
View File
@@ -49,11 +49,22 @@ void PCA6416AComponent::setup() {
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); }
void PCA6416AComponent::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
void PCA6416AComponent::dump_config() {
@@ -62,6 +73,7 @@ void PCA6416AComponent::dump_config() {
} else {
ESP_LOGCONFIG(TAG, "PCA6416A:");
}
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
@@ -101,6 +113,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_register_(pin, true, pull_dir);
this->update_register_(pin, false, pull_en);
}
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
this->update_register_(pin, true, io_dir);
if (has_pullup_) {
@@ -109,6 +124,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
} else {
ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors");
}
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_register_(pin, false, io_dir);
}
+4
View File
@@ -24,7 +24,10 @@ class PCA6416AComponent : public Component,
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -43,6 +46,7 @@ class PCA6416AComponent : public Component,
esphome::i2c::ErrorCode last_error_;
/// Only the PCAL6416A has pull-up resistors
bool has_pullup_{false};
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Helper class to expose a PCA6416A pin as an internal input GPIO pin.
+5
View File
@@ -1,3 +1,8 @@
CODEOWNERS = ["@clydebarrow"]
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
DEPRECATED_COMPONENT = """
The 'qspi_dbi' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
"""
+6
View File
@@ -1,3 +1,5 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import display, spi
@@ -29,6 +31,7 @@ from . import CONF_DRAW_FROM_ORIGIN
from .models import DriverChip
DEPENDENCIES = ["spi"]
LOGGER = logging.getLogger(__name__)
qspi_dbi_ns = cg.esphome_ns.namespace("qspi_dbi")
QSPI_DBI = qspi_dbi_ns.class_(
@@ -160,6 +163,9 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
LOGGER.warning(
"The 'qspi_dbi' component is deprecated, it is recommended to use 'mipi_spi' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=True)
+25 -6
View File
@@ -9,7 +9,7 @@
#include <WiFi.h>
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
#elif defined(USE_ETHERNET)
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below)
#include "esphome/components/ethernet/ethernet_component.h"
#endif
#include <hardware/structs/rosc.h>
@@ -43,9 +43,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
// assertion failures). See esphome#10681.
//
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
// firing during critical sections.
// WiFi uses cyw43_arch_lwip_begin/end.
//
// For wired Ethernet, taking only the async_context lock is NOT enough. The
// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP`
// counter to decide whether to defer packet processing. If we hold the
// async_context lock without bumping `__inLWIP`, an interrupt-driven packet
// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat`
// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's
// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the
// lock, and on release re-unmask any GPIO IRQs that were deferred while we
// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because
// pulling lwip_wrap.h there poisons many TUs with lwIP types.
//
// When neither WiFi nor Ethernet is configured, this is a no-op since
// there's no network stack and no lwip callbacks to race with.
@@ -53,8 +62,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
#elif defined(USE_ETHERNET)
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
LwIPLock::LwIPLock() {
__inLWIP++;
ethernet_arch_lwip_begin();
}
LwIPLock::~LwIPLock() {
ethernet_arch_lwip_end();
__inLWIP--;
if (__needsIRQEN && !__inLWIP) {
__needsIRQEN = false;
ethernet_arch_lwip_gpio_unmask();
}
}
#else
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
@@ -1 +1,6 @@
CODEOWNERS = ["@clydebarrow"]
DEPRECATED_COMPONENT = """
The 'rpi_dpi_rgb' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_rgb' component.
"""
@@ -1,3 +1,5 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import display
@@ -38,6 +40,7 @@ from esphome.const import (
)
DEPENDENCIES = ["esp32"]
LOGGER = logging.getLogger(__name__)
rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb")
RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component)
@@ -126,6 +129,9 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
LOGGER.warning(
"The 'rpi_dpi_rgb' component is deprecated, it is recommended to use 'mipi_rgb' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+8 -5
View File
@@ -55,11 +55,13 @@ void SafeModeComponent::dump_config() {
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
if (last_invalid != nullptr) {
ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label);
ESP_LOGW(TAG, "The device reset before the boot was marked successful");
ESP_LOGW(TAG,
"OTA rollback detected! Rolled back from partition '%s'\n"
" The device reset before the boot was marked successful",
last_invalid->label);
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!");
ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
" See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
}
}
#endif
@@ -86,7 +88,8 @@ void SafeModeComponent::mark_successful() {
}
void SafeModeComponent::loop() {
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
if (!this->boot_successful_ &&
(App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
// successful boot, reset counter
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->mark_successful();
@@ -112,6 +112,8 @@ class BSDSocketImpl {
int setblocking(bool blocking);
int loop() { return 0; }
/// Check if the socket has buffered data ready to read.
/// See the ready() contract in socket.h — callers must drain or track remaining data.
bool ready() const;
int get_fd() const { return this->fd_; }
@@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon {
errno = ENOSYS;
return -1;
}
// Check if the socket has buffered data ready to read.
// See the ready() contract in socket.h — callers must drain or track remaining data.
// Intentionally unlocked — this is a polling check called every loop iteration.
// A stale read at worst delays processing by one loop tick; the actual I/O in
// read() holds the lwip lock and re-checks properly. See esphome#10681.
@@ -78,6 +78,8 @@ class LwIPSocketImpl {
int setblocking(bool blocking);
int loop() { return 0; }
/// Check if the socket has buffered data ready to read.
/// See the ready() contract in socket.h — callers must drain or track remaining data.
bool ready() const;
int get_fd() const { return this->fd_; }
+13
View File
@@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored);
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
// declared above, while the impl headers are included before those declarations.
//
// Contract (applies to ALL socket implementations — each platform implements
// ready() differently, but this contract holds regardless of the mechanism):
// ready() checks if the socket has buffered data ready to read. When it returns
// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until
// read() returns 0 to indicate EOF / connection closed, or track that it stopped
// early and retry without calling ready(). The next call to ready() will only
// report new data correctly if all callers fulfill this contract. Failing to
// drain the socket may cause ready() to return false while data remains readable.
//
// In practice each socket is owned by a single component, so this contract is
// straightforward to fulfill — but the owning component must be aware of it,
// especially if it limits how many messages it processes per loop iteration.
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
inline bool Socket::ready() const {
#ifdef USE_LWIP_FAST_SELECT
@@ -9,7 +9,6 @@ static const char *const TAG = "speed.fan";
void SpeedFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_();
if (restore.has_value()) {
+5 -3
View File
@@ -16,8 +16,11 @@ class SpeedFan : public Component, public fan::Fan {
void set_output(output::FloatOutput *output) { this->output_ = output; }
void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; }
void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
fan::FanTraits get_traits() override {
this->wire_preset_modes_(this->traits_);
return this->traits_;
}
protected:
void control(const fan::FanCall &call) override;
@@ -28,7 +31,6 @@ class SpeedFan : public Component, public fan::Fan {
output::BinaryOutput *direction_{nullptr};
int speed_count_{};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};
} // namespace speed
+9
View File
@@ -104,11 +104,17 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
delayMicroseconds(SWITCHING_DELAY_US);
}
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
void SX126x::setup() {
// setup pins
this->busy_pin_->setup();
this->rst_pin_->setup();
this->dio1_pin_->setup();
if (this->dio1_pin_->is_internal()) {
static_cast<InternalGPIOPin *>(this->dio1_pin_)
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
// start spi
this->spi_setup();
@@ -348,6 +354,9 @@ void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
}
void SX126x::loop() {
if (this->dio1_pin_->is_internal()) {
this->disable_loop();
}
if (!this->dio1_pin_->digital_read()) {
return;
}
+2
View File
@@ -3,6 +3,7 @@
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "sx126x_reg.h"
#include <utility>
#include <vector>
@@ -100,6 +101,7 @@ class SX126x : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX126x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
+5 -1
View File
@@ -53,6 +53,8 @@ void SX127x::write_fifo_(const std::vector<uint8_t> &packet) {
this->disable();
}
void IRAM_ATTR SX127x::gpio_intr(SX127x *arg) { arg->enable_loop_soon_any_context(); }
void SX127x::setup() {
// setup reset
this->rst_pin_->setup();
@@ -60,6 +62,7 @@ void SX127x::setup() {
// setup dio0
if (this->dio0_pin_) {
this->dio0_pin_->setup();
this->dio0_pin_->attach_interrupt(&SX127x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
// start spi
@@ -313,6 +316,7 @@ void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
}
void SX127x::loop() {
this->disable_loop();
if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) {
return;
}
@@ -383,7 +387,7 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
if (millis() - start > 20) {
ESP_LOGE(TAG, "Set mode failure");
this->mark_failed();
break;
return;
}
}
}
+2
View File
@@ -4,6 +4,7 @@
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include <vector>
namespace esphome {
@@ -86,6 +87,7 @@ class SX127x : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX127x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_mode_(uint8_t modulation, uint8_t mode);
+4
View File
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(TCA9555Component),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -38,6 +40,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value):
+18 -1
View File
@@ -24,9 +24,18 @@ void TCA9555Component::setup() {
this->mark_failed();
return;
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&TCA9555Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR TCA9555Component::gpio_intr(TCA9555Component *arg) { arg->enable_loop_soon_any_context(); }
void TCA9555Component::dump_config() {
ESP_LOGCONFIG(TAG, "TCA9555:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
@@ -36,6 +45,9 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
// Set mode mask bit
this->mode_mask_ |= 1 << pin;
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
// Clear mode mask bit
this->mode_mask_ &= ~(1 << pin);
@@ -43,7 +55,12 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
// Write GPIO to enable input mode
this->write_gpio_modes_();
}
void TCA9555Component::loop() { this->reset_pin_cache_(); }
void TCA9555Component::loop() {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
bool TCA9555Component::read_gpio_outputs_() {
if (this->is_failed())
+5
View File
@@ -24,7 +24,10 @@ class TCA9555Component : public Component,
void loop() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(TCA9555Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
@@ -39,6 +42,8 @@ class TCA9555Component : public Component,
bool read_gpio_modes_();
bool write_gpio_modes_();
bool read_gpio_outputs_();
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Helper class to expose a TCA9555 pin as an internal input GPIO pin.
@@ -9,7 +9,6 @@ void TemplateFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ =
fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_();
if (restore.has_value()) {
@@ -13,8 +13,11 @@ class TemplateFan final : public Component, public fan::Fan {
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
fan::FanTraits get_traits() override {
this->wire_preset_modes_(this->traits_);
return this->traits_;
}
protected:
void control(const fan::FanCall &call) override;
@@ -23,7 +26,6 @@ class TemplateFan final : public Component, public fan::Fan {
bool has_direction_{false};
int speed_count_{0};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};
} // namespace esphome::template_
@@ -333,15 +333,7 @@ climate::ClimateTraits ThermostatClimate::traits() {
traits.add_supported_preset(entry.preset);
}
// Extract custom preset names from the custom_preset_config_ vector
if (!this->custom_preset_config_.empty()) {
std::vector<const char *> custom_preset_names;
custom_preset_names.reserve(this->custom_preset_config_.size());
for (const auto &entry : this->custom_preset_config_) {
custom_preset_names.push_back(entry.name);
}
traits.set_supported_custom_presets(custom_preset_names);
}
// Custom presets are stored on Climate base class and wired via get_traits()
return traits;
}
@@ -1306,6 +1298,13 @@ void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> pre
void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
this->custom_preset_config_ = presets;
// Populate Climate base class custom presets vector
std::vector<const char *> names;
names.reserve(presets.size());
for (const auto &entry : this->custom_preset_config_) {
names.push_back(entry.name);
}
this->set_supported_custom_presets(names);
}
ThermostatClimate::ThermostatClimate() = default;
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.4.0-dev"
__version__ = "2026.5.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+2 -2
View File
@@ -12,14 +12,14 @@ platformio==6.1.19
esptool==5.2.0
click==8.3.2
esphome-dashboard==20260408.1
aioesphomeapi==44.12.0
aioesphomeapi==44.13.3
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import
esphome-glyphsets==0.2.0
pillow==12.2.0
resvg-py==0.2.6
resvg-py==0.3.1
freetype-py==2.5.1
jinja2==3.1.6
bleak==2.1.1
+1 -1
View File
@@ -1,6 +1,6 @@
pylint==4.0.5
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.15.9 # also change in .pre-commit-config.yaml when updating
ruff==0.15.10 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit
+274 -19
View File
@@ -60,6 +60,10 @@ FILE_HEADER = """// This file was automatically generated with a tool.
# Maps enum type name (e.g. ".BluetoothDeviceRequestType") to max enum value.
_enum_max_values: dict[str, int] = {}
# Populated by main() before message generation.
# Maps message name (e.g. "BluetoothLERawAdvertisement") to its descriptor.
_message_desc_map: dict[str, Any] = {}
def indent_list(text: str, padding: str = " ") -> list[str]:
"""Indent each line of the given text with the specified padding."""
@@ -427,6 +431,23 @@ class TypeInfo(ABC):
Estimated size in bytes including field ID and typical data
"""
def get_max_encoded_size(self) -> int | None:
"""Get the maximum possible encoded size in bytes for this field.
Returns the worst-case encoded size including field ID and maximum
possible value encoding. Returns None if the size is unbounded
(e.g., variable-length strings without max_data_length).
Used by (inline_encode) validation to ensure sub-messages fit in a
single-byte length varint (< 128 bytes).
"""
return None # Unbounded by default
def _varint_max_size(bits: int) -> int:
"""Return the maximum varint encoding size for a value with the given number of bits."""
return (max(bits, 1) + 6) // 7 # ceil(bits / 7), min 1 byte for varint(0)
TYPE_INFO: dict[int, TypeInfo] = {}
@@ -514,8 +535,30 @@ def register_type(name: int):
return func
class FixedSizeTypeMixin:
"""Mixin for types with a known fixed encoded size (float, double, fixed32, fixed64)."""
def get_max_encoded_size(self) -> int:
return self.calculate_field_id_size() + self.get_fixed_size_bytes()
class VarintTypeMixin:
"""Mixin for varint types. Subclasses set _varint_max_bits."""
_varint_max_bits: int = 64 # Default to worst case
def get_max_encoded_size(self) -> int:
max_val = self.max_value
if max_val is not None:
return self.calculate_field_id_size() + _varint_max_size(
max_val.bit_length() if max_val > 0 else 1
)
return self.calculate_field_id_size() + _varint_max_size(self._varint_max_bits)
@register_type(1)
class DoubleType(TypeInfo):
class DoubleType(FixedSizeTypeMixin, TypeInfo):
# Unsupported but defined for completeness
cpp_type = "double"
default_value = "0.0"
decode_64bit = "value.as_double()"
@@ -541,7 +584,7 @@ class DoubleType(TypeInfo):
@register_type(2)
class FloatType(TypeInfo):
class FloatType(FixedSizeTypeMixin, TypeInfo):
cpp_type = "float"
default_value = "0.0f"
decode_32bit = "value.as_float()"
@@ -567,8 +610,9 @@ class FloatType(TypeInfo):
@register_type(3)
class Int64Type(TypeInfo):
class Int64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t"
_varint_max_bits = 64
default_value = "0"
decode_varint = "static_cast<int64_t>(value)"
encode_func = "encode_int64"
@@ -587,8 +631,9 @@ class Int64Type(TypeInfo):
@register_type(4)
class UInt64Type(TypeInfo):
class UInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint64_t"
_varint_max_bits = 64
default_value = "0"
decode_varint = "value"
encode_func = "encode_uint64"
@@ -607,8 +652,9 @@ class UInt64Type(TypeInfo):
@register_type(5)
class Int32Type(TypeInfo):
class Int32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t"
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
default_value = "0"
decode_varint = "static_cast<int32_t>(value)"
encode_func = "encode_int32"
@@ -627,7 +673,7 @@ class Int32Type(TypeInfo):
@register_type(6)
class Fixed64Type(TypeInfo):
class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint64_t"
default_value = "0"
decode_64bit = "value.as_fixed64()"
@@ -653,7 +699,7 @@ class Fixed64Type(TypeInfo):
@register_type(7)
class Fixed32Type(TypeInfo):
class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint32_t"
default_value = "0"
decode_32bit = "value.as_fixed32()"
@@ -689,7 +735,8 @@ class Fixed32Type(TypeInfo):
@register_type(8)
class BoolType(TypeInfo):
class BoolType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 1
cpp_type = "bool"
default_value = "false"
decode_varint = "value != 0"
@@ -807,6 +854,16 @@ class StringType(TypeInfo):
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
def get_max_encoded_size(self) -> int | None:
max_len = self.max_data_length
if max_len is not None:
return (
self.calculate_field_id_size()
+ _varint_max_size(max_len.bit_length())
+ max_len
)
return None # Unbounded
@register_type(11)
class MessageType(TypeInfo):
@@ -1122,6 +1179,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
def get_max_encoded_size(self) -> int | None:
max_len = self.max_data_length
if max_len is not None:
return (
self.calculate_field_id_size()
+ _varint_max_size(max_len.bit_length())
+ max_len
)
return None
class PackedBufferTypeInfo(TypeInfo):
"""Type for packed repeated fields that expose raw buffer instead of decoding.
@@ -1299,14 +1366,23 @@ class FixedArrayBytesType(TypeInfo):
self.calculate_field_id_size() + 1 + 31
) # field ID + length byte + typical 31 bytes
def get_max_encoded_size(self) -> int:
# field_id + varint(array_size) + array_size
return (
self.calculate_field_id_size()
+ _varint_max_size(self.array_size.bit_length())
+ self.array_size
)
@property
def wire_type(self) -> WireType:
return WireType.LENGTH_DELIMITED
@register_type(13)
class UInt32Type(TypeInfo):
class UInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint32_t"
_varint_max_bits = 32
default_value = "0"
decode_varint = "value"
encode_func = "encode_uint32"
@@ -1328,7 +1404,9 @@ class UInt32Type(TypeInfo):
@register_type(14)
class EnumType(TypeInfo):
class EnumType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 32
@property
def cpp_type(self) -> str:
return f"enums::{self._field.type_name[1:]}"
@@ -1379,7 +1457,7 @@ class EnumType(TypeInfo):
@register_type(15)
class SFixed32Type(TypeInfo):
class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int32_t"
default_value = "0"
decode_32bit = "value.as_sfixed32()"
@@ -1405,7 +1483,7 @@ class SFixed32Type(TypeInfo):
@register_type(16)
class SFixed64Type(TypeInfo):
class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int64_t"
default_value = "0"
decode_64bit = "value.as_sfixed64()"
@@ -1431,8 +1509,9 @@ class SFixed64Type(TypeInfo):
@register_type(17)
class SInt32Type(TypeInfo):
class SInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t"
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit
default_value = "0"
decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))"
encode_func = "encode_sint32"
@@ -1451,8 +1530,9 @@ class SInt32Type(TypeInfo):
@register_type(18)
class SInt64Type(TypeInfo):
class SInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t"
_varint_max_bits = 64
default_value = "0"
decode_varint = "decode_zigzag64(value)"
encode_func = "encode_sint64"
@@ -1500,6 +1580,91 @@ def _generate_array_dump_content(
return o
def _is_inline_encode(sub_msg_name: str) -> bool:
"""Check if a sub-message type has the (inline_encode) option set."""
sub_desc = _message_desc_map.get(sub_msg_name)
if not sub_desc:
return False
inline_opt = getattr(pb, "inline_encode", None)
if inline_opt is None:
return False
return get_opt(sub_desc, inline_opt, False)
def _generate_inline_encode_block(
field_number: int, sub_msg_name: str, element: str
) -> str:
"""Generate inline encode code for a sub-message with (inline_encode) = true.
Instead of calling encode_sub_message (function pointer indirection),
this inlines the sub-message's field encoding directly. Uses 1-byte
backpatch for the length (validated to be < 128 at generation time).
Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement
on complex element expressions.
Args:
field_number: The parent field number for this sub-message
sub_msg_name: The sub-message type name
element: C++ expression for the element (e.g., "it" or "this->field[i]")
"""
sub_desc = _message_desc_map[sub_msg_name]
tag = (field_number << 3) | 2 # wire type 2 = LENGTH_DELIMITED
assert tag < 128, f"inline_encode requires single-byte tag, got {tag}"
lines = []
lines.append(f"auto &sub_msg = {element};")
lines.append(f"ProtoEncode::write_raw_byte(pos, {tag});")
lines.append("uint8_t *len_pos = pos;")
lines.append("ProtoEncode::reserve_byte(pos);")
# Generate inline field encoding for each sub-message field
for field in sub_desc.field:
if field.options.deprecated:
continue
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
encode_line = ti.encode_content
# Replace this-> with sub_msg reference for the sub-message fields
encode_line = encode_line.replace("this->", "sub_msg.")
lines.extend(wrap_with_ifdef(encode_line, get_field_opt(field, pb.field_ifdef)))
lines.append("*len_pos = static_cast<uint8_t>(pos - len_pos - 1);")
return "\n".join(lines)
def _generate_inline_size_block(
field_number: int, sub_msg_name: str, element: str
) -> str:
"""Generate inline size calculation for a sub-message with (inline_encode) = true.
Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement
on complex element expressions like 'this->advertisements[i]'.
Args:
field_number: The parent field number for this sub-message
sub_msg_name: The sub-message type name
element: C++ expression for the element
"""
sub_desc = _message_desc_map[sub_msg_name]
lines = []
lines.append(f"auto &sub_msg = {element};")
# 1 byte tag + 1 byte length (guaranteed < 128 by validation)
lines.append("size += 2;")
for field in sub_desc.field:
if field.options.deprecated:
continue
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
force = get_field_opt(field, pb.force, False)
size_line = ti.get_size_calculation(f"sub_msg.{ti.field_name}", force)
# Replace hardcoded this-> references (e.g., FixedArrayBytesType uses this->field_len)
size_line = size_line.replace("this->", "sub_msg.")
lines.extend(wrap_with_ifdef(size_line, get_field_opt(field, pb.field_ifdef)))
return "\n".join(lines)
class FixedArrayRepeatedType(TypeInfo):
"""Special type for fixed-size repeated fields using std::array.
@@ -1526,6 +1691,10 @@ class FixedArrayRepeatedType(TypeInfo):
return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);"
# Repeated message elements use encode_sub_message (force=true is default)
if isinstance(self._ti, MessageType):
if _is_inline_encode(self._ti.cpp_type):
return _generate_inline_encode_block(
self.number, self._ti.cpp_type, element
)
return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});"
return (
f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);"
@@ -1633,8 +1802,19 @@ class FixedArrayRepeatedType(TypeInfo):
]
return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}"
is_inline = isinstance(self._ti, MessageType) and _is_inline_encode(
self._ti.cpp_type
)
# When using a define, always use loop-based approach
if self.is_define:
if is_inline:
o = f"for (const auto &it : {name}) {{\n"
o += indent(
_generate_inline_size_block(self.number, self._ti.cpp_type, "it")
)
o += "\n}"
return o
o = f"for (const auto &it : {name}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n"
o += "}"
@@ -1642,6 +1822,14 @@ class FixedArrayRepeatedType(TypeInfo):
# For fixed arrays, we always encode all elements
if is_inline:
o = f"for (const auto &it : {name}) {{\n"
o += indent(
_generate_inline_size_block(self.number, self._ti.cpp_type, "it")
)
o += "\n}"
return o
# Special case for single-element arrays - no loop needed
if self.array_size == 1:
return self._ti.get_size_calculation(f"{name}[0]", True)
@@ -1714,6 +1902,15 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType):
def get_size_calculation(self, name: str, force: bool = False) -> str:
# Calculate size only for active elements
if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type):
o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n"
o += indent(
_generate_inline_size_block(
self.number, self._ti.cpp_type, f"{name}[i]"
)
)
o += "\n}"
return o
o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n"
o += f" {self._ti.get_size_calculation(f'{name}[i]', True)}\n"
o += "}"
@@ -2222,6 +2419,28 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
return total_size
def calculate_message_max_size(desc: descriptor.DescriptorProto) -> int | None:
"""Calculate the maximum possible encoded size for a message.
Returns None if any field has unbounded size (e.g., variable-length strings).
Used to validate that (inline_encode) messages fit in a single-byte length varint.
"""
total_size = 0
for field in desc.field:
if field.options.deprecated:
continue
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
max_size = ti.get_max_encoded_size()
if max_size is None:
return None
total_size += max_size
return total_size
def build_message_type(
desc: descriptor.DescriptorProto,
base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]],
@@ -2451,11 +2670,23 @@ def build_message_type(
prot = "void decode(const uint8_t *buffer, size_t length);"
public_content.append(prot)
# Check if this message uses inline_encode — if so, skip generating standalone
# encode/calculate_size methods since the encoding is inlined into the parent.
inline_opt = getattr(pb, "inline_encode", None)
is_inline_only = (
message_id is None # Not a service message (no id)
and inline_opt is not None
and get_opt(desc, inline_opt, False)
)
# Only generate encode method if this message needs encoding and has fields
if needs_encode and encode:
if needs_encode and encode and not is_inline_only:
# Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls
encode_debug = [
line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,") for line in encode
line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,").replace(
"(pos)", "(pos PROTO_ENCODE_DEBUG_ARG)"
)
for line in encode
]
o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
@@ -2470,7 +2701,7 @@ def build_message_type(
# If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used
# Add calculate_size method only if this message needs encoding and has fields
if needs_encode and size_calc:
if needs_encode and size_calc and not is_inline_only:
o = f"uint32_t {desc.name}::calculate_size() const {{\n"
o += " uint32_t size = 0;\n"
o += indent("\n".join(size_calc)) + "\n"
@@ -2830,6 +3061,32 @@ def main() -> None:
if not enum.options.deprecated and enum.value:
_enum_max_values[f".{enum.name}"] = max(v.number for v in enum.value)
# Build message descriptor map for inline_encode lookups
mt = file.message_type
_message_desc_map.update({m.name: m for m in mt if not m.options.deprecated})
# Validate inline_encode messages fit in single-byte length varint
inline_encode_opt = getattr(pb, "inline_encode", None)
if inline_encode_opt is not None:
for m in mt:
if m.options.deprecated:
continue
if not get_opt(m, inline_encode_opt, False):
continue
max_size = calculate_message_max_size(m)
if max_size is None:
raise ValueError(
f"Message '{m.name}' has (inline_encode) = true but contains "
f"fields with unbounded size. Inline encoding requires all "
f"fields to have bounded maximum size."
)
if max_size >= 128:
raise ValueError(
f"Message '{m.name}' has (inline_encode) = true but max "
f"encoded size is {max_size} bytes (>= 128). Inline encoding "
f"requires sub-messages that fit in a single-byte length varint."
)
# Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes
enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = (
build_type_usage_map(file)
@@ -3048,8 +3305,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
content += "\n} // namespace enums\n\n"
mt = file.message_type
# Identify empty SOURCE_CLIENT messages that don't need class generation
for m in mt:
if m.options.deprecated:
@@ -75,7 +75,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) {
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
buffer.clear();
MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
MessageInfo messages[5] = {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}};
for (int j = 0; j < 5; j++) {
uint16_t offset = buffer.size();
@@ -89,7 +89,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) {
ProtoWriteBuffer writer(&buffer, offset + padding);
msg.encode(writer);
messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size);
messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size, padding);
}
helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span<const MessageInfo>(messages, 5));
+270 -1
View File
@@ -10,7 +10,6 @@ namespace esphome::benchmarks {
static constexpr int kInnerIterations = 2000;
// --- random_float() ---
// Ported from ol.yaml:148 "Random Float Benchmark"
static void RandomFloat(benchmark::State &state) {
for (auto _ : state) {
@@ -38,4 +37,274 @@ static void RandomUint32(benchmark::State &state) {
}
BENCHMARK(RandomUint32);
// --- format_hex_to() - 6 bytes (MAC address sized) ---
static void FormatHexTo_6Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
char buffer[13]; // 6 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 6);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_6Bytes);
// --- format_hex_to() - 16 bytes (UUID sized) ---
static void FormatHexTo_16Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89,
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
char buffer[33]; // 16 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 16);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_16Bytes);
// --- format_hex_to() - 100 bytes (large payload) ---
static void FormatHexTo_100Bytes(benchmark::State &state) {
uint8_t data[100];
for (int i = 0; i < 100; i++) {
data[i] = static_cast<uint8_t>(i);
}
char buffer[201]; // 100 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 100);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_100Bytes);
// --- format_hex_pretty_to() - 6 bytes with ':' separator ---
static void FormatHexPrettyTo_6Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
char buffer[18]; // 6 * 3
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_pretty_to(buffer, data, 6);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexPrettyTo_6Bytes);
// --- format_mac_addr_upper() ---
static void FormatMacAddrUpper(benchmark::State &state) {
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_mac_addr_upper(mac, buffer);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatMacAddrUpper);
// --- fnv1_hash() - short string ---
static void Fnv1Hash_Short(benchmark::State &state) {
const char *str = "sensor.temperature";
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash(str);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1Hash_Short);
// --- fnv1_hash() - long string ---
static void Fnv1Hash_Long(benchmark::State &state) {
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash(str);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1Hash_Long);
// --- fnv1a_hash() - short string ---
// Use DoNotOptimize on the input pointer to prevent constexpr evaluation
static void Fnv1aHash_Short(benchmark::State &state) {
const char *str = "sensor.temperature";
benchmark::DoNotOptimize(str);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1a_hash(str);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1aHash_Short);
// --- fnv1a_hash() - long string ---
static void Fnv1aHash_Long(benchmark::State &state) {
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
benchmark::DoNotOptimize(str);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1a_hash(str);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1aHash_Long);
// --- fnv1_hash_object_id() - typical entity name ---
static void Fnv1HashObjectId(benchmark::State &state) {
char name[] = "Living Room Temperature Sensor";
size_t len = sizeof(name) - 1;
benchmark::DoNotOptimize(name);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash_object_id(name, len);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1HashObjectId);
// --- parse_hex() - 6 bytes from string ---
static void ParseHex_6Bytes(benchmark::State &state) {
const char *hex_str = "ABCDEF012345";
uint8_t data[6];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
parse_hex(hex_str, data, 6);
}
benchmark::DoNotOptimize(data);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ParseHex_6Bytes);
// --- parse_hex() - 16 bytes from string ---
static void ParseHex_16Bytes(benchmark::State &state) {
const char *hex_str = "ABCDEF0123456789FEDCBA9876543210";
uint8_t data[16];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
parse_hex(hex_str, data, 16);
}
benchmark::DoNotOptimize(data);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ParseHex_16Bytes);
// --- crc8() - 8 bytes ---
static void CRC8_8Bytes(benchmark::State &state) {
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
for (auto _ : state) {
uint8_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= crc8(data, 8);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CRC8_8Bytes);
// --- crc16() - 8 bytes ---
static void CRC16_8Bytes(benchmark::State &state) {
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
for (auto _ : state) {
uint16_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= crc16(data, 8);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CRC16_8Bytes);
// --- value_accuracy_to_buf() - typical sensor value ---
static void ValueAccuracyToBuf(benchmark::State &state) {
char raw_buf[VALUE_ACCURACY_MAX_LEN] = {};
std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf);
float value = 23.456f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
value_accuracy_to_buf(buf, value, 2);
}
benchmark::DoNotOptimize(raw_buf);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ValueAccuracyToBuf);
// --- int8_to_str() ---
static void Int8ToStr(benchmark::State &state) {
char buffer[5] = {};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
int8_to_str(buffer, static_cast<int8_t>(i & 0xFF));
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Int8ToStr);
// --- base64_decode() - into pre-allocated buffer ---
static void Base64Decode_32Bytes(benchmark::State &state) {
// 32 bytes encoded = 44 base64 chars
const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw==";
size_t encoded_len = 44;
uint8_t output[32];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
base64_decode(encoded, encoded_len, output, sizeof(output));
}
benchmark::DoNotOptimize(output);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Base64Decode_32Bytes);
} // namespace esphome::benchmarks
+6 -2
View File
@@ -1,6 +1,10 @@
mcp23016:
i2c_id: i2c_bus
id: mcp23016_hub
- i2c_id: i2c_bus
id: mcp23016_hub
- i2c_id: i2c_bus
id: mcp23016_hub_int
address: 0x21
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
+4
View File
@@ -2,6 +2,10 @@ pca6416a:
- id: pca6416a_hub
i2c_id: i2c_bus
address: 0x21
- id: pca6416a_hub_int
i2c_id: i2c_bus
address: 0x22
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more