From 6e3bc7b1ddb5b8ac91ecf0653087dc835264ca8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 07:33:05 -1000 Subject: [PATCH 001/101] [ci] Use pull_request_target for codeowner approved label workflow (#14561) --- .github/scripts/codeowners.js | 2 +- .../codeowner-approved-label-update.yml | 63 +++++---------- .../workflows/codeowner-approved-label.yml | 78 ------------------- 3 files changed, 21 insertions(+), 122 deletions(-) delete mode 100644 .github/workflows/codeowner-approved-label.yml diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js index 5d69c11b1a..9b2f2922c0 100644 --- a/.github/scripts/codeowners.js +++ b/.github/scripts/codeowners.js @@ -2,7 +2,7 @@ // // Used by: // - codeowner-review-request.yml -// - codeowner-approved-label.yml + codeowner-approved-label-update.yml +// - codeowner-approved-label-update.yml // - auto-label-pr/detectors.js (detectCodeOwner) /** diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 9168cce1d6..c2eb886913 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -1,13 +1,15 @@ -# Fallback for fork PRs: phase 1 (codeowner-approved-label.yml) handles -# non-fork PRs directly but can't write labels on fork PRs (read-only token). -# This workflow re-determines the action and applies it if needed. +# Adds/removes a 'code-owner-approved' label when a component-specific +# codeowner approves (or dismisses) a PR. +# +# Uses pull_request_target so that fork PRs do not require workflow approval. +# The label is reconciled on every PR update; for review events specifically, +# this means the label is applied on the next push after a codeowner review. -name: Codeowner Approved Label Update +name: Codeowner Approved Label on: - workflow_run: - workflows: ["Codeowner Approved Label"] - types: [completed] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] permissions: issues: write @@ -15,51 +17,23 @@ permissions: contents: read jobs: - update-label: + codeowner-approved: name: Run - if: > - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'pull_request_review' + if: ${{ github.repository == 'esphome/esphome' }} runs-on: ubuntu-latest steps: - - name: Get PR details - id: pr - env: - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - REPO: ${{ github.repository }} - run: | - pr_data=$(gh pr list --repo "$REPO" --state open --search "$HEAD_SHA" \ - --json number,baseRefName --jq '.[0] // empty') - - if [ -z "$pr_data" ]; then - echo "No open PR found for SHA $HEAD_SHA, skipping" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - pr_number=$(echo "$pr_data" | jq -r '.number') - base_ref=$(echo "$pr_data" | jq -r '.baseRefName') - - echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" - echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" - echo "Found PR #$pr_number targeting $base_ref" - - - name: Checkout base repository - if: steps.pr.outputs.skip != 'true' + - name: Checkout base branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - repository: ${{ github.repository }} - ref: ${{ steps.pr.outputs.base_ref }} + ref: ${{ github.event.pull_request.base.sha }} sparse-checkout: | .github/scripts/codeowners.js CODEOWNERS - - name: Update label - if: steps.pr.outputs.skip != 'true' + - name: Check codeowner approval and update label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + PR_NUMBER: ${{ github.event.pull_request.number }} with: script: | const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); @@ -76,6 +50,11 @@ jobs: github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME ); + if (action === LabelAction.NONE) { + console.log('No label change needed'); + return; + } + if (action === LabelAction.ADD) { await github.rest.issues.addLabels({ owner, repo, issue_number: pr_number, labels: [LABEL_NAME] @@ -90,6 +69,4 @@ jobs: } catch (error) { if (error.status !== 404) throw error; } - } else { - console.log('No label change needed'); } diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml deleted file mode 100644 index 12199bd0b0..0000000000 --- a/.github/workflows/codeowner-approved-label.yml +++ /dev/null @@ -1,78 +0,0 @@ -# Adds/removes a 'code-owner-approved' label when a component-specific -# codeowner approves (or dismisses) a PR. -# -# Handles non-fork PRs directly. For fork PRs the GITHUB_TOKEN is read-only, -# so label writes are deferred to codeowner-approved-label-update.yml which -# triggers via workflow_run with write permissions. - -name: Codeowner Approved Label - -on: - pull_request_review: - types: [submitted, dismissed] - -permissions: - issues: write - pull-requests: read - contents: read - -jobs: - codeowner-approved: - name: Run - if: ${{ github.repository == 'esphome/esphome' }} - runs-on: ubuntu-latest - steps: - - name: Checkout base branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.base.sha }} - sparse-checkout: | - .github/scripts/codeowners.js - CODEOWNERS - - - name: Check codeowner approval and update label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - with: - script: | - const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); - - const owner = context.repo.owner; - const repo = context.repo.repo; - const pr_number = parseInt(process.env.PR_NUMBER, 10); - const LABEL_NAME = 'code-owner-approved'; - - console.log(`Processing PR #${pr_number} for codeowner approval label`); - - const codeownersPatterns = loadCodeowners(); - const action = await determineLabelAction( - github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME - ); - - if (action === LabelAction.NONE) { - console.log('No label change needed'); - return; - } - - try { - if (action === LabelAction.ADD) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr_number, labels: [LABEL_NAME] - }); - console.log(`Added '${LABEL_NAME}' label`); - } else if (action === LabelAction.REMOVE) { - await github.rest.issues.removeLabel({ - owner, repo, issue_number: pr_number, name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label`); - } - } catch (error) { - if (error.status === 403) { - console.log('Fork PR: deferring label write to phase 2 workflow'); - } else if (error.status === 404) { - console.log('Label already removed'); - } else { - throw error; - } - } From 65b7c73bf3fdbbe9260040b96e758561dc9be548 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 08:02:34 -1000 Subject: [PATCH 002/101] [sgp4x] Fix undefined behavior from mutating entity config at runtime (#14562) Co-authored-by: Claude Opus 4.6 --- esphome/components/sgp4x/sgp4x.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 23589265ca..44d0a54080 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -35,13 +35,9 @@ void SGP4xComponent::setup() { this->self_test_time_ = SPG40_SELFTEST_TIME; this->measure_time_ = SGP40_MEASURE_TIME; if (this->nox_sensor_) { - ESP_LOGE(TAG, "SGP41 required for NOx"); - // disable the sensor - this->nox_sensor_->set_disabled_by_default(true); - // make sure it's not visible in HA - this->nox_sensor_->set_internal(true); - this->nox_sensor_->state = NAN; - // remove pointer to sensor + ESP_LOGE(TAG, "SGP41 required for NOx, disabling NOx sensor"); + // Drop the pointer so update() never publishes to it. + // The entity remains registered but will never receive state updates. this->nox_sensor_ = nullptr; } } else if (featureset == SGP41_FEATURESET) { From b2378e830e947ecc8d79f197abf838868927053e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 19:11:52 +0100 Subject: [PATCH 003/101] [rtttl] Add AudioStreamInfo and set volume (#14439) Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 40 ++++++++++-------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 4ccfc539ea..9bf0450993 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -29,11 +29,6 @@ static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10; static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048; static constexpr uint16_t SAMPLE_RATE = 16000; -struct SpeakerSample { - int8_t left{0}; - int8_t right{0}; -}; - inline double deg2rad(double degrees) { static constexpr double PI_ON_180 = M_PI / 180.0; return degrees * PI_ON_180; @@ -108,6 +103,9 @@ void Rtttl::loop() { } } else if (this->state_ == State::INIT) { if (this->speaker_->is_stopped()) { + audio::AudioStreamInfo audio_stream_info = audio::AudioStreamInfo(16, 1, SAMPLE_RATE); + this->speaker_->set_audio_stream_info(audio_stream_info); + this->speaker_->set_volume(this->gain_); this->speaker_->start(); this->set_state_(State::STARTING); } @@ -120,35 +118,27 @@ void Rtttl::loop() { return; } if (this->samples_sent_ != this->samples_count_) { - SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; + int16_t sample[SAMPLE_BUFFER_SIZE]; uint16_t sample_index = 0; double rem = 0.0; - while (true) { + while (sample_index < SAMPLE_BUFFER_SIZE && this->samples_sent_ < this->samples_count_) { // Try and send out the remainder of the existing note, one per `loop()` if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - - int8_t val = (127 * this->gain_) * sin(deg2rad(rem)); - - sample[sample_index].left = val; - sample[sample_index].right = val; + sample[sample_index] = INT16_MAX * sin(deg2rad(rem)); } else { - sample[sample_index].left = 0; - sample[sample_index].right = 0; - } - - if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { - break; + sample[sample_index] = 0; } this->samples_sent_++; sample_index++; } if (sample_index > 0) { - size_t bytes_to_send = sample_index * sizeof(SpeakerSample); - size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); - if (send != bytes_to_send) { - this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample))); + size_t bytes = sample_index * sizeof(int16_t); + size_t sent_bytes = this->speaker_->play((uint8_t *) (&sample), bytes); + size_t samples_sent = sent_bytes / sizeof(int16_t); + if (samples_sent != sample_index) { + this->samples_sent_ -= (sample_index - samples_sent); } return; } @@ -408,11 +398,7 @@ void Rtttl::finish_() { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; + int16_t sample[2] = {0, 0}; this->speaker_->play((uint8_t *) (&sample), sizeof(sample)); this->speaker_->finish(); this->set_state_(State::STOPPING); From 8a915dcbbed3af2e285dce91f32c03743310ca21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 08:34:27 -1000 Subject: [PATCH 004/101] [core] Move device class strings to PROGMEM on ESP8266 (#14443) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 84 ++++++++++++++----- esphome/components/api/api_connection.h | 34 ++------ .../components/mqtt/mqtt_binary_sensor.cpp | 6 +- esphome/components/mqtt/mqtt_button.cpp | 6 -- esphome/components/mqtt/mqtt_component.cpp | 5 ++ esphome/components/mqtt/mqtt_cover.cpp | 7 +- esphome/components/mqtt/mqtt_event.cpp | 7 -- esphome/components/mqtt/mqtt_number.cpp | 4 - esphome/components/mqtt/mqtt_sensor.cpp | 5 -- esphome/components/mqtt/mqtt_text_sensor.cpp | 6 -- esphome/components/mqtt/mqtt_valve.cpp | 7 +- esphome/components/web_server/web_server.cpp | 3 +- esphome/core/config.py | 6 ++ esphome/core/entity_base.cpp | 37 +++++++- esphome/core/entity_base.h | 33 ++++++-- esphome/core/entity_helpers.py | 8 +- tests/unit_tests/core/test_entity_helpers.py | 17 ++++ 17 files changed, 167 insertions(+), 108 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 98ba1abe0b..77920432c0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -396,6 +396,48 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess return static_cast(header_padding + calculated_size + footer_size); } +uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + uint8_t message_type, APIConnection *conn, + uint32_t remaining_size) { + // Set common fields that are shared by all entity types + msg.key = entity->get_object_id_hash(); + + // API 1.14+ clients compute object_id client-side from the entity name + // For older clients, we must send object_id for backward compatibility + // See: https://github.com/esphome/backlog/issues/76 + // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then + // Buffer must remain in scope until encode_message_to_buffer is called + char object_id_buf[OBJECT_ID_MAX_LEN]; + if (!conn->client_supports_api_version(1, 14)) { + msg.object_id = entity->get_object_id_to(object_id_buf); + } + + if (entity->has_own_name()) { + msg.name = entity->get_name(); + } + + // Set common EntityBase properties +#ifdef USE_ENTITY_ICON + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); +#endif + msg.disabled_by_default = entity->is_disabled_by_default(); + msg.entity_category = static_cast(entity->get_entity_category()); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_message_to_buffer(msg, message_type, conn, remaining_size); +} + +uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, + uint8_t message_type, APIConnection *conn, + uint32_t remaining_size) { + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + device_class_field = StringRef(entity->get_device_class_to(dc_buf)); + return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size); +} + #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE, @@ -414,10 +456,9 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; - msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info_with_device_class( + binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -443,8 +484,8 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, + ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -609,9 +650,9 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, + ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -631,8 +672,8 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * auto *a_switch = static_cast(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, + ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -661,9 +702,8 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; - msg.device_class = text_sensor->get_device_class_ref(); - return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info_with_device_class( + text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -776,11 +816,11 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * ListEntitiesNumberResponse msg; msg.unit_of_measurement = number->get_unit_of_measurement_ref(); msg.mode = static_cast(number->traits.get_mode()); - msg.device_class = number->get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, + ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -925,8 +965,8 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast(entity); ListEntitiesButtonResponse msg; - msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, + ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -986,11 +1026,11 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c auto *valve = static_cast(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.device_class = valve->get_device_class_ref(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, + ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1434,9 +1474,9 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; - msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, + ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1492,8 +1532,8 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; - msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, + ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 88f0ef82d6..2c66a194a6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -334,36 +334,12 @@ class APIConnection final : public APIServerConnectionBase { // Helper to fill entity info base and encode message static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { - // Set common fields that are shared by all entity types - msg.key = entity->get_object_id_hash(); + APIConnection *conn, uint32_t remaining_size); - // API 1.14+ clients compute object_id client-side from the entity name - // For older clients, we must send object_id for backward compatibility - // See: https://github.com/esphome/backlog/issues/76 - // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called - char object_id_buf[OBJECT_ID_MAX_LEN]; - if (!conn->client_supports_api_version(1, 14)) { - msg.object_id = entity->get_object_id_to(object_id_buf); - } - - if (entity->has_own_name()) { - msg.name = entity->get_name(); - } - - // Set common EntityBase properties -#ifdef USE_ENTITY_ICON - char icon_buf[MAX_ICON_LENGTH]; - msg.icon = StringRef(entity->get_icon_to(icon_buf)); -#endif - msg.disabled_by_default = entity->is_disabled_by_default(); - msg.entity_category = static_cast(entity->get_entity_category()); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); - } + // Wrapper for entity types that have a device_class field + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size); #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 75995f61e0..ebb29db44f 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,15 +30,11 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->binary_sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_OFF] = mqtt::global_mqtt_client->get_availability().payload_not_available; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } bool MQTTBinarySensorComponent::send_initial_state() { diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 718fe93016..7e0ae7d06e 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -30,13 +30,7 @@ void MQTTButtonComponent::dump_config() { } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - const auto device_class = this->button_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button") diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index d31a78b090..afc514609c 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -214,6 +214,11 @@ bool MQTTComponent::send_discovery_() { if (icon[0] != '\0') { root[MQTT_ICON] = icon; } + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + const char *dc = this->get_entity()->get_device_class_to(dc_buf); + if (dc[0] != '\0') { + root[MQTT_DEVICE_CLASS] = dc; + } const auto entity_category = this->get_entity()->get_entity_category(); if (entity_category != ENTITY_CATEGORY_NONE) { diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 9752004094..ddb4b2d69d 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -91,12 +91,6 @@ void MQTTCoverComponent::dump_config() { } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->cover_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; @@ -129,6 +123,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); } } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 37d5c2551a..93ff6971b3 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -20,13 +20,6 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->event_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a2734f2beb..b0bac8b3d7 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -57,10 +57,6 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = NumberMqttModeStrings::get_progmem_str(static_cast(mode), static_cast(NUMBER_MODE_BOX)); } - const auto device_class = this->number_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index a7d311d194..c66465dd16 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,11 +44,6 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - if (this->sensor_->has_accuracy_decimals()) { root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index a6b9f90b68..3acd71b50d 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -14,12 +14,6 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2b9f02858b..b155a4c897 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -64,12 +64,6 @@ void MQTTValveComponent::dump_config() { } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->valve_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; @@ -78,6 +72,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve") diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index bc90c88e57..5590e67b82 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2137,7 +2137,8 @@ json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } - root[ESPHOME_F("device_class")] = obj->get_device_class_ref(); + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + root[ESPHOME_F("device_class")] = obj->get_device_class_to(dc_buf); this->add_sorting_info_(root, obj); } diff --git a/esphome/core/config.py b/esphome/core/config.py index 8631726a02..d4a839cb79 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -223,6 +223,12 @@ else: # Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h FRIENDLY_NAME_MAX_LEN = 120 +# Max device class string length (47 chars + null = 48-byte PROGMEM buffer) +# Keep in sync with MAX_DEVICE_CLASS_LENGTH in esphome/core/entity_base.h: +# DEVICE_CLASS_MAX_LENGTH == MAX_DEVICE_CLASS_LENGTH - 1 (C++ includes the null) +DEVICE_CLASS_MAX_LENGTH = 47 + + # Max icon string length (63 chars + null = 64-byte PROGMEM buffer) # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 37e7fcc998..5c4e1c4445 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -51,7 +51,27 @@ __attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return " __attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; } __attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; } -// Entity device class (from index) +// Entity device class — buffer-based API for PROGMEM safety on ESP8266 +const char *EntityBase::get_device_class_to([[maybe_unused]] std::span buffer) const { +#ifdef USE_ENTITY_DEVICE_CLASS + const uint8_t idx = this->device_class_idx_; +#else + const uint8_t idx = 0; +#endif +#ifdef USE_ESP8266 + if (idx == 0) + return ""; + const char *dc = entity_device_class_lookup(idx); + ESPHOME_strncpy_P(buffer.data(), dc, buffer.size() - 1); + buffer[buffer.size() - 1] = '\0'; + return buffer.data(); +#else + return entity_device_class_lookup(idx); +#endif +} + +#ifndef USE_ESP8266 +// Deprecated device class accessors — not available on ESP8266 (rodata is RAM) StringRef EntityBase::get_device_class_ref() const { #ifdef USE_ENTITY_DEVICE_CLASS return StringRef(entity_device_class_lookup(this->device_class_idx_)); @@ -59,7 +79,14 @@ StringRef EntityBase::get_device_class_ref() const { return StringRef(entity_device_class_lookup(0)); #endif } -std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); } +std::string EntityBase::get_device_class() const { +#ifdef USE_ENTITY_DEVICE_CLASS + return std::string(entity_device_class_lookup(this->device_class_idx_)); +#else + return std::string(entity_device_class_lookup(0)); +#endif +} +#endif // !USE_ESP8266 // Entity unit of measurement (from index) StringRef EntityBase::get_unit_of_measurement_ref() const { @@ -191,8 +218,10 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) #endif void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) { - if (!obj.get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + const char *dc = obj.get_device_class_to(dc_buf); + if (dc[0] != '\0') { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, dc); } } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1ce1e658e0..20eb68b67a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -36,6 +36,11 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; +// Maximum device class string buffer size (47 chars + null terminator) +// Longest standard device class: "volatile_organic_compounds_parts" (32 chars) +// Device classes are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. +static constexpr size_t MAX_DEVICE_CLASS_LENGTH = 48; + // Maximum icon string buffer size (63 chars + null terminator) // Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. static constexpr size_t MAX_ICON_LENGTH = 64; @@ -113,13 +118,31 @@ class EntityBase { #endif } - // Get device class as StringRef (from packed index) + // Get this entity's device class into a stack buffer. + // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). + // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. + const char *get_device_class_to(std::span buffer) const; + +#ifdef USE_ESP8266 + // On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed + // directly as const char*. Use get_device_class_to() with a stack buffer instead. + template StringRef get_device_class_ref() const { + static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). " + "Use get_device_class_to() with a stack buffer."); + return StringRef(""); + } + template std::string get_device_class() const { + static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). " + "Use get_device_class_to() with a stack buffer."); + return ""; + } +#else + // Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM. + ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_device_class_ref() const; - /// Get the device class as std::string (deprecated, prefer get_device_class_ref()) - ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " - "ESPHome 2026.9.0", - "2026.3.0") + ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") std::string get_device_class() const; +#endif // Get unit of measurement as StringRef (from packed index) StringRef get_unit_of_measurement_ref() const; /// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref()) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 01fa27b833..a46d2466fd 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,7 +17,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.config import ICON_MAX_LENGTH +from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -132,7 +132,7 @@ def _generate_category_code( _CATEGORY_CONFIGS = ( - ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", True), ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False), ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True), ) @@ -179,6 +179,10 @@ def _register_string( def register_device_class(value: str) -> int: """Register a device_class string and return its 1-based index.""" + if value and len(value) > DEVICE_CLASS_MAX_LENGTH: + raise ValueError( + f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'" + ) return _register_string( value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" ) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 79bc3095b9..1392a1d043 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_device_class, register_icon, setup_entity, ) @@ -926,6 +927,22 @@ def test_register_icon_max_length() -> None: assert register_icon("") == 0 +def test_register_device_class_max_length() -> None: + """Test register_device_class rejects device classes exceeding 47 characters.""" + # 47 chars should succeed + max_dc = "a" * 47 + idx = register_device_class(max_dc) + assert idx > 0 + + # 48 chars should fail + too_long = "a" * 48 + with pytest.raises(ValueError, match="Device class string too long"): + register_device_class(too_long) + + # Empty string returns 0 + assert register_device_class("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], From 9654140c00fecc7c86c554c7d733436c2505d2b5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:00:46 -0500 Subject: [PATCH 005/101] [tm1638][rp2040_pio_led_strip][atm90e32] Fix bounds checks and off-by-one (#14559) Co-authored-by: Claude Opus 4.6 --- esphome/components/atm90e32/atm90e32.cpp | 4 +-- .../rp2040_pio_led_strip/led_strip.cpp | 2 +- esphome/components/tm1638/tm1638.cpp | 29 ++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 412964d0f8..ee7fe5ce75 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { - uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); + uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, phase_labels[phase]); @@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, phase_labels[phase]); } else { - uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); + uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index dc0d3c315a..fdb49fb3ef 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -70,7 +70,7 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] >= 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 8ef546ff32..c67ff1adbc 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -147,35 +147,38 @@ void TM1638Component::set_intensity(uint8_t brightness_level) { uint8_t TM1638Component::print(uint8_t start_pos, const char *str) { uint8_t pos = start_pos; - bool last_was_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1638_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') { - data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); // subract 32 to account for ASCII offset - } else if (data == TM1638_UNKNOWN_CHAR) { + // Subtract 32 to account for ASCII offset + data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); + } else { ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str); } - if (*str == '.') // handle dots - { - if (pos != start_pos && - !last_was_dot) // if we are not at the first position, backup by one unless last char was a dot - { + if (*str == '.') { + // Merge dot onto previous character unless we're at the start or last was also a dot + if (pos != start_pos && !last_was_dot) { pos--; } - this->buffer_[pos] |= 0b10000000; // turn on the dot on the previous position - last_was_dot = true; // set a bit in case the next chracter is also a dot - } else // if not a dot, then just write the character to display - { + if (pos >= 8) { + ESP_LOGI(TAG, "TM1638 String is too long for the display!"); + break; + } + // Turn on the dot on the previous position + this->buffer_[pos] |= 0b10000000; + last_was_dot = true; + } else { + // Not a dot, write the character to display if (pos >= 8) { ESP_LOGI(TAG, "TM1638 String is too long for the display!"); break; } this->buffer_[pos] = data; - last_was_dot = false; // clear dot tracking bit + last_was_dot = false; } pos++; From 42dbb51022a61e07b26d82b6d348f470d2afa72f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 09:03:54 -1000 Subject: [PATCH 006/101] [api] Devirtualize protobuf encode/calculate_size (#14449) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 274 ++-- esphome/components/api/api_connection.h | 94 +- esphome/components/api/api_pb2.cpp | 1436 ++++++++++------- esphome/components/api/api_pb2.h | 352 ++-- esphome/components/api/api_pb2_service.h | 8 - esphome/components/api/api_server.cpp | 8 +- esphome/components/api/api_server.h | 2 +- esphome/components/api/list_entities.cpp | 2 +- esphome/components/api/proto.h | 421 +---- .../bluetooth_proxy/bluetooth_connection.cpp | 21 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 18 +- .../voice_assistant/voice_assistant.cpp | 11 +- .../components/zwave_proxy/zwave_proxy.cpp | 6 +- script/api_protobuf/api_protobuf.py | 107 +- 14 files changed, 1373 insertions(+), 1387 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 77920432c0..8721072e49 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -275,7 +275,7 @@ void APIConnection::check_keepalive_(uint32_t now) { // Only send ping if we're not disconnecting ESP_LOGVV(TAG, "Sending keepalive PING"); PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); + this->flags_.sent_ping = this->send_message(req); if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority @@ -336,7 +336,7 @@ bool APIConnection::send_disconnect_response_() { this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; - return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_disconnect_response() { // Don't close socket here, let APIServer::loop() do it @@ -344,61 +344,19 @@ void APIConnection::on_disconnect_response() { this->flags_.remove = true; } -// 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_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size) { -#ifdef HAS_PROTO_MESSAGE_DUMP - // If in log-only mode, just log and return - if (conn->flags_.log_only_mode) { - DumpBuffer dump_buf; - conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); - return 1; // Return non-zero to indicate "success" for logging - } +uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { + msg.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); #endif - - // Calculate size - uint32_t calculated_size = msg.calculated_size(); - - // 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(); - - // 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 - } - - // Get buffer size after allocation (which includes header padding) - std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); - - if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller, just clear flag - conn->flags_.batch_first_message = false; - } else { - // Batch message second or later - // Add padding for previous message footer + this message header - size_t current_size = shared_buf.size(); - shared_buf.reserve(current_size + total_calculated_size); - shared_buf.resize(current_size + footer_size + header_padding); - } - - // Pre-resize buffer to include payload, then encode through raw pointer - size_t write_start = shared_buf.size(); - shared_buf.resize(write_start + calculated_size); - ProtoWriteBuffer buffer{&shared_buf, write_start}; - msg.encode(buffer); - - // Return total size (header + payload + footer) - return static_cast(header_padding + calculated_size + footer_size); + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, - uint8_t message_type, APIConnection *conn, - uint32_t remaining_size) { + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); @@ -406,7 +364,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp // For older clients, we must send object_id for backward compatibility // See: https://github.com/esphome/backlog/issues/76 // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called + // Buffer must remain in scope until encode_to_buffer is called char object_id_buf[OBJECT_ID_MAX_LEN]; if (!conn->client_supports_api_version(1, 14)) { msg.object_id = entity->get_object_id_to(object_id_buf); @@ -426,16 +384,17 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, StringRef &device_class_field, - uint8_t message_type, APIConnection *conn, + CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, uint32_t remaining_size) { char dc_buf[MAX_DEVICE_CLASS_LENGTH]; device_class_field = StringRef(entity->get_device_class_to(dc_buf)); - return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size); + return fill_and_encode_entity_info(entity, msg, size_fn, encode_fn, conn, remaining_size); } #ifdef USE_BINARY_SENSOR @@ -449,16 +408,14 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_state(binary_sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info_with_device_class( - binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(binary_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -474,7 +431,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(cover, msg, conn, remaining_size); } uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast(entity); @@ -484,8 +441,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, - ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -517,7 +473,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(fan, msg, conn, remaining_size); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast(entity); @@ -528,7 +484,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(fan, msg, conn, remaining_size); } void APIConnection::on_fan_command_request(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -571,7 +527,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(light, resp, conn, remaining_size); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast(entity); @@ -596,7 +552,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(light, msg, conn, remaining_size); } void APIConnection::on_light_command_request(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -641,7 +597,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -651,8 +607,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); msg.state_class = static_cast(sensor->get_state_class()); - return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, - ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -665,15 +620,14 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection auto *a_switch = static_cast(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size); } uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, - ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -697,13 +651,12 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(text_sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; - return fill_and_encode_entity_info_with_device_class( - text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(text_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -743,7 +696,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(climate, resp, conn, remaining_size); } uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast(entity); @@ -770,7 +723,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(climate, msg, conn, remaining_size); } void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -808,7 +761,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(number, resp, conn, remaining_size); } uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -819,8 +772,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, - ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -840,12 +792,12 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(date, resp, conn, remaining_size); } uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(date, msg, conn, remaining_size); } void APIConnection::on_date_command_request(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -865,12 +817,12 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(time, resp, conn, remaining_size); } uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(time, msg, conn, remaining_size); } void APIConnection::on_time_command_request(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -892,12 +844,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(datetime, resp, conn, remaining_size); } uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(datetime, msg, conn, remaining_size); } void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -916,7 +868,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(text, resp, conn, remaining_size); } uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -926,7 +878,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(text, msg, conn, remaining_size); } void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -945,14 +897,14 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(select, resp, conn, remaining_size); } uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(select, msg, conn, remaining_size); } void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -965,8 +917,7 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast(entity); ListEntitiesButtonResponse msg; - return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, - ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, conn, remaining_size); } void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -983,7 +934,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(a_lock, resp, conn, remaining_size); } uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -992,7 +943,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(a_lock, msg, conn, remaining_size); } void APIConnection::on_lock_command_request(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -1020,7 +971,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(valve, resp, conn, remaining_size); } uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast(entity); @@ -1029,8 +980,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, - ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1056,7 +1006,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(media_player, resp, conn, remaining_size); } uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast(entity); @@ -1073,8 +1023,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.purpose = static_cast(supported_format.purpose); media_format.sample_bytes = supported_format.sample_bytes; } - return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info(media_player, msg, conn, remaining_size); } void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1115,7 +1064,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message(msg)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1141,7 +1090,7 @@ void APIConnection::set_camera_state(std::shared_ptr image) uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(camera, msg, conn, remaining_size); } void APIConnection::on_camera_image_request(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1296,7 +1245,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } auto &config = voice_assistant::global_voice_assistant->get_configuration(); @@ -1328,7 +1277,7 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { if (!this->send_voice_assistant_get_configuration_response_(msg)) { @@ -1363,8 +1312,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_state(a_alarm_control_panel, resp, conn, remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -1373,8 +1321,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); - return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size); + return fill_and_encode_entity_info(a_alarm_control_panel, msg, conn, remaining_size); } void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1421,7 +1368,7 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); @@ -1432,7 +1379,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(wh, msg, conn, remaining_size); } void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { @@ -1468,15 +1415,14 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(event, resp, conn, remaining_size); } uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, - ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, conn, remaining_size); } #endif @@ -1493,9 +1439,7 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF #endif } -void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { - this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE); -} +void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif #ifdef USE_INFRARED @@ -1503,7 +1447,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection auto *infrared = static_cast(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif @@ -1527,13 +1471,12 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(update, resp, conn, remaining_size); } uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; - return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, - ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1559,7 +1502,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast(level); msg.set_message(reinterpret_cast(line), message_len); - return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message(msg); } void APIConnection::complete_authentication_() { @@ -1616,12 +1559,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); - return this->send_message(resp, HelloResponse::MESSAGE_TYPE); + return this->send_message(resp); } bool APIConnection::send_ping_response_() { PingResponse resp; - return this->send_message(resp, PingResponse::MESSAGE_TYPE); + return this->send_message(resp); } bool APIConnection::send_device_info_response_() { @@ -1745,7 +1688,7 @@ bool APIConnection::send_device_info_response_() { } #endif - return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_hello_request(const HelloRequest &msg) { if (!this->send_hello_response_(msg)) { @@ -1845,7 +1788,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.call_id = call_id; resp.success = success; resp.error_message = error_message; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, @@ -1856,7 +1799,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.error_message = error_message; resp.response_data = response_data; resp.response_data_len = response_data_len; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1895,7 +1838,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio resp.success = true; } - return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { if (!this->send_noise_encryption_set_key_response_(msg)) { @@ -1924,16 +1867,73 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } -bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { - uint32_t payload_size = msg.calculated_size(); - std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); +bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, + const void *msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise) + if (message_type != SubscribeLogsResponse::MESSAGE_TYPE +#ifdef USE_CAMERA + && message_type != CameraImageResponse::MESSAGE_TYPE +#endif + ) { + auto *proto_msg = static_cast(msg); + DumpBuffer dump_buf; + this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + } +#endif + auto &shared_buf = this->parent_->get_shared_buffer_ref(); this->prepare_first_message_buffer(shared_buf, payload_size); size_t write_start = shared_buf.size(); shared_buf.resize(write_start + payload_size); ProtoWriteBuffer buffer{&shared_buf, write_start}; - msg.encode(buffer); + encode_fn(msg, buffer); 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(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(); + + // 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 + + std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); + + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; + } else { + // Batch message second or later + // Add padding for previous message footer + this message header + size_t current_size = shared_buf.size(); + shared_buf.reserve(current_size + total_calculated_size); + shared_buf.resize(current_size + footer_size + header_padding); + } + + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + encode_fn(msg, buffer); + + // Return total size (header + payload + footer) + return static_cast(header_padding + calculated_size + footer_size); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -2292,17 +2292,17 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(resp, conn, remaining_size); } uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(req, conn, remaining_size); } uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(req, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -2321,7 +2321,7 @@ void APIConnection::process_state_subscriptions_() { resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""); resp.once = it.once; - if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { + if (this->send_message(resp)) { this->state_subs_at_++; } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2c66a194a6..54b6db6800 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -129,7 +129,7 @@ class APIConnection final : public APIServerConnectionBase { void send_homeassistant_action(const HomeassistantActionRequest &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); + this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; @@ -153,7 +153,7 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; - this->send_message(req, GetTimeRequest::MESSAGE_TYPE); + this->send_message(req); } #endif @@ -263,7 +263,19 @@ class APIConnection final : public APIServerConnectionBase { void on_fatal_error() override; void on_no_setup_connection() override; - bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override; + + // Function pointer type for type-erased message encoding + using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); + // Function pointer type for type-erased size calculation + using CalculateSizeFn = uint32_t (*)(const void *); + + template bool send_message(const T &msg) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg); + } else { + return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg, &msg); + } + } void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); @@ -318,28 +330,68 @@ class APIConnection final : public APIServerConnectionBase { void process_state_subscriptions_(); #endif - // Non-template helper to encode any ProtoMessage - static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size); - - // Helper to fill entity state base and encode message - static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { - msg.key = entity->get_object_id_hash(); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); + // Size thunk — converts void* back to concrete type for direct calculate_size() call + template static uint32_t calc_size(const void *msg) { + return static_cast(msg)->calculate_size(); } - // Helper to fill entity info base and encode message - static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size); + // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0) + static void encode_msg_noop(const void *, ProtoWriteBuffer &) {} - // Wrapper for entity types that have a device_class field + // 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); + + // Thin template wrapper — computes size, delegates buffer work to non-template helper + template 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); + } else { + return encode_to_buffer(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); + } + } + + // Non-template core — fills state fields and encodes + static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template + static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_state(entity, msg, &calc_size, &proto_encode_msg, conn, remaining_size); + } + + // Non-template core — fills info fields, allocates buffers, and encodes + static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template + static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info(entity, msg, &calc_size, &proto_encode_msg, conn, remaining_size); + } + + // Non-template core — fills device_class, then delegates to fill_and_encode_entity_info static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, - StringRef &device_class_field, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size); + StringRef &device_class_field, CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg, + StringRef &device_class_field, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size, + &proto_encode_msg, conn, remaining_size); + } #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9e74d5ddc7..d8703aa416 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -37,20 +37,24 @@ void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(3, this->server_info); buffer.encode_string(4, this->name); } -void HelloResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->api_version_major); - size.add_uint32(1, this->api_version_minor); - size.add_length(1, this->server_info.size()); - size.add_length(1, this->name.size()); +uint32_t HelloResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->api_version_major); + size += ProtoSize::calc_uint32(1, this->api_version_minor); + size += ProtoSize::calc_length(1, this->server_info.size()); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } -void AreaInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->area_id); - size.add_length(1, this->name.size()); +uint32_t AreaInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->area_id); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #endif #ifdef USE_DEVICES @@ -59,10 +63,12 @@ void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); } -void DeviceInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->device_id); - size.add_length(1, this->name.size()); - size.add_uint32(1, this->area_id); +uint32_t DeviceInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->device_id); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, this->area_id); + return size; } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { @@ -120,60 +126,62 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(24, this->zwave_home_id); #endif } -void DeviceInfoResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_length(1, this->mac_address.size()); - size.add_length(1, this->esphome_version.size()); - size.add_length(1, this->compilation_time.size()); - size.add_length(1, this->model.size()); +uint32_t DeviceInfoResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->mac_address.size()); + size += ProtoSize::calc_length(1, this->esphome_version.size()); + size += ProtoSize::calc_length(1, this->compilation_time.size()); + size += ProtoSize::calc_length(1, this->model.size()); #ifdef USE_DEEP_SLEEP - size.add_bool(1, this->has_deep_sleep); + size += ProtoSize::calc_bool(1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_name.size()); + size += ProtoSize::calc_length(1, this->project_name.size()); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_version.size()); + size += ProtoSize::calc_length(1, this->project_version.size()); #endif #ifdef USE_WEBSERVER - size.add_uint32(1, this->webserver_port); + size += ProtoSize::calc_uint32(1, this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_uint32(1, this->bluetooth_proxy_feature_flags); + size += ProtoSize::calc_uint32(1, this->bluetooth_proxy_feature_flags); #endif - size.add_length(1, this->manufacturer.size()); - size.add_length(1, this->friendly_name.size()); + size += ProtoSize::calc_length(1, this->manufacturer.size()); + size += ProtoSize::calc_length(1, this->friendly_name.size()); #ifdef USE_VOICE_ASSISTANT - size.add_uint32(2, this->voice_assistant_feature_flags); + size += ProtoSize::calc_uint32(2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - size.add_length(2, this->suggested_area.size()); + size += ProtoSize::calc_length(2, this->suggested_area.size()); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_length(2, this->bluetooth_mac_address.size()); + size += ProtoSize::calc_length(2, this->bluetooth_mac_address.size()); #endif #ifdef USE_API_NOISE - size.add_bool(2, this->api_encryption_supported); + size += ProtoSize::calc_bool(2, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS - size.add_message_object(2, this->area); + size += ProtoSize::calc_message(2, this->area.calculate_size()); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_proxy_feature_flags); + size += ProtoSize::calc_uint32(2, this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_home_id); + size += ProtoSize::calc_uint32(2, this->zwave_home_id); #endif + return size; } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { @@ -191,20 +199,22 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->is_status_binary_sensor); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -214,13 +224,15 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t BinarySensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_COVER @@ -242,23 +254,25 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_tilt); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCoverResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_tilt); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -269,14 +283,16 @@ void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void CoverStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_float(1, this->tilt); - size.add_uint32(1, static_cast(this->current_operation)); +uint32_t CoverStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_float(1, this->tilt); + size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -337,27 +353,29 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_oscillation); - size.add_bool(1, this->supports_speed); - size.add_bool(1, this->supports_direction); - size.add_int32(1, this->supported_speed_count); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesFanResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_oscillation); + size += ProtoSize::calc_bool(1, this->supports_speed); + size += ProtoSize::calc_bool(1, this->supports_direction); + size += ProtoSize::calc_int32(1, this->supported_speed_count); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { for (const char *it : *this->supported_preset_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -370,16 +388,18 @@ void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void FanStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->oscillating); - size.add_uint32(1, static_cast(this->direction)); - size.add_int32(1, this->speed_level); - size.add_length(1, this->preset_mode.size()); +uint32_t FanStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->oscillating); + size += ProtoSize::calc_uint32(1, static_cast(this->direction)); + size += ProtoSize::calc_int32(1, this->speed_level); + size += ProtoSize::calc_length(1, this->preset_mode.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -464,30 +484,32 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLightResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } - size.add_float(1, this->min_mireds); - size.add_float(1, this->max_mireds); + size += ProtoSize::calc_float(1, this->min_mireds); + size += ProtoSize::calc_float(1, this->max_mireds); if (!this->effects->empty()) { for (const char *it : *this->effects) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -507,23 +529,25 @@ void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void LightStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_float(1, this->brightness); - size.add_uint32(1, static_cast(this->color_mode)); - size.add_float(1, this->color_brightness); - size.add_float(1, this->red); - size.add_float(1, this->green); - size.add_float(1, this->blue); - size.add_float(1, this->white); - size.add_float(1, this->color_temperature); - size.add_float(1, this->cold_white); - size.add_float(1, this->warm_white); - size.add_length(1, this->effect.size()); +uint32_t LightStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_float(1, this->brightness); + size += ProtoSize::calc_uint32(1, static_cast(this->color_mode)); + size += ProtoSize::calc_float(1, this->color_brightness); + size += ProtoSize::calc_float(1, this->red); + size += ProtoSize::calc_float(1, this->green); + size += ProtoSize::calc_float(1, this->blue); + size += ProtoSize::calc_float(1, this->white); + size += ProtoSize::calc_float(1, this->color_temperature); + size += ProtoSize::calc_float(1, this->cold_white); + size += ProtoSize::calc_float(1, this->warm_white); + size += ProtoSize::calc_length(1, this->effect.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -653,23 +677,25 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_length(1, this->unit_of_measurement.size()); - size.add_int32(1, this->accuracy_decimals); - size.add_bool(1, this->force_update); - size.add_length(1, this->device_class.size()); - size.add_uint32(1, static_cast(this->state_class)); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_int32(1, this->accuracy_decimals); + size += ProtoSize::calc_bool(1, this->force_update); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->state_class)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -679,13 +705,15 @@ void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void SensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t SensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_SWITCH @@ -704,20 +732,22 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSwitchResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -726,12 +756,14 @@ void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void SwitchStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SwitchStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -774,19 +806,21 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -796,13 +830,15 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void TextSensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextSensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -822,9 +858,11 @@ void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } -void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast(this->level)); - size.add_length(1, this->message_len_); +uint32_t SubscribeLogsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast(this->level)); + size += ProtoSize::calc_length(1, this->message_len_); + return size; } #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { @@ -840,16 +878,22 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD return true; } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } -void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } -void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { - size.add_length(1, this->key.size()); - size.add_length(1, this->value.size()); +uint32_t HomeassistantServiceMap::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->key.size()); + size += ProtoSize::calc_length(1, this->value.size()); + return size; } void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); @@ -873,21 +917,35 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(8, this->response_template); #endif } -void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { - size.add_length(1, this->service.size()); - size.add_repeated_message(1, this->data); - size.add_repeated_message(1, this->data_template); - size.add_repeated_message(1, this->variables); - size.add_bool(1, this->is_event); +uint32_t HomeassistantActionRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->service.size()); + if (!this->data.empty()) { + for (const auto &it : this->data) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->data_template.empty()) { + for (const auto &it : this->data_template) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->variables.empty()) { + for (const auto &it : this->variables) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_bool(1, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - size.add_uint32(1, this->call_id); + size += ProtoSize::calc_uint32(1, this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_bool(1, this->wants_response); + size += ProtoSize::calc_bool(1, this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_length(1, this->response_template.size()); + size += ProtoSize::calc_length(1, this->response_template.size()); #endif + return size; } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -929,10 +987,12 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); } -void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->entity_id.size()); - size.add_length(1, this->attribute.size()); - size.add_bool(1, this->once); +uint32_t SubscribeHomeAssistantStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->entity_id.size()); + size += ProtoSize::calc_length(1, this->attribute.size()); + size += ProtoSize::calc_bool(1, this->once); + return size; } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -1034,9 +1094,11 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } -void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_uint32(1, static_cast(this->type)); +uint32_t ListEntitiesServicesArgument::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->type)); + return size; } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); @@ -1046,11 +1108,17 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(4, static_cast(this->supports_response)); } -void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_fixed32(1, this->key); - size.add_repeated_message(1, this->args); - size.add_uint32(1, static_cast(this->supports_response)); +uint32_t ListEntitiesServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_fixed32(1, this->key); + if (!this->args.empty()) { + for (const auto &it : this->args) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, static_cast(this->supports_response)); + return size; } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1165,13 +1233,15 @@ void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(4, this->response_data, this->response_data_len); #endif } -void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->call_id); - size.add_bool(1, this->success); - size.add_length(1, this->error_message.size()); +uint32_t ExecuteServiceResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->call_id); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_length(1, this->error_message.size()); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - size.add_length(1, this->response_data_len); + size += ProtoSize::calc_length(1, this->response_data_len); #endif + return size; } #endif #ifdef USE_CAMERA @@ -1188,18 +1258,20 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCameraResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1209,13 +1281,15 @@ void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void CameraImageResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->data_len_); - size.add_bool(1, this->done); +uint32_t CameraImageResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->data_len_); + size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1275,60 +1349,62 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(27, this->feature_flags); } -void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_current_temperature); - size.add_bool(1, this->supports_two_point_target_temperature); +uint32_t ListEntitiesClimateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_current_temperature); + size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } - size.add_float(1, this->visual_min_temperature); - size.add_float(1, this->visual_max_temperature); - size.add_float(1, this->visual_target_temperature_step); - size.add_bool(1, this->supports_action); + size += ProtoSize::calc_float(1, this->visual_min_temperature); + size += ProtoSize::calc_float(1, this->visual_max_temperature); + size += ProtoSize::calc_float(1, this->visual_target_temperature_step); + size += ProtoSize::calc_bool(1, this->supports_action); if (!this->supported_fan_modes->empty()) { for (const auto &it : *this->supported_fan_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } if (!this->supported_swing_modes->empty()) { for (const auto &it : *this->supported_swing_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } if (!this->supported_custom_fan_modes->empty()) { for (const char *it : *this->supported_custom_fan_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { for (const auto &it : *this->supported_presets) { - size.add_uint32_force(2, static_cast(it)); + size += ProtoSize::calc_uint32_force(2, static_cast(it)); } } if (!this->supported_custom_presets->empty()) { for (const char *it : *this->supported_custom_presets) { - size.add_length_force(2, strlen(it)); + size += ProtoSize::calc_length_force(2, strlen(it)); } } - size.add_bool(2, this->disabled_by_default); + size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(2, this->icon.size()); + size += ProtoSize::calc_length(2, this->icon.size()); #endif - size.add_uint32(2, static_cast(this->entity_category)); - size.add_float(2, this->visual_current_temperature_step); - size.add_bool(2, this->supports_current_humidity); - size.add_bool(2, this->supports_target_humidity); - size.add_float(2, this->visual_min_humidity); - size.add_float(2, this->visual_max_humidity); + size += ProtoSize::calc_uint32(2, static_cast(this->entity_category)); + size += ProtoSize::calc_float(2, this->visual_current_temperature_step); + size += ProtoSize::calc_bool(2, this->supports_current_humidity); + size += ProtoSize::calc_bool(2, this->supports_target_humidity); + size += ProtoSize::calc_float(2, this->visual_min_humidity); + size += ProtoSize::calc_float(2, this->visual_max_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif - size.add_uint32(2, this->feature_flags); + size += ProtoSize::calc_uint32(2, this->feature_flags); + return size; } void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1349,24 +1425,26 @@ void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ClimateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->mode)); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); - size.add_uint32(1, static_cast(this->action)); - size.add_uint32(1, static_cast(this->fan_mode)); - size.add_uint32(1, static_cast(this->swing_mode)); - size.add_length(1, this->custom_fan_mode.size()); - size.add_uint32(1, static_cast(this->preset)); - size.add_length(1, this->custom_preset.size()); - size.add_float(1, this->current_humidity); - size.add_float(1, this->target_humidity); +uint32_t ClimateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, static_cast(this->action)); + size += ProtoSize::calc_uint32(1, static_cast(this->fan_mode)); + size += ProtoSize::calc_uint32(1, static_cast(this->swing_mode)); + size += ProtoSize::calc_length(1, this->custom_fan_mode.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->preset)); + size += ProtoSize::calc_length(1, this->custom_preset.size()); + size += ProtoSize::calc_float(1, this->current_humidity); + size += ProtoSize::calc_float(1, this->target_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1481,27 +1559,29 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(12, this->supported_features); } -void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_float(1, this->min_temperature); - size.add_float(1, this->max_temperature); - size.add_float(1, this->target_temperature_step); + size += ProtoSize::calc_float(1, this->min_temperature); + size += ProtoSize::calc_float(1, this->max_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_step); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } - size.add_uint32(1, this->supported_features); + size += ProtoSize::calc_uint32(1, this->supported_features); + return size; } void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1515,17 +1595,19 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_float(7, this->target_temperature_low); buffer.encode_float(8, this->target_temperature_high); } -void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_uint32(1, static_cast(this->mode)); +uint32_t WaterHeaterStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->state); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, this->state); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + return size; } bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1588,24 +1670,26 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesNumberResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_float(1, this->min_value); - size.add_float(1, this->max_value); - size.add_float(1, this->step); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->unit_of_measurement.size()); - size.add_uint32(1, static_cast(this->mode)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_float(1, this->min_value); + size += ProtoSize::calc_float(1, this->max_value); + size += ProtoSize::calc_float(1, this->step); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1615,13 +1699,15 @@ void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void NumberStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t NumberStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1666,23 +1752,25 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSelectResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif if (!this->options->empty()) { for (const char *it : *this->options) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1692,13 +1780,15 @@ void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void SelectStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t SelectStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1753,25 +1843,27 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSirenResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { for (const char *it : *this->tones) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->supports_duration); - size.add_bool(1, this->supports_volume); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_duration); + size += ProtoSize::calc_bool(1, this->supports_volume); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1780,12 +1872,14 @@ void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void SirenStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SirenStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1860,22 +1954,24 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLockResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_open); - size.add_bool(1, this->requires_code); - size.add_length(1, this->code_format.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_open); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_length(1, this->code_format.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1884,12 +1980,14 @@ void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void LockStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->state)); +uint32_t LockStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1946,19 +2044,21 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesButtonResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesButtonResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1991,12 +2091,14 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, static_cast(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } -void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { - size.add_length(1, this->format.size()); - size.add_uint32(1, this->sample_rate); - size.add_uint32(1, this->num_channels); - size.add_uint32(1, static_cast(this->purpose)); - size.add_uint32(1, this->sample_bytes); +uint32_t MediaPlayerSupportedFormat::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->format.size()); + size += ProtoSize::calc_uint32(1, this->sample_rate); + size += ProtoSize::calc_uint32(1, this->num_channels); + size += ProtoSize::calc_uint32(1, static_cast(this->purpose)); + size += ProtoSize::calc_uint32(1, this->sample_bytes); + return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); @@ -2016,21 +2118,27 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(11, this->feature_flags); } -void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_bool(1, this->supports_pause); - size.add_repeated_message(1, this->supported_formats); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_pause); + if (!this->supported_formats.empty()) { + for (const auto &it : this->supported_formats) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->feature_flags); + size += ProtoSize::calc_uint32(1, this->feature_flags); + return size; } void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2041,14 +2149,16 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(5, this->device_id); #endif } -void MediaPlayerStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->state)); - size.add_float(1, this->volume); - size.add_bool(1, this->muted); +uint32_t MediaPlayerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += ProtoSize::calc_float(1, this->volume); + size += ProtoSize::calc_bool(1, this->muted); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2122,21 +2232,25 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->address_type); buffer.encode_bytes(4, this->data, this->data_len); } -void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_sint32(1, this->rssi); - size.add_uint32(1, this->address_type); - size.add_length(1, this->data_len); +uint32_t BluetoothLERawAdvertisement::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_sint32(1, this->rssi); + size += ProtoSize::calc_uint32(1, this->address_type); + size += ProtoSize::calc_length(1, this->data_len); + return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } } -void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { +uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { + uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { - size.add_message_object_force(1, this->advertisements[i]); + size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size()); } + return size; } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2163,11 +2277,13 @@ void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->mtu); buffer.encode_int32(4, this->error); } -void BluetoothDeviceConnectionResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->connected); - size.add_uint32(1, this->mtu); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->connected); + size += ProtoSize::calc_uint32(1, this->mtu); + size += ProtoSize::calc_int32(1, this->error); + return size; } bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2187,13 +2303,15 @@ void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->short_uuid); } -void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTDescriptor::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { @@ -2207,15 +2325,21 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(5, this->short_uuid); } -void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTCharacteristic::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->properties); - size.add_repeated_message(1, this->descriptors); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->properties); + if (!this->descriptors.empty()) { + for (const auto &it : this->descriptors) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { @@ -2228,14 +2352,20 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(4, this->short_uuid); } -void BluetoothGATTService::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTService::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_repeated_message(1, this->characteristics); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + if (!this->characteristics.empty()) { + for (const auto &it : this->characteristics) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); @@ -2243,14 +2373,24 @@ void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_message(2, it); } } -void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_repeated_message(1, this->services); +uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + if (!this->services.empty()) { + for (const auto &it : this->services) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + return size; } void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } -void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } +uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + return size; +} bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -2269,10 +2409,12 @@ void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTReadResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTReadResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2361,10 +2503,12 @@ void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); @@ -2375,80 +2519,96 @@ void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { } } } -void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->free); - size.add_uint32(1, this->limit); +uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->free); + size += ProtoSize::calc_uint32(1, this->limit); for (const auto &it : this->allocated) { if (it != 0) { - size.add_uint64_force(1, it); + size += ProtoSize::calc_uint64_force(1, it); } } + return size; } void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); } -void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_int32(1, this->error); +uint32_t BluetoothGATTErrorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTWriteResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTNotifyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); } -void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->paired); - size.add_int32(1, this->error); +uint32_t BluetoothDevicePairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->paired); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_uint32(3, static_cast(this->configured_mode)); } -void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast(this->state)); - size.add_uint32(1, static_cast(this->mode)); - size.add_uint32(1, static_cast(this->configured_mode)); +uint32_t BluetoothScannerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_uint32(1, static_cast(this->configured_mode)); + return size; } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2480,10 +2640,12 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); } -void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->noise_suppression_level); - size.add_uint32(1, this->auto_gain); - size.add_float(1, this->volume_multiplier); +uint32_t VoiceAssistantAudioSettings::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->noise_suppression_level); + size += ProtoSize::calc_uint32(1, this->auto_gain); + size += ProtoSize::calc_float(1, this->volume_multiplier); + return size; } void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); @@ -2492,12 +2654,14 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } -void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { - size.add_bool(1, this->start); - size.add_length(1, this->conversation_id.size()); - size.add_uint32(1, this->flags); - size.add_message_object(1, this->audio_settings); - size.add_length(1, this->wake_word_phrase.size()); +uint32_t VoiceAssistantRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->start); + size += ProtoSize::calc_length(1, this->conversation_id.size()); + size += ProtoSize::calc_uint32(1, this->flags); + size += ProtoSize::calc_message(1, this->audio_settings.calculate_size()); + size += ProtoSize::calc_length(1, this->wake_word_phrase.size()); + return size; } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2574,9 +2738,11 @@ void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } -void VoiceAssistantAudio::calculate_size(ProtoSize &size) const { - size.add_length(1, this->data_len); - size.add_bool(1, this->end); +uint32_t VoiceAssistantAudio::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_bool(1, this->end); + return size; } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2642,7 +2808,11 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength return true; } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } -void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +uint32_t VoiceAssistantAnnounceFinished::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); @@ -2650,14 +2820,16 @@ void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(3, it, true); } } -void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { - size.add_length(1, this->id.size()); - size.add_length(1, this->wake_word.size()); +uint32_t VoiceAssistantWakeWord::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->id.size()); + size += ProtoSize::calc_length(1, this->wake_word.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { - size.add_length_force(1, it.size()); + size += ProtoSize::calc_length_force(1, it.size()); } } + return size; } bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2719,14 +2891,20 @@ void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const } buffer.encode_uint32(3, this->max_active_wake_words); } -void VoiceAssistantConfigurationResponse::calculate_size(ProtoSize &size) const { - size.add_repeated_message(1, this->available_wake_words); - if (!this->active_wake_words->empty()) { - for (const auto &it : *this->active_wake_words) { - size.add_length_force(1, it.size()); +uint32_t VoiceAssistantConfigurationResponse::calculate_size() const { + uint32_t size = 0; + if (!this->available_wake_words.empty()) { + for (const auto &it : this->available_wake_words) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); } } - size.add_uint32(1, this->max_active_wake_words); + if (!this->active_wake_words->empty()) { + for (const auto &it : *this->active_wake_words) { + size += ProtoSize::calc_length_force(1, it.size()); + } + } + size += ProtoSize::calc_uint32(1, this->max_active_wake_words); + return size; } bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -2756,21 +2934,23 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_uint32(1, this->supported_features); - size.add_bool(1, this->requires_code); - size.add_bool(1, this->requires_code_to_arm); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->supported_features); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_bool(1, this->requires_code_to_arm); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2779,12 +2959,14 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void AlarmControlPanelStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->state)); +uint32_t AlarmControlPanelStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2841,22 +3023,24 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_uint32(1, this->min_length); - size.add_uint32(1, this->max_length); - size.add_length(1, this->pattern.size()); - size.add_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->min_length); + size += ProtoSize::calc_uint32(1, this->max_length); + size += ProtoSize::calc_length(1, this->pattern.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2866,13 +3050,15 @@ void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void TextStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2922,18 +3108,20 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2945,15 +3133,17 @@ void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void DateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->year); - size.add_uint32(1, this->month); - size.add_uint32(1, this->day); +uint32_t DateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->year); + size += ProtoSize::calc_uint32(1, this->month); + size += ProtoSize::calc_uint32(1, this->day); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3001,18 +3191,20 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3024,15 +3216,17 @@ void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void TimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->hour); - size.add_uint32(1, this->minute); - size.add_uint32(1, this->second); +uint32_t TimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->hour); + size += ProtoSize::calc_uint32(1, this->minute); + size += ProtoSize::calc_uint32(1, this->second); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3084,24 +3278,26 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesEventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); if (!this->event_types->empty()) { for (const char *it : *this->event_types) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3110,12 +3306,14 @@ void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void EventResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->event_type.size()); +uint32_t EventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_VALVE @@ -3136,22 +3334,24 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesValveResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3161,13 +3361,15 @@ void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void ValveStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_uint32(1, static_cast(this->current_operation)); +uint32_t ValveStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3215,18 +3417,20 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3236,13 +3440,15 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void DateTimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_fixed32(1, this->epoch_seconds); +uint32_t DateTimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3285,19 +3491,21 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesUpdateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3314,20 +3522,22 @@ void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void UpdateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_bool(1, this->in_progress); - size.add_bool(1, this->has_progress); - size.add_float(1, this->progress); - size.add_length(1, this->current_version.size()); - size.add_length(1, this->latest_version.size()); - size.add_length(1, this->title.size()); - size.add_length(1, this->release_summary.size()); - size.add_length(1, this->release_url.size()); +uint32_t UpdateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_bool(1, this->in_progress); + size += ProtoSize::calc_bool(1, this->has_progress); + size += ProtoSize::calc_float(1, this->progress); + size += ProtoSize::calc_length(1, this->current_version.size()); + size += ProtoSize::calc_length(1, this->latest_version.size()); + size += ProtoSize::calc_length(1, this->title.size()); + size += ProtoSize::calc_length(1, this->release_summary.size()); + size += ProtoSize::calc_length(1, this->release_url.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3369,7 +3579,11 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu return true; } void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } -void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } +uint32_t ZWaveProxyFrame::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + return size; +} bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -3396,9 +3610,11 @@ void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } -void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast(this->type)); - size.add_length(1, this->data_len); +uint32_t ZWaveProxyRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast(this->type)); + size += ProtoSize::calc_length(1, this->data_len); + return size; } #endif #ifdef USE_INFRARED @@ -3416,19 +3632,21 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(8, this->capabilities); } -void ListEntitiesInfraredResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesInfraredResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->capabilities); + return size; } #endif #ifdef USE_IR_RF @@ -3482,16 +3700,18 @@ void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { buffer.encode_sint32(3, it, true); } } -void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const { +uint32_t InfraredRFReceiveEvent::calculate_size() const { + uint32_t size = 0; #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_fixed32(1, this->key); + size += ProtoSize::calc_fixed32(1, this->key); if (!this->timings->empty()) { for (const auto &it : *this->timings) { - size.add_sint32_force(1, it); + size += ProtoSize::calc_sint32_force(1, it); } } + return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a97f6c0a76..89cb1158f3 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -388,8 +388,8 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -453,8 +453,8 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -468,8 +468,8 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -533,8 +533,8 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -564,8 +564,8 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -581,8 +581,8 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -603,8 +603,8 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -621,8 +621,8 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -663,8 +663,8 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -683,8 +683,8 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -730,8 +730,8 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -757,8 +757,8 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -821,8 +821,8 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -838,8 +838,8 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -857,8 +857,8 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -873,8 +873,8 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -907,8 +907,8 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -924,8 +924,8 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -963,8 +963,8 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -996,8 +996,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1010,8 +1010,8 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1039,8 +1039,8 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1083,8 +1083,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1174,8 +1174,8 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1193,8 +1193,8 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1263,8 +1263,8 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1280,8 +1280,8 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1302,8 +1302,8 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1353,8 +1353,8 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1381,8 +1381,8 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1439,8 +1439,8 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1460,8 +1460,8 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1504,8 +1504,8 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1521,8 +1521,8 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1555,8 +1555,8 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1572,8 +1572,8 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1609,8 +1609,8 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1625,8 +1625,8 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1670,8 +1670,8 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1686,8 +1686,8 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1723,8 +1723,8 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1755,8 +1755,8 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1773,8 +1773,8 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1791,8 +1791,8 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1847,8 +1847,8 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1864,8 +1864,8 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1901,8 +1901,8 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1929,8 +1929,8 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1944,8 +1944,8 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1958,8 +1958,8 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1975,8 +1975,8 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1991,8 +1991,8 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2030,8 +2030,8 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2125,8 +2125,8 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2143,8 +2143,8 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2161,8 +2161,8 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2178,8 +2178,8 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2195,8 +2195,8 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2213,8 +2213,8 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2231,8 +2231,8 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2249,8 +2249,8 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2267,8 +2267,8 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2313,8 +2313,8 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2333,8 +2333,8 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2395,8 +2395,8 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2453,8 +2453,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2466,8 +2466,8 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2516,8 +2516,8 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2551,8 +2551,8 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2567,8 +2567,8 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2606,8 +2606,8 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2623,8 +2623,8 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2657,8 +2657,8 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2676,8 +2676,8 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2711,8 +2711,8 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2730,8 +2730,8 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2767,8 +2767,8 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2783,8 +2783,8 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2804,8 +2804,8 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2821,8 +2821,8 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2856,8 +2856,8 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2873,8 +2873,8 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2907,8 +2907,8 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2931,8 +2931,8 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2966,8 +2966,8 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2985,8 +2985,8 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3005,8 +3005,8 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3052,8 +3052,8 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1441507406..e70b97196b 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -19,14 +19,6 @@ class APIServerConnectionBase : public ProtoService { public: #endif - bool send_message(const ProtoMessage &msg, uint8_t message_type) { -#ifdef HAS_PROTO_MESSAGE_DUMP - DumpBuffer dump_buf; - this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); -#endif - return this->send_message_impl(msg, message_type); - } - virtual void on_hello_request(const HelloRequest &value){}; virtual void on_disconnect_request(){}; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 0352d7347b..06816fe3e0 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -359,11 +359,11 @@ void APIServer::on_update(update::UpdateEntity *obj) { #endif #ifdef USE_ZWAVE_PROXY -void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { +void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { // We could add code to manage a second subscription type, but, since this message type is // very infrequent and small, we simply send it to all clients for (auto &c : this->clients_) - c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + c->send_message(msg); } #endif @@ -531,7 +531,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; - c->send_message(req, DisconnectRequest::MESSAGE_TYPE); + c->send_message(req); } }); } @@ -631,7 +631,7 @@ void APIServer::on_shutdown() { // Send disconnect requests to all connected clients for (auto &c : this->clients_) { DisconnectRequest req; - if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { + if (!c->send_message(req)) { // If we can't send the disconnect request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6eff2005f8..e6c10d1595 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -179,7 +179,7 @@ class APIServer : public Component, void on_update(update::UpdateEntity *obj) override; #endif #ifdef USE_ZWAVE_PROXY - void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); + void on_zwave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_IR_RF void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector *timings); diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fe43a47c3b..0a94c1699b 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie #ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); + return this->client_->send_message(resp); } #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 750fff0810..702208d9de 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -364,7 +364,11 @@ class ProtoWriteBuffer { /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); /// Encode a nested message field (force=true for repeated, false for singular) - void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); + /// Templated so concrete message type is preserved for direct encode/calculate_size calls. + template void encode_message(uint32_t field_id, const T &value, bool force = true); + // Non-template core for encode_message — all buffer work happens here + void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); std::vector *get_buffer() const { return buffer_; } protected: @@ -452,20 +456,20 @@ class DumpBuffer { class ProtoMessage { public: - // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer &buffer) const {} - // Default implementation for messages with no fields - virtual void calculate_size(ProtoSize &size) const {} - // Convenience: calculate and return size directly (defined after ProtoSize) - uint32_t calculated_size() const; + // Non-virtual defaults for messages with no fields. + // Concrete message classes hide these with their own implementations. + // All call sites use templates to preserve the concrete type, so virtual + // dispatch is not needed. This eliminates per-message vtable entries for + // encode/calculate_size, saving ~1.3 KB of flash across all message types. + void encode(ProtoWriteBuffer &buffer) const {} + uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } #endif protected: - // Non-virtual: messages are never deleted polymorphically. - // Protected prevents accidental `delete base_ptr` (compile error). + // Non-virtual destructor is protected to prevent polymorphic deletion. ~ProtoMessage() = default; }; @@ -494,32 +498,7 @@ class ProtoDecodableMessage : public ProtoMessage { }; class ProtoSize { - private: - uint32_t total_size_ = 0; - public: - /** - * @brief ProtoSize class for Protocol Buffer serialization size calculation - * - * This class provides methods to calculate the exact byte counts needed - * for encoding various Protocol Buffer field types. The class now uses an - * object-based approach to reduce parameter passing overhead while keeping - * varint calculation methods static for external use. - * - * Implements Protocol Buffer encoding size calculation according to: - * https://protobuf.dev/programming-guides/encoding/ - * - * Key features: - * - Object-based approach reduces flash usage by eliminating parameter passing - * - Early-return optimization for zero/default values - * - Static varint methods for external callers - * - Specialized handling for different field types according to protobuf spec - */ - - ProtoSize() = default; - - uint32_t get_size() const { return total_size_; } - /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @@ -616,320 +595,77 @@ class ProtoSize { return varint(tag); } - /** - * @brief Common parameters for all add_*_field methods - * - * All add_*_field methods follow these common patterns: - * * @param field_id_size Pre-calculated size of the field ID in bytes - * @param value The value to calculate size for (type varies) - * @param force Whether to calculate size even if the value is default/zero/empty - * - * Each method follows this implementation pattern: - * 1. Skip calculation if value is default (0, false, empty) and not forced - * 2. Calculate the size based on the field's encoding rules - * 3. Add the field_id_size + calculated value size to total_size - */ - - /** - * @brief Calculates and adds the size of an int32 field to the total message size - */ - inline void add_int32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_int32_force(field_id_size, value); - } + // Static methods that RETURN size contribution (no ProtoSize object needed). + // Used by generated calculate_size() methods to accumulate into a plain uint32_t register. + static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + (value < 0 ? 10 : varint(static_cast(value))) : 0; } - - /** - * @brief Calculates and adds the size of an int32 field to the total message size (force version) - */ - inline void add_int32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when forced - // Negative values are encoded as 10-byte varints in protobuf - total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast(value))); + static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + (value < 0 ? 10 : varint(static_cast(value))); } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size - */ - inline void add_uint32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - add_uint32_force(field_id_size, value); - } + static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size (force version) - */ - inline void add_uint32_force(uint32_t field_id_size, uint32_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size - */ - inline void add_bool(uint32_t field_id_size, bool value) { - if (value) { - // Boolean fields always use 1 byte when true - total_size_ += field_id_size + 1; - } + static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } + static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } + static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { + return value != 0.0f ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size (force version) - */ - inline void add_bool_force(uint32_t field_id_size, bool value) { - // Always calculate size when force is true - // Boolean fields always use 1 byte - total_size_ += field_id_size + 1; + static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a float field to the total message size - */ - inline void add_float(uint32_t field_id_size, float value) { - if (value != 0.0f) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + 4 : 0; } - - // NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a fixed32 field to the total message size - */ - inline void add_fixed32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + varint(encode_zigzag32(value)) : 0; } - - // NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a sfixed32 field to the total message size - */ - inline void add_sfixed32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + varint(encode_zigzag32(value)); } - - // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_sint32_force(field_id_size, value); - } + static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size (force version) - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when force is true - // ZigZag encoding for sint32 - total_size_ += field_id_size + varint(encode_zigzag32(value)); + static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size - */ - inline void add_int64(uint32_t field_id_size, int64_t value) { - if (value != 0) { - add_int64_force(field_id_size, value); - } + static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size (force version) - */ - inline void add_int64_force(uint32_t field_id_size, int64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size - */ - inline void add_uint64(uint32_t field_id_size, uint64_t value) { - if (value != 0) { - add_uint64_force(field_id_size, value); - } + static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { + return len ? field_id_size + varint(static_cast(len)) + static_cast(len) : 0; } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size (force version) - */ - inline void add_uint64_force(uint32_t field_id_size, uint64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) { + return field_id_size + varint(static_cast(len)) + static_cast(len); } - - // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed - // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size - */ - inline void add_length(uint32_t field_id_size, size_t len) { - if (len != 0) { - add_length_force(field_id_size, len); - } + static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(encode_zigzag64(value)) : 0; } - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated - * field version) - */ - inline void add_length_force(uint32_t field_id_size, size_t len) { - // Always calculate size when force is true - // Field ID + length varint + data bytes - total_size_ += field_id_size + varint(static_cast(len)) + static_cast(len); + static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(encode_zigzag64(value)); } - - /** - * @brief Adds a pre-calculated size directly to the total - * - * This is used when we can calculate the total size by multiplying the number - * of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.) - * - * @param size The pre-calculated total size to add - */ - inline void add_precalculated_size(uint32_t size) { total_size_ += size; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This helper function directly updates the total_size reference if the nested size - * is greater than zero. - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) { - if (nested_size != 0) { - add_message_field_force(field_id_size, nested_size); - } + static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) { - // Always calculate size when force is true - // Field ID + length varint + nested message content - total_size_ += field_id_size + varint(nested_size) + nested_size; + static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This version takes a ProtoMessage object, calculates its size internally, - * and updates the total_size reference. This eliminates the need for a temporary variable - * at the call site. - * - * @param message The nested message object - */ - inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field(field_id_size, nested_size); + static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) { + return nested_size ? field_id_size + varint(nested_size) + nested_size : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param message The nested message object - */ - inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field_force(field_id_size, nested_size); - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size - * - * This helper processes a vector of message objects, calculating the size for each message - * and adding it to the total size. - * - * @tparam MessageType The type of the nested messages in the vector - * @param messages Vector of message objects - */ - template - inline void add_repeated_message(uint32_t field_id_size, const std::vector &messages) { - // Skip if the vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector - * version) - * - * @tparam MessageType The type of the nested messages in the FixedVector - * @param messages FixedVector of message objects - */ - template - inline void add_repeated_message(uint32_t field_id_size, const FixedVector &messages) { - // Skip if the fixed vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculate size of a packed repeated sint32 field - */ - inline void add_packed_sint32(uint32_t field_id_size, const std::vector &values) { - if (values.empty()) - return; - - size_t packed_size = 0; - for (int value : values) { - packed_size += varint(encode_zigzag32(value)); - } - - // field_id + length varint + packed data - total_size_ += field_id_size + varint(static_cast(packed_size)) + static_cast(packed_size); + static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) { + return field_id_size + varint(nested_size) + nested_size; } }; // Implementation of methods that depend on ProtoSize being fully defined -inline uint32_t ProtoMessage::calculated_size() const { - ProtoSize size; - this->calculate_size(size); - return size.get_size(); -} - // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { if (values.empty()) @@ -949,31 +685,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } } -// Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { - // Calculate the message size first - ProtoSize msg_size; - value.calculate_size(msg_size); - uint32_t msg_length_bytes = msg_size.get_size(); +// Encode thunk — converts void* back to concrete type for direct encode() call +template void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) { + static_cast(msg)->encode(buf); +} - // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) - // Repeated messages (force=true) are always encoded since an empty item is meaningful +// Implementation of encode_message - must be after ProtoMessage is defined +template inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { + this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg, force); +} + +// Non-template core for encode_message +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { if (msg_length_bytes == 0 && !force) return; - - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - - // Write the length varint directly through pos_ + this->encode_field_raw(field_id, 2); this->encode_varint_raw(msg_length_bytes); - - // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API uint8_t *start = this->pos_; - value.encode(*this); + encode_fn(value, *this); if (static_cast(this->pos_ - start) != msg_length_bytes) this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); #else - value.encode(*this); + encode_fn(value, *this); #endif } @@ -993,14 +728,6 @@ class ProtoService { virtual void on_no_setup_connection() = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - /** - * Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending. - * This is the implementation method - callers should use send_message() which adds logging. - * @param msg The protobuf message to send. - * @param message_type The message type identifier. - * @return True if the message was sent successfully, false otherwise. - */ - virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0; // Authentication helper methods inline bool check_connection_setup_() { diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index b2000fbd94..21573f0184 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -183,10 +183,7 @@ void BluetoothConnection::send_service_for_discovery_() { static constexpr size_t MAX_PACKET_SIZE = 1360; // Keep running total of actual message size - size_t current_size = 0; - api::ProtoSize size; - resp.calculate_size(size); - current_size = size.get_size(); + size_t current_size = resp.calculate_size(); while (this->send_service_ < this->service_count_) { esp_gattc_service_elem_t service_result; @@ -302,9 +299,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // end if (total_char_count > 0) // Calculate the actual size of just this service - api::ProtoSize service_sizer; - service_resp.calculate_size(service_sizer); - size_t service_size = service_sizer.get_size() + 1; // +1 for field tag + size_t service_size = service_resp.calculate_size() + 1; // +1 for field tag // Check if adding this service would exceed the limit if (current_size + service_size > MAX_PACKET_SIZE) { @@ -333,7 +328,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // Send the message with dynamically batched services - api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); + api_conn->send_message(resp); } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { @@ -422,7 +417,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - api_connection->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -438,7 +433,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - api_connection->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -454,7 +449,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -470,7 +465,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_NOTIFY_EVT: { @@ -483,7 +478,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - api_connection->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index cab328e2f5..21da4ead14 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -44,7 +44,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.configured_mode = this->configured_scan_active_ ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; - this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); + this->api_connection_->send_message(resp); } void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { @@ -112,7 +112,7 @@ void BluetoothProxy::flush_pending_advertisements() { return; // Send the message - this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); + this->api_connection_->send_message(this->response_); ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); @@ -269,7 +269,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest call.success = ret == ESP_OK; call.error = ret; - this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); break; } @@ -389,7 +389,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui call.connected = connected; call.mtu = mtu; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_connections_free() { if (this->api_connection_ != nullptr) { @@ -398,7 +398,7 @@ void BluetoothProxy::send_connections_free() { } void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { - api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); + api_connection->send_message(this->connections_free_response_); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { @@ -406,7 +406,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { return; api::BluetoothGATTGetServicesDoneResponse call; call.address = address; - this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { @@ -416,7 +416,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ call.address = address; call.handle = handle; call.error = error; - this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { @@ -427,7 +427,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ call.paired = paired; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { @@ -438,7 +438,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e call.success = success; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d6cbfd4b21..51d52a8af8 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -251,8 +251,7 @@ void VoiceAssistant::loop() { } #endif - if (this->api_client_ == nullptr || - !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { + if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { ESP_LOGW(TAG, "Could not request start"); this->error_trigger_.trigger("not-connected", "Could not request start"); this->continuous_ = false; @@ -275,7 +274,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAudio msg; msg.data = this->send_buffer_; msg.data_len = read_bytes; - this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); + this->api_client_->send_message(msg); } else { if (!this->udp_socket_running_) { if (!this->start_udp_socket_()) { @@ -354,7 +353,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); + this->api_client_->send_message(msg); break; } } @@ -612,7 +611,7 @@ void VoiceAssistant::signal_stop_() { ESP_LOGD(TAG, "Signaling stop"); api::VoiceAssistantRequest msg; msg.start = false; - this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE); + this->api_client_->send_message(msg); } void VoiceAssistant::start_playback_timeout_() { @@ -622,7 +621,7 @@ void VoiceAssistant::start_playback_timeout_() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); + this->api_client_->send_message(msg); }); } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index b0836ac072..9e5c57814d 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -119,7 +119,7 @@ void ZWaveProxy::process_uart_() { // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; } - this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + this->api_connection_->send_message(this->outgoing_proto_msg_); } } } @@ -209,7 +209,7 @@ void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) { msg.data_len = this->home_id_.size(); if (conn != nullptr) { // Send to specific connection - conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + conn->send_message(msg); } else if (api::global_api_server != nullptr) { // We could add code to manage a second subscription type, but, since this message is // very infrequent and small, we simply send it to all clients @@ -346,7 +346,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->buffer_[0] = byte; this->outgoing_proto_msg_.data = this->buffer_.data(); this->outgoing_proto_msg_.data_len = 1; - this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + this->api_connection_->send_message(this->outgoing_proto_msg_); } } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 9c9cda4d36..85352689e6 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -270,18 +270,21 @@ class TypeInfo(ABC): def _get_simple_size_calculation( self, name: str, force: bool, base_method: str, value_expr: str = None ) -> str: - """Helper for simple size calculations. + """Helper for simple size calculations using static ProtoSize methods. Args: name: Field name force: Whether this is for a repeated field - base_method: Base method name (e.g., "add_int32") + base_method: Base method name (e.g., "int32") value_expr: Optional value expression (defaults to name) """ field_id_size = self.calculate_field_id_size() - method = f"{base_method}_force" if force else base_method + method = f"calc_{base_method}_force" if force else f"calc_{base_method}" + # calc_bool_force only takes field_id_size (no value needed - bool is always 1 byte) + if base_method == "bool" and force: + return f"size += ProtoSize::{method}({field_id_size});" value = value_expr or name - return f"size.{method}({field_id_size}, {value});" + return f"size += ProtoSize::{method}({field_id_size}, {value});" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -410,7 +413,7 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_double({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -434,7 +437,7 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_float({field_id_size}, {name});" + return f"size += ProtoSize::calc_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -457,7 +460,7 @@ class Int64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int64") + return self._get_simple_size_calculation(name, force, "int64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -477,7 +480,7 @@ class UInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint64") + return self._get_simple_size_calculation(name, force, "uint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -497,7 +500,7 @@ class Int32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int32") + return self._get_simple_size_calculation(name, force, "int32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -518,7 +521,7 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_fixed64({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -542,7 +545,7 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_fixed32({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -563,7 +566,7 @@ class BoolType(TypeInfo): return f"out.append(YESNO({name}));" def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_bool") + return self._get_simple_size_calculation(name, force, "bool") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 1 # field ID + 1 byte @@ -647,18 +650,18 @@ class StringType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: # For SOURCE_CLIENT only messages, use the string field directly if not self._needs_encode: - return self._get_simple_size_calculation(name, force, "add_length") + return self._get_simple_size_calculation(name, force, "length") # Check if this is being called from a repeated field context # In that case, 'name' will be 'it' and we need to use the repeated version if name == "it": - # For repeated fields, we need to use add_length_force which includes field ID + # For repeated fields, we need to use length_force which includes field ID field_id_size = self.calculate_field_id_size() - return f"size.add_length_force({field_id_size}, it.size());" + return f"size += ProtoSize::calc_length_force({field_id_size}, it.size());" # For messages that need encoding, use the StringRef size field_id_size = self.calculate_field_id_size() - return f"size.add_length({field_id_size}, this->{self.field_name}_ref_.size());" + return f"size += ProtoSize::calc_length({field_id_size}, this->{self.field_name}_ref_.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -721,7 +724,9 @@ class MessageType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_message_object") + field_id_size = self.calculate_field_id_size() + method = "calc_message_force" if force else "calc_message" + return f"size += ProtoSize::{method}({field_id_size}, {name}.calculate_size());" def get_estimated_size(self) -> int: # For message types, we can't easily estimate the submessage size without @@ -822,7 +827,7 @@ class BytesType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes @@ -897,7 +902,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" class PointerToStringBufferType(PointerToBufferTypeBase): @@ -939,7 +944,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return f'dump_field(out, "{self.name}", this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -1103,9 +1108,9 @@ class FixedArrayBytesType(TypeInfo): if force: # For repeated fields, always calculate size (no zero check) - return f"size.add_length_force({field_id_size}, {length_field});" - # For non-repeated fields, add_length already checks for zero - return f"size.add_length({field_id_size}, {length_field});" + return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" + # For non-repeated fields, length already checks for zero + return f"size += ProtoSize::calc_length({field_id_size}, {length_field});" def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size @@ -1132,7 +1137,7 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint32") + return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1168,7 +1173,7 @@ class EnumType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation( - name, force, "add_uint32", f"static_cast({name})" + name, force, "uint32", f"static_cast({name})" ) def get_estimated_size(self) -> int: @@ -1190,7 +1195,7 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_sfixed32({field_id_size}, {name});" + return f"size += ProtoSize::calc_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -1214,7 +1219,7 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_sfixed64({field_id_size}, {name});" + return f"size += ProtoSize::calc_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -1237,7 +1242,7 @@ class SInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint32") + return self._get_simple_size_calculation(name, force, "sint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1257,7 +1262,7 @@ class SInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint64") + return self._get_simple_size_calculation(name, force, "sint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1694,11 +1699,17 @@ class RepeatedTypeInfo(TypeInfo): # For repeated fields, we always need to pass force=True to the underlying type's calculation # This is because the encode method always sets force=true for repeated fields - # Handle message types separately as they use a dedicated helper + # Handle message types separately - generate inline loop if isinstance(self._ti, MessageType): field_id_size = self._ti.calculate_field_id_size() - container = f"*{name}" if self._use_pointer else name - return f"size.add_repeated_message({field_id_size}, {container});" + container_ref = f"*{name}" if self._use_pointer else name + empty_check = f"{name}->empty()" if self._use_pointer else f"{name}.empty()" + o = f"if (!{empty_check}) {{\n" + o += f" for (const auto &it : {container_ref}) {{\n" + o += f" size += ProtoSize::calc_message_force({field_id_size}, it.calculate_size());\n" + o += " }\n" + o += "}" + return o # For non-message types, generate size calculation with iteration container_ref = f"*{name}" if self._use_pointer else name @@ -1713,14 +1724,14 @@ class RepeatedTypeInfo(TypeInfo): field_id_size = self._ti.calculate_field_id_size() bytes_per_element = field_id_size + num_bytes size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" - o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" + o += f" size += {size_expr} * {bytes_per_element};\n" else: # Other types need the actual value # Special handling for const char* elements if self._use_pointer and "const char" in self._container_no_template: field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" - o += f" size.add_length_force({field_id_size}, strlen(it));\n" + o += f" size += ProtoSize::calc_length_force({field_id_size}, strlen(it));\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" @@ -2233,23 +2244,19 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer &buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const;" public_content.append(prot) # 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: - o = f"void {desc.name}::calculate_size(ProtoSize &size) const {{" - # For a single field, just inline it for simplicity - if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120: - o += f" {size_calc[0]} }}\n" - else: - # For multiple fields - o += "\n" - o += indent("\n".join(size_calc)) + "\n" - o += "}\n" + o = f"uint32_t {desc.name}::calculate_size() const {{\n" + o += " uint32_t size = 0;\n" + o += indent("\n".join(size_calc)) + "\n" + o += " return size;\n" + o += "}\n" cpp += o - prot = "void calculate_size(ProtoSize &size) const override;" + prot = "uint32_t calculate_size() const;" public_content.append(prot) # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used @@ -2933,14 +2940,8 @@ static const char *const TAG = "api.service"; hpp += " public:\n" hpp += "#endif\n\n" - # Add non-template send_message method - hpp += " bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n" - hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - hpp += " DumpBuffer dump_buf;\n" - hpp += " this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));\n" - hpp += "#endif\n" - hpp += " return this->send_message_impl(msg, message_type);\n" - hpp += " }\n\n" + # send_message is now a template on APIConnection directly + # No non-template send_message method needed here # Add logging helper method implementations to cpp cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" From 3c7956e72d34f3cc3fb9f5100afc09e974982f26 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:04:00 -0500 Subject: [PATCH 007/101] [multiple] Add default initializers to uninitialized member variables (#14556) Co-authored-by: Claude Opus 4.6 --- esphome/components/bedjet/bedjet_hub.h | 8 ++++---- .../components/current_based/current_based_cover.h | 12 ++++++------ esphome/components/deep_sleep/deep_sleep_component.h | 2 +- esphome/components/max6956/max6956.h | 4 ++-- esphome/components/ms8607/ms8607.h | 8 ++++---- .../remote_transmitter/remote_transmitter.h | 2 +- esphome/components/sen5x/sen5x.h | 8 ++++---- esphome/components/sim800l/sim800l.h | 10 +++++----- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 6258795b02..59b0af93ad 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo std::unique_ptr codec_; bool discover_characteristics_(); - uint16_t char_handle_cmd_; - uint16_t char_handle_name_; - uint16_t char_handle_status_; - uint16_t config_descr_status_; + uint16_t char_handle_cmd_{0}; + uint16_t char_handle_name_{0}; + uint16_t char_handle_status_{0}; + uint16_t config_descr_status_{0}; uint8_t write_notify_config_descriptor_(bool enable); }; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index 76bd85cdf7..40b39517e4 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -67,21 +67,21 @@ class CurrentBasedCover : public cover::Cover, public Component { sensor::Sensor *open_sensor_{nullptr}; Trigger<> open_trigger_; - float open_moving_current_threshold_; + float open_moving_current_threshold_{0.0f}; float open_obstacle_current_threshold_{FLT_MAX}; - uint32_t open_duration_; + uint32_t open_duration_{0}; sensor::Sensor *close_sensor_{nullptr}; Trigger<> close_trigger_; - float close_moving_current_threshold_; + float close_moving_current_threshold_{0.0f}; float close_obstacle_current_threshold_{FLT_MAX}; - uint32_t close_duration_; + uint32_t close_duration_{0}; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; Trigger<> malfunction_trigger_; - uint32_t start_sensing_delay_; - float obstacle_rollback_; + uint32_t start_sensing_delay_{0}; + float obstacle_rollback_{0.0f}; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 1998b815f3..14713d51a1 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -145,7 +145,7 @@ class DeepSleepComponent : public Component { #endif // USE_BK72XX #ifdef USE_ESP32 - InternalGPIOPin *wakeup_pin_; + InternalGPIOPin *wakeup_pin_{nullptr}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 0c609b0b43..31f97c11f8 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -63,8 +63,8 @@ class MAX6956 : public Component, public i2c::I2CDevice { bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register bool write_reg_(uint8_t reg, uint8_t value); - max6956::MAX6956CURRENTMODE brightness_mode_; - uint8_t global_brightness_; + max6956::MAX6956CURRENTMODE brightness_mode_{}; + uint8_t global_brightness_{0}; private: int8_t prev_bright_[28] = {0}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index ceb3dd22c8..2888b6cdd2 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -67,9 +67,9 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { /// use raw temperature & pressure to calculate & publish values void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; /** I2CDevice object to communicate with secondary I2C address for the humidity sensor * @@ -77,7 +77,7 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { * * Default address for humidity is 0x40 */ - MS8607HumidityDevice *humidity_device_; + MS8607HumidityDevice *humidity_device_{nullptr}; /// This device's pressure & temperature calibration values, read from PROM struct CalibrationValues { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index aee52ea170..6b4ebfe24b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -96,7 +96,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, bool inverted_{false}; bool non_blocking_{false}; #endif - uint8_t carrier_duty_percent_; + uint8_t carrier_duty_percent_{50}; Trigger<> transmit_trigger_; Trigger<> complete_trigger_; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index e3bf931b41..a9d4da86b8 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -104,12 +104,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri char serial_number_[17] = "UNKNOWN"; uint16_t voc_baseline_state_[4]{0}; - uint32_t voc_baseline_time_; - uint16_t firmware_version_; + uint32_t voc_baseline_time_{0}; + uint16_t firmware_version_{0}; Sen5xType type_{Sen5xType::UNKNOWN}; - ERRORCODE error_code_; + ERRORCODE error_code_{ERRORCODE::UNKNOWN}; bool initialized_{false}; - bool store_baseline_; + bool store_baseline_{false}; sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index a2da686ce1..e9e2f66d78 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -107,11 +107,11 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { std::string recipient_; std::string outgoing_message_; std::string ussd_; - bool send_pending_; - bool dial_pending_; - bool connect_pending_; - bool disconnect_pending_; - bool send_ussd_pending_; + bool send_pending_{false}; + bool dial_pending_{false}; + bool connect_pending_{false}; + bool disconnect_pending_{false}; + bool send_ussd_pending_{false}; uint8_t call_state_{6}; CallbackManager sms_received_callback_; From 3db436e48e0cafd48711bd782600cbea7a5f9adc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:05:34 -0500 Subject: [PATCH 008/101] [esp32_ble_server][espnow][time] Fix logic bugs (#14553) Co-authored-by: Claude Opus 4.6 --- .../components/esp32_ble_server/ble_server.cpp | 18 +++++++----------- esphome/components/espnow/automation.h | 4 ++-- esphome/components/time/real_time_clock.cpp | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index f292cf8722..ecc53e197f 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -7,6 +7,7 @@ #ifdef USE_ESP32 +#include #include #include #include @@ -38,21 +39,16 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - uint16_t index_to_remove = 0; - // Iterate over the services to start - for (unsigned i = 0; i < this->services_to_start_.size(); i++) { - BLEService *service = this->services_to_start_[i]; + for (auto &service : this->services_to_start_) { if (service->is_created()) { service->start(); // Needs to be called once per characteristic in the service - } else { - index_to_remove = i + 1; } } - // Remove the services that have been started - if (index_to_remove > 0) { - this->services_to_start_.erase(this->services_to_start_.begin(), - this->services_to_start_.begin() + index_to_remove - 1); - } + // Remove services that have been started + this->services_to_start_.erase( + std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(), + [](BLEService *service) { return service->is_starting() || service->is_running(); }), + this->services_to_start_.end()); } break; } diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 0b26681400..0fbb14e388 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -138,7 +138,7 @@ class OnReceiveTrigger : public Trigger, public ESPNowUnknownPeerHandler { @@ -167,7 +167,7 @@ class OnBroadcastedTrigger : public Trigger Date: Fri, 6 Mar 2026 14:14:12 -0500 Subject: [PATCH 009/101] [multiple] Fix cast/operator precedence bugs (#14560) Co-authored-by: Claude Opus 4.6 --- esphome/components/datetime/date_entity.h | 2 +- esphome/components/es7210/es7210.cpp | 2 +- esphome/components/sgp4x/sgp4x.cpp | 2 +- esphome/components/tsl2591/tsl2591.cpp | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index cbf2b85506..8233e809a1 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -91,7 +91,7 @@ class DateCall { DateEntity *parent_; - optional year_; + optional year_; optional month_; optional day_; }; diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 1358121c1b..4371075fa9 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -172,7 +172,7 @@ uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB mic_gain += 0.5; if (mic_gain <= 33.0) { - return (uint8_t) mic_gain / 3; + return (uint8_t) (mic_gain / 3); } if (mic_gain < 36.0) { return 12; diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 44d0a54080..cb41e374f8 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -199,7 +199,7 @@ void SGP4xComponent::measure_raw_() { response_words = 2; } } - uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100)); + uint16_t rhticks = (uint16_t) llround((humidity * 65535) / 100); uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175); // first parameter are the relative humidity ticks data[0] = rhticks; diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 42c524a074..4ce673a91a 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -327,7 +327,9 @@ uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_ return (combined_illuminance >> 16); } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { // Reads all and subtracts out the infrared - return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); + uint16_t full = combined_illuminance & 0xFFFF; + uint16_t ir = combined_illuminance >> 16; + return (ir > full) ? 0 : (full - ir); } // unknown channel! ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel); From 219d5170e006e7297004b1cfeb7e5323307980b0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:15:54 -0500 Subject: [PATCH 010/101] [noblex] Fix IR receive losing decoded bytes between calls (#14533) Co-authored-by: Claude Opus 4.6 --- esphome/components/noblex/noblex.cpp | 130 +++++++++++++-------------- esphome/components/noblex/noblex.h | 3 +- 2 files changed, 64 insertions(+), 69 deletions(-) diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index f1e76eabf2..e7e421d177 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -118,15 +118,15 @@ void NoblexClimate::transmit_state() { data->mark(NOBLEX_HEADER_MARK); data->space(NOBLEX_HEADER_SPACE); // Data (sent remote_state from the MSB to the LSB) - for (uint8_t i : remote_state) { - for (int8_t j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { + for (int byte_idx = 0; byte_idx < 8; byte_idx++) { + for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) { + if ((byte_idx == 4) && (bit_idx == 4)) { // Header intermediate data->mark(NOBLEX_BIT_MARK); data->space(NOBLEX_GAP); // gap en bit 36 } else { data->mark(NOBLEX_BIT_MARK); - bool bit = i & (1 << j); + bool bit = remote_state[byte_idx] & (1 << bit_idx); data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE); } } @@ -145,76 +145,71 @@ void NoblexClimate::transmit_state() { // Handle received IR Buffer bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { - uint8_t remote_state[8] = {0}; - uint8_t crc = 0, crc_calculated = 0; - - if (!receiving_) { - // Validate header - if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { - ESP_LOGV(TAG, "Header"); - receiving_ = true; - // Read first 36 bits - for (int i = 0; i < 5; i++) { - // Read bit - for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { - remote_state[i] |= 1 << j; - // Header intermediate - ESP_LOGVV(TAG, "GAP"); - return false; - } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - remote_state[i] |= 1 << j; - } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { - ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); - return false; - } - } - ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); - } - - } else { - ESP_LOGV(TAG, "Header fail"); - receiving_ = false; - return false; - } - - } else { - // Read the remaining 28 bits - for (int i = 4; i < 8; i++) { - // Read bit + if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { + // First part: header + first 36 bits, followed by 20ms gap + data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE); + ESP_LOGV(TAG, "Header"); + this->receiving_ = false; + memset(this->remote_state_, 0, sizeof(this->remote_state_)); + for (int i = 0; i < 5; i++) { for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j >= 4)) { - // nothing + if ((i == 4) && (j == 4)) { + this->remote_state_[i] |= 1 << j; + ESP_LOGVV(TAG, "GAP"); + this->receiving_ = true; + return false; } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - remote_state[i] |= 1 << j; + this->remote_state_[i] |= 1 << j; } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } + return false; + } - // Read crc - for (int i = 3; i >= 0; i--) { - if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - crc |= 1 << i; + // Second part: remaining 28 bits + 4-bit CRC + footer + if (!this->receiving_) { + return false; + } + this->receiving_ = false; + for (int i = 4; i < 8; i++) { + for (int j = 7; j >= 0; j--) { + if ((i == 4) && (j >= 4)) { + // already decoded in first part + } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + this->remote_state_[i] |= 1 << j; } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { - ESP_LOGVV(TAG, "Bit %d CRC fail", i); + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "CRC %02X", crc); - - // Validate footer - if (!data.expect_mark(NOBLEX_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - receiving_ = false; + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } - for (uint8_t i : remote_state) + // Read CRC + uint8_t crc = 0; + for (int i = 3; i >= 0; i--) { + if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + crc |= 1 << i; + } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Bit %d CRC fail", i); + return false; + } + } + ESP_LOGV(TAG, "CRC %02X", crc); + + // Validate footer + if (!data.expect_mark(NOBLEX_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + // Validate CRC + uint8_t crc_calculated = 0; + for (uint8_t i : this->remote_state_) crc_calculated += reverse_bits(i); crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4; ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated); @@ -224,11 +219,12 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { return false; } - ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], - remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); + ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0], + this->remote_state_[1], this->remote_state_[2], this->remote_state_[3], this->remote_state_[4], + this->remote_state_[5], this->remote_state_[6], this->remote_state_[7]); auto powered_on = false; - if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) { + if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) { powered_on = true; this->powered_on_assumed = powered_on; } else { @@ -241,7 +237,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { // Set received mode if (powered_on_assumed) { - auto mode = (remote_state[0] & 0xE0) >> 5; + auto mode = (this->remote_state_[0] & 0xE0) >> 5; ESP_LOGV(TAG, "Mode: %02X", mode); switch (mode) { case IRNoblexMode::IR_NOBLEX_MODE_AUTO: @@ -263,7 +259,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received temp - uint8_t temp = remote_state[1]; + uint8_t temp = this->remote_state_[1]; ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); temp = 0x0F & reverse_bits(temp); @@ -272,7 +268,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->target_temperature = temp; // Set received fan speed - auto fan = (remote_state[0] & 0x0C) >> 2; + auto fan = (this->remote_state_[0] & 0x0C) >> 2; ESP_LOGV(TAG, "Fan: %02X", fan); switch (fan) { case IRNoblexFan::IR_NOBLEX_FAN_HIGH: @@ -291,7 +287,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received swing status - if (remote_state[0] & 0x02) { + if (this->remote_state_[0] & 0x02) { ESP_LOGV(TAG, "Swing vertical"); this->swing_mode = climate::CLIMATE_SWING_VERTICAL; } else { @@ -299,8 +295,6 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->swing_mode = climate::CLIMATE_SWING_OFF; } - for (uint8_t &i : remote_state) - i = 0; this->publish_state(); return true; } // end on_receive() diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index 57990db005..3d52a1a538 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -41,7 +41,8 @@ class NoblexClimate : public climate_ir::ClimateIR { /// Handle received IR Buffer. bool on_receive(remote_base::RemoteReceiveData data) override; bool send_swing_cmd_{false}; - bool receiving_ = false; + bool receiving_{false}; + uint8_t remote_state_[8]{}; }; } // namespace noblex From 9ab5f5d451a4d1aaf030fbff72bff0011478e5fb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:42:05 -0500 Subject: [PATCH 011/101] [light] Fix unsigned underflow in addressable scan effect (#14546) Co-authored-by: Claude Opus 4.6 --- .../light/addressable_light_effect.h | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index a85ea4661d..461ddbc085 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -171,12 +171,27 @@ class AddressableScanEffect : public AddressableLightEffect { if (now - this->last_move_ < this->move_interval_) return; - if (direction_) { + const auto num_leds = static_cast(it.size()); + if (this->scan_width_ >= num_leds) { + it.all() = current_color; + it.schedule_show(); + this->last_move_ = now; + return; + } + + const uint32_t max_pos = num_leds - this->scan_width_; + if (this->at_led_ >= max_pos) { + this->at_led_ = max_pos; + this->direction_ = false; + } + + if (this->direction_) { this->at_led_++; - if (this->at_led_ == it.size() - this->scan_width_) + if (this->at_led_ >= max_pos) this->direction_ = false; } else { - this->at_led_--; + if (this->at_led_ > 0) + this->at_led_--; if (this->at_led_ == 0) this->direction_ = true; } From a9cceebb33612054866465254456336fd4d76ba5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:48:50 -0500 Subject: [PATCH 012/101] [pid][nextion][pn532_i2c][pipsolar] Fix copy-paste and logic bugs (#14551) Co-authored-by: Claude Opus 4.6 --- esphome/components/nextion/nextion_component.cpp | 2 +- esphome/components/pid/pid_autotuner.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 1 + esphome/components/pn532_i2c/pn532_i2c.cpp | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 324ad87372..30c8b80524 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -88,7 +88,7 @@ void NextionComponent::update_component_settings(bool force_update) { this->send_state_to_nextion(); } - if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { + if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco_is_set)) { this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); this->component_flags_.bco_needs_update = false; } diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index d1d9c200cf..e1ddd1d7c6 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -97,7 +97,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } bool zc_symmetrical = this->frequency_detector_.is_increase_decrease_symmetrical(); - bool amplitude_convergent = this->frequency_detector_.is_increase_decrease_symmetrical(); + bool amplitude_convergent = this->amplitude_detector_.is_amplitude_convergent(); if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway @@ -362,7 +362,7 @@ bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const for (auto v : this->phase_mins) global_min = std::min(global_min, v); for (auto v : this->phase_maxs) - global_max = std::min(global_max, v); + global_max = std::max(global_max, v); float global_amplitude = (global_max - global_min) / 2.0f; float mean_amplitude = this->get_mean_oscillation_amplitude(); return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 9c5caec775..eb6d3931e0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -647,6 +647,7 @@ void Pipsolar::handle_qpiws_(const char *message) { case 34: this->publish_binary_sensor_(enabled, this->warning_high_ac_input_during_bus_soft_start_); value_warnings_present |= enabled.value_or(false); + break; case 35: this->publish_binary_sensor_(enabled, this->warning_battery_equalization_); value_warnings_present |= enabled.value_or(false); diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index b306222a21..41f0f079aa 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -49,7 +49,7 @@ bool PN532I2C::read_response(uint8_t command, std::vector &data) { return false; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return false; @@ -95,7 +95,7 @@ uint8_t PN532I2C::read_response_length_() { return 0; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return 0; From 8f3db96291c06708a23f5171515cd26ec84e33a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:50:26 -0500 Subject: [PATCH 013/101] [esp32_ble_server][weikai][ade7880] Fix copy-paste bugs (#14552) Co-authored-by: Claude Opus 4.6 --- esphome/components/ade7880/ade7880.cpp | 6 +++--- esphome/components/ade7880/ade7880_registers.h | 3 +++ esphome/components/esp32_ble_server/ble_characteristic.cpp | 4 ++-- esphome/components/weikai/weikai.cpp | 4 +++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index f6a15190cd..8fb3e55b91 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -121,7 +121,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -137,7 +137,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -153,7 +153,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h index 8b5b68abb0..9fd8ca3bf5 100644 --- a/esphome/components/ade7880/ade7880_registers.h +++ b/esphome/components/ade7880/ade7880_registers.h @@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402; constexpr uint16_t AFWATTHR = 0xE403; constexpr uint16_t BFWATTHR = 0xE404; constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t ARWATTHR = 0xE406; +constexpr uint16_t BRWATTHR = 0xE407; +constexpr uint16_t CRWATTHR = 0xE408; constexpr uint16_t AFVARHR = 0xE409; constexpr uint16_t BFVARHR = 0xE40A; constexpr uint16_t CFVARHR = 0xE40B; diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index d4ccefd9b2..1806354712 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -310,8 +310,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); } } - esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, param->exec_write.trans_id, + ESP_GATT_OK, nullptr); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 1d835daf1e..3f5d6c787c 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -445,6 +445,7 @@ void WeikaiChannel::flush() { } size_t WeikaiChannel::xfer_fifo_to_buffer_() { + size_t total = 0; size_t to_transfer; size_t free; while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) { @@ -458,9 +459,10 @@ size_t WeikaiChannel::xfer_fifo_to_buffer_() { this->reg(0).read_fifo(data, to_transfer); for (size_t i = 0; i < to_transfer; i++) this->receive_buffer_.push(data[i]); + total += to_transfer; } } // while work to do - return to_transfer; + return total; } /// From 0469612d0774ef26acef975446e4c187dde876f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:02:17 -0500 Subject: [PATCH 014/101] [multiple] Fix assorted medium-severity bugs (#14555) Co-authored-by: Claude Opus 4.6 --- esphome/components/bytebuffer/bytebuffer.h | 2 +- esphome/components/cap1188/cap1188.cpp | 2 +- esphome/components/hte501/hte501.cpp | 2 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +---- esphome/components/inkplate/inkplate.cpp | 10 ++++----- esphome/components/msa3xx/msa3xx.cpp | 6 +---- esphome/components/nfc/ndef_record_text.cpp | 5 +++++ esphome/components/nfc/nfc.cpp | 22 +++++++++++++------ esphome/components/nfc/nfc.h | 2 +- .../components/template/text/template_text.h | 6 ++++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h index 030484ce32..3c68094dbc 100644 --- a/esphome/components/bytebuffer/bytebuffer.h +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -263,7 +263,7 @@ class ByteBuffer { void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; } void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); } - void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint24(uint32_t value, size_t offset) { this->put_uint32_(value, offset, 3); } void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); } void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); } // Signed versions of the put functions diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 9e8c87d147..64bdc620cd 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -92,7 +92,7 @@ void CAP1188Component::loop() { this->read_register(CAP1188_MAIN, &data, 1); data = data & ~CAP1188_MAIN_INT; - this->write_register(CAP1188_MAIN, &data, 2); + this->write_register(CAP1188_MAIN, &data, 1); } for (auto *channel : this->channels_) { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 972e72c170..ef9ef1fabf 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -49,7 +49,7 @@ void HTE501Component::update() { this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) || i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 8a20192c1e..9f510eef74 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -599,11 +599,7 @@ bool INA2XX::read_unsigned_16_(uint8_t reg, uint16_t &out) { } int64_t INA2XX::two_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } } // namespace ina2xx_base } // namespace esphome diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index df9c2b29c7..7551c6fc77 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -407,7 +407,7 @@ void Inkplate::display1b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); @@ -575,7 +575,7 @@ void Inkplate::display3b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); uint32_t pos; uint32_t data; @@ -646,7 +646,7 @@ bool Inkplate::partial_update_() { int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; eink_on_(); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); for (int k = 0; k < rep; k++) { vscan_start_(); @@ -704,7 +704,7 @@ void Inkplate::vscan_start_() { } void Inkplate::hscan_start_(uint32_t d) { - uint8_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); GPIO.out_w1ts = d | clock; GPIO.out_w1tc = this->get_data_pin_mask_() | clock; @@ -751,7 +751,7 @@ void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { uint32_t send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | (((data & 0b11100000) >> 5) << 25); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); for (int k = 0; k < rep; k++) { vscan_start_(); diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index e46bfed193..6d6b21e6af 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -364,11 +364,7 @@ void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset } int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger, diff --git a/esphome/components/nfc/ndef_record_text.cpp b/esphome/components/nfc/ndef_record_text.cpp index 80b0108b46..8a9a2cb014 100644 --- a/esphome/components/nfc/ndef_record_text.cpp +++ b/esphome/components/nfc/ndef_record_text.cpp @@ -14,6 +14,11 @@ NdefRecordText::NdefRecordText(const std::vector &payload) { uint8_t language_code_length = payload[0] & 0b00111111; // Todo, make use of encoding bit? + if (1 + language_code_length > payload.size()) { + ESP_LOGE(TAG, "Record payload too short for language code"); + return; + } + this->language_code_ = std::string(payload.begin() + 1, payload.begin() + 1 + language_code_length); this->text_ = std::string(payload.begin() + 1 + language_code_length, payload.end()); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 8567b0969a..55543cd292 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -35,7 +35,7 @@ uint8_t guess_tag_type(uint8_t uid_length) { } } -uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { +int8_t get_mifare_classic_ndef_start_index(std::vector &data) { for (uint8_t i = 0; i < MIFARE_CLASSIC_BLOCK_SIZE; i++) { if (data[i] == 0x00) { // Do nothing, skip @@ -49,17 +49,25 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { } bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { + if (data.size() < MIFARE_CLASSIC_BLOCK_SIZE) { + ESP_LOGE(TAG, "Error, data too short for NDEF detection."); + return false; + } auto i = get_mifare_classic_ndef_start_index(data); - if (data[i] != 0x03) { + if (i < 0 || data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; } - if (data[i + 1] == 0xFF) { - message_length = ((0xFF & data[i + 2]) << 8) | (0xFF & data[i + 3]); - message_start_index = i + MIFARE_CLASSIC_LONG_TLV_SIZE; + uint8_t idx = static_cast(i); + if (idx + 4 <= data.size() && data[idx + 1] == 0xFF) { + message_length = ((0xFF & data[idx + 2]) << 8) | (0xFF & data[idx + 3]); + message_start_index = idx + MIFARE_CLASSIC_LONG_TLV_SIZE; + } else if (idx + 2 <= data.size()) { + message_length = data[idx + 1]; + message_start_index = idx + MIFARE_CLASSIC_SHORT_TLV_SIZE; } else { - message_length = data[i + 1]; - message_start_index = i + MIFARE_CLASSIC_SHORT_TLV_SIZE; + ESP_LOGE(TAG, "Error, TLV data too short."); + return false; } return true; } diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index cdaea82af6..8ca5cb7ea4 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -72,7 +72,7 @@ ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026. std::string format_bytes(std::span bytes); uint8_t guess_tag_type(uint8_t uid_length); -uint8_t get_mifare_classic_ndef_start_index(std::vector &data); +int8_t get_mifare_classic_ndef_start_index(std::vector &data); bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index); uint32_t get_mifare_classic_buffer_size(uint32_t message_length); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 88c6afdf2c..7f176db09e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -52,7 +52,11 @@ template class TextSaver : public TemplateTextSaverBase { bool hasdata = this->pref_.load(&temp); if (hasdata) { - value.assign(temp + 1, (size_t) temp[0]); + size_t len = static_cast(temp[0]); + if (len > SZ) { + len = SZ; + } + value.assign(temp + 1, len); } this->prev_.assign(value); From 4f4b2bfdecc1ccc06f344d4e0c6dbad88537a3a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:14:35 -0500 Subject: [PATCH 015/101] [bmp581_base][bl0906] Fix 24-bit sign extension bugs (#14558) Co-authored-by: Claude Opus 4.6 --- esphome/components/bl0906/bl0906.cpp | 4 +++- esphome/components/bmp581_base/bmp581_base.cpp | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index c1cd48a1ac..7b643bba98 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -10,7 +10,9 @@ static const char *const TAG = "bl0906"; constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } -constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } +constexpr int32_t to_int32_t(sbe24_t input) { + return static_cast(encode_uint32((uint8_t) input.h, input.m, input.l, 0)) >> 8; +} // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index c4a96ebc39..89a92de31d 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -429,7 +429,7 @@ bool BMP581Component::read_temperature_(float &temperature) { } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) return true; @@ -458,7 +458,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] From 2c83c6a79f356d3f0f6910fc573e47c53e695c5e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:47:56 -0500 Subject: [PATCH 016/101] [shelly_dimmer][lvgl][seeed_mr60fda2][packet_transport] Fix buffer bounds checks (#14534) Co-authored-by: Claude Opus 4.6 --- esphome/components/lvgl/lvgl_esphome.cpp | 3 ++ .../packet_transport/packet_transport.cpp | 2 + .../seeed_mr60fda2/seeed_mr60fda2.cpp | 44 ++++++++----------- .../shelly_dimmer/shelly_dimmer.cpp | 4 +- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 3e447e9169..66cb25b864 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -422,6 +422,9 @@ void LvglComponent::write_random_() { auto row = random_uint32() % this->disp_drv_.ver_res; row = row / this->draw_rounding * this->draw_rounding; auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; + // clamp size so the square fits within the draw buffer + if ((size + 1) * (size + 1) > this->draw_buf_.size) + size = static_cast(sqrtf(this->draw_buf_.size)) - 1; lv_area_t area; area.x1 = col; area.y1 = row; diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 6f1286b469..964037a02c 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -137,6 +137,8 @@ class PacketDecoder { return DECODE_EMPTY; if (this->buffer_[this->position_] != key) return DECODE_UNMATCHED; + if (this->position_ + 1 + sizeof(T) > this->len_) + return DECODE_ERROR; this->position_++; T value = 0; for (size_t i = 0; i != sizeof(T); ++i) { diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 5d571618d3..c6527a948e 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -149,28 +149,25 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { switch (this->current_frame_locate_) { case LOCATE_FRAME_HEADER: // starting buffer if (buffer == FRAME_HEADER_BUFFER) { - this->current_frame_len_ = 1; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_len_ = 0; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_ID_FRAME1: this->current_frame_id_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_ID_FRAME2: this->current_frame_id_ += buffer; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_LENGTH_FRAME_H: this->current_data_frame_len_ = buffer << 8; - if (this->current_data_frame_len_ == 0x00) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + if (this->current_data_frame_len_ == 0) { + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -181,15 +178,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) { this->current_frame_locate_ = LOCATE_FRAME_HEADER; } else { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_TYPE_FRAME1: this->current_frame_type_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_TYPE_FRAME2: @@ -198,8 +193,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { (this->current_frame_type_ == PEOPLE_EXIST_TYPE_BUFFER) || (this->current_frame_type_ == RESULT_INSTALL_HEIGHT) || (this->current_frame_type_ == RESULT_PARAMETERS) || (this->current_frame_type_ == RESULT_HEIGHT_THRESHOLD) || (this->current_frame_type_ == RESULT_SENSITIVITY)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -207,8 +201,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { break; case LOCATE_HEAD_CKSUM_FRAME: if (validate_checksum(this->current_frame_buf_, this->current_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); @@ -223,21 +216,20 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { } break; case LOCATE_DATA_FRAME: - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; - this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer; - if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { - this->current_frame_locate_++; - } - if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) { + if (this->current_frame_len_ >= FRAME_BUF_MAX_SIZE) { ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM); this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + } + this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME + 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; + if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { + this->current_frame_locate_++; } break; case LOCATE_DATA_CKSUM_FRAME: if (validate_checksum(this->current_data_buf_, this->current_data_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; this->process_frame_(); } else { diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index bdb33d31af..88fcbcbfe1 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -188,8 +188,8 @@ bool ShellyDimmer::upgrade_firmware_() { break; } - std::memcpy(buffer, p, BUFFER_SIZE); - p += BUFFER_SIZE; + std::memcpy(buffer, p, len); + p += len; if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); From 587bf68091caffac14f4d7ed2714fac28848354c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:30 -0500 Subject: [PATCH 017/101] [ltr501][pvvx_mithermometer][smt100] Convert static locals to instance members (#14569) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/ltr501/ltr501.cpp | 21 +++++++++---------- esphome/components/ltr501/ltr501.h | 3 +++ .../pvvx_mithermometer/pvvx_mithermometer.cpp | 7 +++---- .../pvvx_mithermometer/pvvx_mithermometer.h | 2 ++ esphome/components/smt100/smt100.cpp | 16 +++++++------- esphome/components/smt100/smt100.h | 3 +++ 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 04de91e362..4c9006be1d 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -146,7 +146,6 @@ void LTRAlsPs501Component::update() { void LTRAlsPs501Component::loop() { ErrorCode err = i2c::ERROR_OK; - static uint8_t tries{0}; switch (this->state_) { case State::DELAYED_SETUP: @@ -175,20 +174,20 @@ void LTRAlsPs501Component::loop() { case State::WAITING_FOR_DATA: if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { - tries = 0; + this->tries_ = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); this->read_sensor_data_(this->als_readings_); this->apply_lux_calculation_(this->als_readings_); this->state_ = State::DATA_COLLECTED; - } else if (tries >= MAX_TRIES) { + } else if (this->tries_ >= MAX_TRIES) { ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); - tries = 0; + this->tries_ = 0; this->status_set_warning(); this->state_ = State::IDLE; return; } else { - tries++; + this->tries_++; } break; @@ -230,21 +229,21 @@ void LTRAlsPs501Component::loop() { } void LTRAlsPs501Component::check_and_trigger_ps_() { - static uint32_t last_high_trigger_time{0}; - static uint32_t last_low_trigger_time{0}; uint16_t ps_data = this->read_ps_data_(); uint32_t now = millis(); if (ps_data != this->ps_readings_) { this->ps_readings_ = ps_data; // Higher values - object is closer to sensor - if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_high_trigger_time = now; + if (ps_data > this->ps_threshold_high_ && + now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_high_trigger_time_ = now; ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_high_); this->on_ps_high_trigger_callback_.call(); - } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_low_trigger_time = now; + } else if (ps_data < this->ps_threshold_low_ && + now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_low_trigger_time_ = now; ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_low_); this->on_ps_low_trigger_callback_.call(); diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 02c025da30..d9a53c9bd4 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -74,6 +74,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { READY_TO_PUBLISH, KEEP_PUBLISHING } state_{State::NOT_INITIALIZED}; + uint8_t tries_{0}; LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; @@ -130,6 +131,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { PsGain501 ps_gain_{PsGain501::PS_GAIN_1}; uint16_t ps_threshold_high_{0xffff}; uint16_t ps_threshold_low_{0x0000}; + uint32_t last_ps_high_trigger_time_{0}; + uint32_t last_ps_low_trigger_time_{0}; // // Sensors for publishing data diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 239a1e74fe..35badf48bb 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -66,12 +66,11 @@ optional PVVXMiThermometer::parse_header_(const esp32_ble_tracker:: return {}; } - static uint8_t last_frame_count = 0; - if (last_frame_count == raw[13]) { - ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + if (this->last_frame_count_ == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", this->last_frame_count_); return {}; } - last_frame_count = raw[13]; + this->last_frame_count_ = raw[13]; return result; } diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index c15e1e7e22..09b5e91a16 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -39,6 +39,8 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *battery_voltage_{nullptr}; sensor::Sensor *signal_strength_{nullptr}; + uint8_t last_frame_count_{0}; + optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); bool report_results_(const optional &result, const char *address); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 1bcb964264..105cc06edb 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -12,10 +12,9 @@ void SMT100Component::update() { } void SMT100Component::loop() { - static char buffer[MAX_LINE_LENGTH]; while (this->available() != 0) { - if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); + if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { + int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); @@ -56,7 +55,6 @@ void SMT100Component::dump_config() { } int SMT100Component::readline_(int readch, char *buffer, int len) { - static int pos = 0; int rpos; if (readch > 0) { @@ -64,13 +62,13 @@ int SMT100Component::readline_(int readch, char *buffer, int len) { case '\n': // Ignore new-lines break; case '\r': // Return on CR - rpos = pos; - pos = 0; // Reset position index ready for next time + rpos = this->readline_pos_; + this->readline_pos_ = 0; // Reset position index ready for next time return rpos; default: - if (pos < len - 1) { - buffer[pos++] = readch; - buffer[pos] = 0; + if (this->readline_pos_ < len - 1) { + buffer[this->readline_pos_++] = readch; + buffer[this->readline_pos_] = 0; } } } diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index df8803e1c6..cb01b1ed55 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -28,6 +28,9 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { protected: int readline_(int readch, char *buffer, int len); + char readline_buffer_[MAX_LINE_LENGTH]{}; + int readline_pos_{0}; + sensor::Sensor *counts_sensor_{nullptr}; sensor::Sensor *permittivity_sensor_{nullptr}; sensor::Sensor *moisture_sensor_{nullptr}; From 5777908da712d64dde2f760d68aaa707b606dc1f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:53 -0500 Subject: [PATCH 018/101] [iaqcore][scd30][sen21231][beken_spi_led_strip] Fix uninitialized variables and missing error checks (#14568) Co-authored-by: Claude Opus 4.6 --- esphome/components/beken_spi_led_strip/led_strip.cpp | 2 +- esphome/components/iaqcore/iaqcore.cpp | 10 ++++++---- esphome/components/scd30/scd30.cpp | 2 +- esphome/components/sen21231/sen21231.cpp | 7 ++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 67b8472257..f425f3ca5c 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -78,7 +78,7 @@ static void spi_set_clock(uint32_t max_hz) { int source_clk = 0; int spi_clk = 0; int div = 0; - uint32_t param; + uint32_t param = PWD_SPI_CLK_BIT; if (max_hz > 4333000) { if (max_hz > 30000000) { spi_clk = 30000000; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 274f9086b6..c414eb8f60 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -10,11 +10,13 @@ static const char *const TAG = "iaqcore"; enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 }; +static constexpr size_t SENSOR_DATA_LENGTH = 9; + struct SensorData { - uint16_t co2; - IAQCoreErrorCode status; int32_t resistance; + uint16_t co2; uint16_t tvoc; + IAQCoreErrorCode status; SensorData(const uint8_t *buffer) { this->co2 = encode_uint16(buffer[0], buffer[1]); @@ -33,9 +35,9 @@ void IAQCore::setup() { } void IAQCore::update() { - uint8_t buffer[sizeof(SensorData)]; + uint8_t buffer[SENSOR_DATA_LENGTH]; - if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, SENSOR_DATA_LENGTH) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3c2c06fd68..e61d0142be 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -222,7 +222,7 @@ bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) } uint16_t SCD30Component::get_forced_calibration_reference() { - uint16_t forced_calibration_reference; + uint16_t forced_calibration_reference = 0; // Get current CO2 calibration if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) { ESP_LOGE(TAG, "Unable to read forced calibration reference."); diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp index 67001c3f14..8c9f3d7134 100644 --- a/esphome/components/sen21231/sen21231.cpp +++ b/esphome/components/sen21231/sen21231.cpp @@ -20,7 +20,12 @@ void Sen21231Sensor::dump_config() { void Sen21231Sensor::read_data_() { person_sensor_results_t results; - this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results)); + if (!this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results))) { + ESP_LOGW(TAG, "Failed to read data from SEN21231"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); ESP_LOGD(TAG, "SEN21231: %d faces detected", results.num_faces); this->publish_state(results.num_faces); if (results.num_faces == 1) { From de7572bd3e7ebb9308744efb6f7faf88c480629d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:04:12 -0500 Subject: [PATCH 019/101] [lightwaverf] Fix ISR safety issues (#14563) Co-authored-by: Claude Opus 4.6 --- esphome/components/lightwaverf/LwRx.cpp | 14 +++++++++++--- esphome/components/lightwaverf/LwRx.h | 4 ++-- esphome/components/lightwaverf/LwTx.cpp | 3 ++- esphome/components/lightwaverf/LwTx.h | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/lightwaverf/LwRx.cpp b/esphome/components/lightwaverf/LwRx.cpp index 2b1ad5e870..9710457850 100644 --- a/esphome/components/lightwaverf/LwRx.cpp +++ b/esphome/components/lightwaverf/LwRx.cpp @@ -8,6 +8,7 @@ #include "LwRx.h" #include +#include "esphome/core/helpers.h" namespace esphome { namespace lightwaverf { @@ -185,13 +186,20 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { bool ret = true; int16_t j = 0; // int if (this->rx_msgcomplete && len <= RX_MSGLEN) { + // Copy message under interrupt lock to prevent ISR overwriting rx_msg mid-read + uint8_t msg_copy[RX_MSGLEN]; + { + InterruptLock lock; + memcpy(msg_copy, this->rx_msg, RX_MSGLEN); + this->rx_msgcomplete = false; + } for (uint8_t i = 0; ret && i < RX_MSGLEN; i++) { if (this->rx_translate || (len != RX_MSGLEN)) { - j = this->rx_find_nibble_(this->rx_msg[i]); + j = this->rx_find_nibble_(msg_copy[i]); if (j < 0) ret = false; } else { - j = this->rx_msg[i]; + j = msg_copy[i]; } switch (len) { case 4: @@ -199,6 +207,7 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { buf[2] = j; if (i == 2) buf[3] = j; + [[fallthrough]]; case 2: if (i == 3) buf[0] = j; @@ -212,7 +221,6 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { break; } } - this->rx_msgcomplete = false; } else { ret = false; } diff --git a/esphome/components/lightwaverf/LwRx.h b/esphome/components/lightwaverf/LwRx.h index 7200f9a51c..8b34de9fbb 100644 --- a/esphome/components/lightwaverf/LwRx.h +++ b/esphome/components/lightwaverf/LwRx.h @@ -105,8 +105,8 @@ class LwRx { uint32_t rx_prev; // time of previous interrupt in microseconds - bool rx_msgcomplete = false; // set high when message available - bool rx_translate = true; // Set false to get raw data + volatile bool rx_msgcomplete = false; // set high when message available + bool rx_translate = true; // Set false to get raw data uint8_t rx_state = 0; diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index b69b93b978..8852935bfd 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -192,7 +192,8 @@ void LwTx::lwtx_set_gap_multiplier(uint8_t gap_multiplier) { this->tx_gap_multip void LwTx::lw_timer_start() { { InterruptLock lock; - static LwTx *arg = this; // NOLINT + static LwTx *arg; + arg = this; timer1_attachInterrupt([] { isr_t_xtimer(arg); }); timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); timer1_write(this->espPeriod); diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h index fe7b942a3a..9192426440 100644 --- a/esphome/components/lightwaverf/LwTx.h +++ b/esphome/components/lightwaverf/LwTx.h @@ -62,8 +62,8 @@ class LwTx { uint8_t tx_repeats = 12; // Number of repeats of message sent uint8_t txon = 1; uint8_t txoff = 0; - bool tx_msg_active = false; // set true to activate message sending - bool tx_translate = true; // Set false to send raw data + volatile bool tx_msg_active = false; // set true to activate message sending + bool tx_translate = true; // Set false to send raw data uint8_t tx_buf[TX_MSGLEN]; // the message buffer during reception uint8_t tx_repeat = 0; // counter for repeats From c26c5935b6c9bb1b7a521efa7188a0c3dad74e66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:19 -1000 Subject: [PATCH 020/101] Bump github/codeql-action from 4.32.5 to 4.32.6 (#14566) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4bd018b5c9..1a0c54da6d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: category: "/language:${{matrix.language}}" From 086c1bb505dc718d35e9c91b6fbea5bb54705735 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:44 -1000 Subject: [PATCH 021/101] Bump docker/build-push-action from 6.19.2 to 7.0.0 in /.github/actions/build-image (#14567) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 38e93c4f17..a895226030 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 035f98569326bafd7da350e4a15c2739c424e987 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:16:36 +0000 Subject: [PATCH 022/101] Bump ruff from 0.15.4 to 0.15.5 (#14565) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d70dd9d0e1..b036da6ef1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.5 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 6b2617b656..93a20896aa 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.4 # also change in .pre-commit-config.yaml when updating +ruff==0.15.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d8deb2255d3a982bb3a755efa9291a2a59014c69 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:18:09 -0500 Subject: [PATCH 023/101] [mipi_rgb] Fix byte order and dirty bounds in fill() (#14537) Co-authored-by: Claude Opus 4.6 --- esphome/components/mipi_rgb/mipi_rgb.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 7ff6868c15..ae7c795846 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -288,9 +288,7 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) { if (!this->check_buffer_()) return; size_t pos = (y * this->width_) + x; - uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); if (this->buffer_[pos] == new_color) return; this->buffer_[pos] = new_color; @@ -315,10 +313,12 @@ void MipiRgb::fill(Color color) { } auto *ptr_16 = reinterpret_cast(this->buffer_); - uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); std::fill_n(ptr_16, this->width_ * this->height_, new_color); + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + this->y_high_ = this->height_ - 1; } int MipiRgb::get_width() { From f53ee70caadb75ef3ffe20f2a471d15ba06e34a7 Mon Sep 17 00:00:00 2001 From: AndreKR Date: Sat, 7 Mar 2026 01:29:20 +0100 Subject: [PATCH 024/101] [http_request] Make TLS buffer configurable on ESP8266 (#14009) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/http_request/__init__.py | 10 ++++++++++ .../components/http_request/http_request_arduino.cpp | 8 +++----- esphome/components/http_request/http_request_arduino.h | 10 ++++++++++ tests/components/http_request/test.esp8266-ard.yaml | 8 +++++--- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 2d6ecae0bc..81337ebdf6 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -50,6 +50,8 @@ CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_REDIRECT_LIMIT = "redirect_limit" CONF_BUFFER_SIZE_RX = "buffer_size_rx" CONF_BUFFER_SIZE_TX = "buffer_size_tx" +CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx" +CONF_TLS_BUFFER_SIZE_TX = "tls_buffer_size_tx" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" @@ -124,6 +126,12 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( cv.uint16_t, cv.only_on_esp32 ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_RX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_TX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32), @@ -150,6 +158,8 @@ async def to_code(config): if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + cg.add(var.set_tls_buffer_size_rx(config[CONF_TLS_BUFFER_SIZE_RX])) + cg.add(var.set_tls_buffer_size_tx(config[CONF_TLS_BUFFER_SIZE_TX])) if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): cg.add(var.set_watchdog_timeout(timeout_ms)) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 56b51e8b5a..f0dd649285 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -18,8 +18,6 @@ namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; #ifdef USE_ESP8266 -static constexpr int RX_BUFFER_SIZE = 512; -static constexpr int TX_BUFFER_SIZE = 512; // ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM static constexpr int ESP8266_SSL_ERR_OOM = -1000; #endif @@ -58,7 +56,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique(); WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); - secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); + secure_client->setBufferSizes(this->tls_buffer_size_rx_, this->tls_buffer_size_tx_); secure_client->setInsecure(); } else { stream_ptr = std::make_unique(); @@ -138,8 +136,8 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); if (last_error == ESP8266_SSL_ERR_OOM) { - ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), - static_cast(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + ESP_LOGW(TAG, "Configured TLS buffer sizes: %u/%u bytes, check max free heap block using the debug component", + (unsigned int) this->tls_buffer_size_rx_, (unsigned int) this->tls_buffer_size_tx_); } } else { ESP_LOGW(TAG, "Connection failure with no error code"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d5ce5c0ff3..b009d45b1c 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -47,10 +47,20 @@ class HttpContainerArduino : public HttpContainer { }; class HttpRequestArduino : public HttpRequestComponent { + public: +#ifdef USE_ESP8266 + void set_tls_buffer_size_rx(uint16_t size) { this->tls_buffer_size_rx_ = size; } + void set_tls_buffer_size_tx(uint16_t size) { this->tls_buffer_size_tx_ = size; } +#endif + protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; +#ifdef USE_ESP8266 + uint16_t tls_buffer_size_rx_{512}; + uint16_t tls_buffer_size_tx_{512}; +#endif }; } // namespace esphome::http_request diff --git a/tests/components/http_request/test.esp8266-ard.yaml b/tests/components/http_request/test.esp8266-ard.yaml index c1937b5a10..dd2d0df62b 100644 --- a/tests/components/http_request/test.esp8266-ard.yaml +++ b/tests/components/http_request/test.esp8266-ard.yaml @@ -1,4 +1,6 @@ -substitutions: - verify_ssl: "false" - <<: !include common.yaml + +http_request: + verify_ssl: false + tls_buffer_size_rx: 16384 + tls_buffer_size_tx: 512 From df11e2765ed016dabcdc211b7321289fa7cbeccf Mon Sep 17 00:00:00 2001 From: Ricardo Sanz Date: Sat, 7 Mar 2026 02:00:52 +0100 Subject: [PATCH 025/101] [climate][haier][template][core] Relocate CONF_CURRENT_TEMPERATURE to general const file (#14503) --- esphome/components/climate/__init__.py | 2 +- esphome/components/haier/climate.py | 8 ++------ esphome/components/template/water_heater/__init__.py | 2 +- esphome/const.py | 1 + 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 1f449ad2a4..f5b91c502c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, CONF_CURRENT_HUMIDITY_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -112,7 +113,6 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 8c3649058f..6c208f6caa 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -3,15 +3,11 @@ import logging from esphome import automation import esphome.codegen as cg from esphome.components import climate, logger, uart -from esphome.components.climate import ( - CONF_CURRENT_TEMPERATURE, - ClimateMode, - ClimatePreset, - ClimateSwingMode, -) +from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, + CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, CONF_ID, CONF_LEVEL, diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 71f98c826a..cb5f2dbe56 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -4,6 +4,7 @@ from esphome.components import water_heater import esphome.config_validation as cv from esphome.const import ( CONF_AWAY, + CONF_CURRENT_TEMPERATURE, CONF_ID, CONF_MODE, CONF_OPTIMISTIC, @@ -18,7 +19,6 @@ from esphome.types import ConfigType from .. import template_ns -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_IS_ON = "is_on" TemplateWaterHeater = template_ns.class_( diff --git a/esphome/const.py b/esphome/const.py index 060e962573..88e3c33fbc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -274,6 +274,7 @@ CONF_CURRENT = "current" CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" +CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" CONF_CUSTOM = "custom" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" From 9b489c9eba639b8dea29dec7815c32c22cda28a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 03:52:51 +0000 Subject: [PATCH 026/101] Bump aioesphomeapi from 44.2.0 to 44.3.1 (#14580) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f111e05a9d..8e875eba62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.2.0 +aioesphomeapi==44.3.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 05ae69b766e9745892e97c8ac40d7017d5fecb3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 19:00:37 -1000 Subject: [PATCH 027/101] [api] Sync api.proto from aioesphomeapi (#14579) --- esphome/components/api/api.proto | 132 ++++++++++++++ esphome/components/api/api_pb2.cpp | 161 +++++++++++++++++ esphome/components/api/api_pb2.h | 200 ++++++++++++++++++++- esphome/components/api/api_pb2_dump.cpp | 117 ++++++++++++ esphome/components/api/api_pb2_service.cpp | 66 +++++++ esphome/components/api/api_pb2_service.h | 21 +++ 6 files changed, 696 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 802e3e3ae2..618fd1b83c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -58,6 +58,7 @@ service APIConnection { rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} + rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} @@ -69,6 +70,12 @@ service APIConnection { rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} + + rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {} + rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {} + rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {} + rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {} + rpc serial_proxy_request(SerialProxyRequest) returns (void) {} } @@ -198,6 +205,17 @@ message DeviceInfo { uint32 area_id = 3; } +enum SerialProxyPortType { + SERIAL_PROXY_PORT_TYPE_TTL = 0; + SERIAL_PROXY_PORT_TYPE_RS232 = 1; + SERIAL_PROXY_PORT_TYPE_RS485 = 2; +} + +message SerialProxyInfo { + string name = 1; // Human-readable port name + SerialProxyPortType port_type = 2; // Port type (RS232, RS485) +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -260,6 +278,9 @@ message DeviceInfoResponse { // Indicates if Z-Wave proxy support is available and features supported uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + + // Serial proxy instance metadata + repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; } message ListEntitiesRequest { @@ -2517,3 +2538,114 @@ message InfraredRFReceiveEvent { fixed32 key = 2; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } + +// ==================== SERIAL PROXY ==================== + +enum SerialProxyParity { + SERIAL_PROXY_PARITY_NONE = 0; + SERIAL_PROXY_PARITY_EVEN = 1; + SERIAL_PROXY_PARITY_ODD = 2; +} + +// Configure UART parameters for a serial proxy instance +message SerialProxyConfigureRequest { + option (id) = 138; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 baudrate = 2; // Baud rate in bits per second + bool flow_control = 3; // Enable hardware flow control + SerialProxyParity parity = 4; // Parity setting + uint32 stop_bits = 5; // Number of stop bits (1 or 2) + uint32 data_size = 6; // Number of data bits (5-8) +} + +// Data received from a serial device, forwarded to clients +message SerialProxyDataReceived { + option (id) = 139; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data received from the serial device +} + +// Write data to a serial device +message SerialProxyWriteRequest { + option (id) = 140; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data to write to the serial device +} + +// Set modem control pin states (RTS and DTR) +message SerialProxySetModemPinsRequest { + option (id) = 141; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +// Request current modem control pin states +message SerialProxyGetModemPinsRequest { + option (id) = 142; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) +} + +// Response with current modem control pin states +message SerialProxyGetModemPinsResponse { + option (id) = 143; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +enum SerialProxyRequestType { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) +} + +// Generic request message for simple serial proxy operations +message SerialProxyRequest { + option (id) = 144; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Request type +} + +// ==================== BLUETOOTH CONNECTION PARAMS ==================== +message BluetoothSetConnectionParamsRequest { + option (id) = 145; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 min_interval = 2; // units of 1.25ms + uint32 max_interval = 3; // units of 1.25ms + uint32 latency = 4; + uint32 timeout = 5; // units of 10ms +} + +message BluetoothSetConnectionParamsResponse { + option (id) = 146; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + int32 error = 2; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d8703aa416..b1176de539 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -71,6 +71,18 @@ uint32_t DeviceInfo::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +void SerialProxyInfo::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_uint32(2, static_cast(this->port_type)); +} +uint32_t SerialProxyInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->port_type)); + return size; +} +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); @@ -125,6 +137,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(24, this->zwave_home_id); #endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + buffer.encode_message(25, it); + } +#endif } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; @@ -180,6 +197,11 @@ uint32_t DeviceInfoResponse::calculate_size() const { #endif #ifdef USE_ZWAVE_PROXY size += ProtoSize::calc_uint32(2, this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + size += ProtoSize::calc_message_force(2, it.calculate_size()); + } #endif return size; } @@ -3714,5 +3736,144 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->baudrate = value.as_uint32(); + break; + case 3: + this->flow_control = value.as_bool(); + break; + case 4: + this->parity = static_cast(value.as_uint32()); + break; + case 5: + this->stop_bits = value.as_uint32(); + break; + case 6: + this->data_size = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +} +uint32_t SerialProxyDataReceived::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_length(1, this->data_len_); + return size; +} +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->line_states = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, this->line_states); +} +uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, this->line_states); + return size; +} +bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->type = static_cast(value.as_uint32()); + break; + default: + return false; + } + return true; +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->address = value.as_uint64(); + break; + case 2: + this->min_interval = value.as_uint32(); + break; + case 3: + this->max_interval = value.as_uint32(); + break; + case 4: + this->latency = value.as_uint32(); + break; + case 5: + this->timeout = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_int32(2, this->error); +} +uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_int32(1, this->error); + return size; +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 89cb1158f3..a6167dc810 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -11,6 +11,11 @@ namespace esphome::api { namespace enums { +enum SerialProxyPortType : uint32_t { + SERIAL_PROXY_PORT_TYPE_TTL = 0, + SERIAL_PROXY_PORT_TYPE_RS232 = 1, + SERIAL_PROXY_PORT_TYPE_RS485 = 2, +}; enum EntityCategory : uint32_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -317,6 +322,18 @@ enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; #endif +#ifdef USE_SERIAL_PROXY +enum SerialProxyParity : uint32_t { + SERIAL_PROXY_PARITY_NONE = 0, + SERIAL_PROXY_PARITY_EVEN = 1, + SERIAL_PROXY_PARITY_ODD = 2, +}; +enum SerialProxyRequestType : uint32_t { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, +}; +#endif } // namespace enums @@ -477,10 +494,24 @@ class DeviceInfo final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyInfo final : public ProtoMessage { + public: + StringRef name{}; + enums::SerialProxyPortType port_type{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 255; + static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -532,6 +563,9 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; +#endif +#ifdef USE_SERIAL_PROXY + std::array serial_proxies{}; #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -3061,5 +3095,169 @@ class InfraredRFReceiveEvent final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyConfigureRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 138; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_configure_request"; } +#endif + uint32_t instance{0}; + uint32_t baudrate{0}; + bool flow_control{false}; + enums::SerialProxyParity parity{}; + uint32_t stop_bits{0}; + uint32_t data_size{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyDataReceived final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 139; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_data_received"; } +#endif + uint32_t instance{0}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyWriteRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 140; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_write_request"; } +#endif + uint32_t instance{0}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 141; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 142; + static constexpr uint8_t ESTIMATED_SIZE = 4; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } +#endif + uint32_t instance{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 143; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 144; + static constexpr uint8_t ESTIMATED_SIZE = 6; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif +#ifdef USE_BLUETOOTH_PROXY +class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 145; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_request"; } +#endif + uint64_t address{0}; + uint32_t min_interval{0}; + uint32_t max_interval{0}; + uint32_t latency{0}; + uint32_t timeout{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothSetConnectionParamsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 146; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_response"; } +#endif + uint64_t address{0}; + int32_t error{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4eec42e936..086b1bdc2f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint out.append(hex_buf).append("\n"); } +template<> const char *proto_enum_to_string(enums::SerialProxyPortType value) { + switch (value) { + case enums::SERIAL_PROXY_PORT_TYPE_TTL: + return "SERIAL_PROXY_PORT_TYPE_TTL"; + case enums::SERIAL_PROXY_PORT_TYPE_RS232: + return "SERIAL_PROXY_PORT_TYPE_RS232"; + case enums::SERIAL_PROXY_PORT_TYPE_RS485: + return "SERIAL_PROXY_PORT_TYPE_RS485"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -752,6 +764,32 @@ template<> const char *proto_enum_to_string(enums: } } #endif +#ifdef USE_SERIAL_PROXY +template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { + switch (value) { + case enums::SERIAL_PROXY_PARITY_NONE: + return "SERIAL_PROXY_PARITY_NONE"; + case enums::SERIAL_PROXY_PARITY_EVEN: + return "SERIAL_PROXY_PARITY_EVEN"; + case enums::SERIAL_PROXY_PARITY_ODD: + return "SERIAL_PROXY_PARITY_ODD"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::SerialProxyRequestType value) { + switch (value) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + default: + return "UNKNOWN"; + } +} +#endif const char *HelloRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "HelloRequest"); @@ -801,6 +839,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyInfo"); + dump_field(out, "name", this->name); + dump_field(out, "port_type", static_cast(this->port_type)); + return out.c_str(); +} +#endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "DeviceInfoResponse"); dump_field(out, "name", this->name); @@ -861,6 +907,13 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif #ifdef USE_ZWAVE_PROXY dump_field(out, "zwave_home_id", this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + out.append(" serial_proxies: "); + it.dump_to(out); + out.append("\n"); + } #endif return out.c_str(); } @@ -2510,6 +2563,70 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "baudrate", this->baudrate); + dump_field(out, "flow_control", this->flow_control); + dump_field(out, "parity", static_cast(this->parity)); + dump_field(out, "stop_bits", this->stop_bits); + dump_field(out, "data_size", this->data_size); + return out.c_str(); +} +const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyDataReceived"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + return out.c_str(); +} +const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyWriteRequest"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data, this->data_len); + return out.c_str(); +} +const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); + dump_field(out, "instance", this->instance); + return out.c_str(); +} +const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast(this->type)); + return out.c_str(); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); + dump_field(out, "address", this->address); + dump_field(out, "min_interval", this->min_interval); + dump_field(out, "max_interval", this->max_interval); + dump_field(out, "latency", this->latency); + dump_field(out, "timeout", this->timeout); + return out.c_str(); +} +const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); + dump_field(out, "address", this->address); + dump_field(out, "error", this->error); + return out.c_str(); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f9151ae3b4..f2f7fa5238 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -634,6 +634,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_infrared_rf_transmit_raw_timings_request(msg); break; } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyConfigureRequest::MESSAGE_TYPE: { + SerialProxyConfigureRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg); +#endif + this->on_serial_proxy_configure_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyWriteRequest::MESSAGE_TYPE: { + SerialProxyWriteRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg); +#endif + this->on_serial_proxy_write_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxySetModemPinsRequest::MESSAGE_TYPE: { + SerialProxySetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg); +#endif + this->on_serial_proxy_set_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: { + SerialProxyGetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg); +#endif + this->on_serial_proxy_get_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyRequest::MESSAGE_TYPE: { + SerialProxyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg); +#endif + this->on_serial_proxy_request(msg); + break; + } +#endif +#ifdef USE_BLUETOOTH_PROXY + case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: { + BluetoothSetConnectionParamsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg); +#endif + this->on_bluetooth_set_connection_params_request(msg); + break; + } #endif default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e70b97196b..a031d2d969 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -216,6 +216,27 @@ class APIServerConnectionBase : public ProtoService { virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; +#endif + protected: void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; From cbebb811965d63f115ea4fba5c8fd7f6da393b29 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Sat, 7 Mar 2026 08:12:27 -0800 Subject: [PATCH 028/101] [openthread] move esp functions into correct file (#14588) --- esphome/components/openthread/openthread.cpp | 9 ++------- esphome/components/openthread/openthread.h | 2 ++ esphome/components/openthread/openthread_esp.cpp | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index fb81481299..1596b6e990 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,7 +1,6 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" -#include "esp_openthread.h" #include @@ -51,7 +50,7 @@ void OpenThreadComponent::on_state_changed_(otChangedFlags flags, void *context) auto *self = static_cast(context); // This runs on the OpenThread task thread with the OT lock held, // so we can safely call otThreadGetDeviceRole directly. - otInstance *instance = esp_openthread_get_instance(); + otInstance *instance = self->get_openthread_instance_(); otDeviceRole role = otThreadGetDeviceRole(instance); self->connected_ = role >= OT_DEVICE_ROLE_CHILD; } @@ -233,16 +232,12 @@ bool OpenThreadComponent::teardown() { otSrpClientClearHostAndServices(instance); otSrpClientBuffersFreeAllServices(instance); global_openthread_component = nullptr; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) ESP_LOGD(TAG, "Exit main loop "); - int error = esp_openthread_mainloop_exit(); + int error = this->openthread_stop_(); if (error != ESP_OK) { ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); this->teardown_complete_ = true; } -#else - this->teardown_complete_ = true; -#endif } return this->teardown_complete_; } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index c87f4fa7c1..75d8fe11fd 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -44,6 +44,8 @@ class OpenThreadComponent : public Component { protected: std::optional get_omr_address_(InstanceLock &lock); static void on_state_changed_(otChangedFlags flags, void *context); + otInstance *get_openthread_instance_(); + int openthread_stop_(); std::function factory_reset_external_callback_; #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index cdc7a404b2..9cc9223b52 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -190,6 +190,8 @@ void OpenThreadComponent::ot_main() { vTaskDelete(NULL); } +int OpenThreadComponent::openthread_stop_() { return esp_openthread_mainloop_exit(); } + network::IPAddresses OpenThreadComponent::get_ip_addresses() { network::IPAddresses addresses; struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -204,6 +206,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { return addresses; } +// not thread safe, only use in read-only use cases +otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } + std::optional InstanceLock::try_acquire(int delay) { if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); From 0e106d843c730eb15e083ebd0147124dadc99a5a Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 7 Mar 2026 17:18:21 +0100 Subject: [PATCH 029/101] [nrf52][zephyr] support for multi on rate callbacks (#14557) --- esphome/components/nrf52/__init__.py | 3 +++ esphome/components/nrf52/dfu.cpp | 35 ++++++++------------------- esphome/components/nrf52/dfu.h | 1 - esphome/components/zephyr/__init__.py | 20 +++++++++++---- esphome/components/zephyr/cdc_acm.cpp | 30 +++++++++++++++++++++++ esphome/components/zephyr/cdc_acm.h | 27 +++++++++++++++++++++ esphome/components/zephyr/const.py | 2 ++ 7 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 esphome/components/zephyr/cdc_acm.cpp create mode 100644 esphome/components/zephyr/cdc_acm.h diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a12d1db1ab..0a9fb5939a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -20,8 +20,10 @@ from esphome.components.zephyr import ( ) from esphome.components.zephyr.const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOOTLOADER, KEY_ZEPHYR, + CdcAcm, ) import esphome.config_validation as cv from esphome.const import ( @@ -159,6 +161,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_VERSION): cv.string_strict, } ), + cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), } ), set_framework, diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp index 9e49373467..c2017248d2 100644 --- a/esphome/components/nrf52/dfu.cpp +++ b/esphome/components/nrf52/dfu.cpp @@ -2,42 +2,27 @@ #ifdef USE_NRF52_DFU -#include -#include -#include #include "esphome/core/log.h" +#include "esphome/components/zephyr/cdc_acm.h" namespace esphome { namespace nrf52 { static const char *const TAG = "dfu"; -volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS -#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), - -static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { - if (rate == 1200) { - goto_dfu = true; - } -} void DeviceFirmwareUpdate::setup() { this->reset_pin_->setup(); - const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; - for (auto &idx : cdc_dev) { - cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); - } -} - -void DeviceFirmwareUpdate::loop() { - if (goto_dfu) { - goto_dfu = false; - volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; - (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; - this->reset_pin_->digital_write(true); - } +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) { + if (rate == 1200) { + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } + }); +#endif } void DeviceFirmwareUpdate::dump_config() { diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h index 979a4567cf..71060e43c1 100644 --- a/esphome/components/nrf52/dfu.h +++ b/esphome/components/nrf52/dfu.h @@ -10,7 +10,6 @@ namespace nrf52 { class DeviceFirmwareUpdate : public Component { public: void setup() override; - void loop() override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void dump_config() override; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 4cc71bddca..b8a091feb9 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -5,11 +5,13 @@ from typing import TypedDict import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_BOARD, KEY_CORE, KEY_FRAMEWORK_VERSION -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.types import ConfigType from .const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOARD, KEY_BOOTLOADER, KEY_EXTRA_BUILD_FILES, @@ -54,7 +56,7 @@ class ZephyrData(TypedDict): user: dict[str, list[str]] -def zephyr_set_core_data(config): +def zephyr_set_core_data(config: ConfigType) -> None: CORE.data[KEY_ZEPHYR] = ZephyrData( board=config[CONF_BOARD], bootloader=config[KEY_BOOTLOADER], @@ -64,7 +66,6 @@ def zephyr_set_core_data(config): pm_static=[], user={}, ) - return config def zephyr_data() -> ZephyrData: @@ -110,7 +111,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: cg.add_platformio_option("extra_scripts", [key]) -def zephyr_to_code(config): +def zephyr_to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_ZEPHYR") cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") @@ -132,6 +133,15 @@ def zephyr_to_code(config): Path(__file__).parent / "pre_build.py.script", ) + CORE.add_job(_cdc_acm_to_code, config) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _cdc_acm_to_code(config: ConfigType) -> None: + if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF]: + var = cg.new_Pvariable(config[CONF_CDC_ACM]) + await cg.register_component(var, {}) + def zephyr_setup_preferences(): cg.add(zephyr_ns.setup_preferences()) @@ -151,7 +161,7 @@ def _format_prj_conf_val(value: PrjConfValueType) -> str: raise ValueError -def zephyr_add_cdc_acm(config, id): +def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None: framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] if CORE.is_nrf52 and framework_ver >= cv.Version(3, 2, 0): zephyr_add_prj_conf("CONFIG_USB_DEVICE_STACK_NEXT", False) diff --git a/esphome/components/zephyr/cdc_acm.cpp b/esphome/components/zephyr/cdc_acm.cpp new file mode 100644 index 0000000000..04ee9a0bef --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.cpp @@ -0,0 +1,30 @@ +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) +#include "cdc_acm.h" +#include +#include + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +namespace esphome::zephyr { + +CdcAcm::CdcAcm() { global_cdc_acm = this; } + +void CdcAcm::setup() { +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + // only one global callback can be registered + cdc_acm_dte_rate_callback_set(idx, CdcAcm::cdc_dte_rate_callback_); + } +#endif // DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) +} + +void CdcAcm::cdc_dte_rate_callback_(const struct device *device, uint32_t rate) { + global_cdc_acm->defer([device, rate]() { global_cdc_acm->rate_callbacks_.call(device, rate); }); +} + +CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/cdc_acm.h b/esphome/components/zephyr/cdc_acm.h new file mode 100644 index 0000000000..2e9da85a11 --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.h @@ -0,0 +1,27 @@ +#pragma once +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::zephyr { + +class CdcAcm : public Component { + public: + CdcAcm(); + void setup() override; + void add_on_rate_callback(std::function &&callback) { + this->rate_callbacks_.add(std::move(callback)); + } + + protected: + static void cdc_dte_rate_callback_(const device *device, uint32_t rate); + CallbackManager rate_callbacks_; +}; + +extern CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py index 06a4fc42bc..f67b058ed7 100644 --- a/esphome/components/zephyr/const.py +++ b/esphome/components/zephyr/const.py @@ -14,3 +14,5 @@ KEY_BOARD: Final = "board" KEY_USER: Final = "user" zephyr_ns = cg.esphome_ns.namespace("zephyr") +CdcAcm = zephyr_ns.class_("CdcAcm", cg.Component) +CONF_CDC_ACM = "cdc_acm" From 8b62c35ea7831d2c0e24f836e096b7b4c1cb1ca0 Mon Sep 17 00:00:00 2001 From: Simon Redman Date: Sat, 7 Mar 2026 11:41:37 -0500 Subject: [PATCH 030/101] [uart] Add error message when initializing UART with unsupported configuration (#13229) --- esphome/components/uart/uart_component_libretiny.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index cb4465068d..83d2acb332 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -110,7 +110,7 @@ void LibreTinyUARTComponent::setup() { #if LT_HW_UART2 ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX); #endif - this->mark_failed(); + this->mark_failed(LOG_STR("SoftwareSerial is not implemented for this chip.")); return; #endif } From 15ffbb0b05da71f6ef008c0a38ffd185d0490edc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:51:02 -0500 Subject: [PATCH 031/101] [uart] Fully enable raw mode with host serial (#14573) --- esphome/components/uart/uart_component_host.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0e5ef3c6bd..9dce25c500 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -124,17 +124,10 @@ void HostUartComponent::setup() { fcntl(this->file_descriptor_, F_SETFL, 0); struct termios options; tcgetattr(this->file_descriptor_, &options); + cfmakeraw(&options); options.c_cflag &= ~CRTSCTS; options.c_cflag |= CREAD | CLOCAL; - options.c_lflag &= ~ICANON; - options.c_lflag &= ~ECHO; - options.c_lflag &= ~ECHOE; - options.c_lflag &= ~ECHONL; - options.c_lflag &= ~ISIG; - options.c_iflag &= ~(IXON | IXOFF | IXANY); - options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); - options.c_oflag &= ~OPOST; - options.c_oflag &= ~ONLCR; + options.c_iflag &= ~(IXOFF | IXANY); // Set data bits options.c_cflag &= ~CSIZE; // Mask the character size bits switch (this->data_bits_) { From abc870006cce42296a2f581478f79fbe69df2391 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:25:13 -1000 Subject: [PATCH 032/101] [captive_portal] Enable support for RP2040 (#14505) --- esphome/components/captive_portal/__init__.py | 9 ++++----- tests/components/captive_portal/test.rp2040-ard.yaml | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 tests/components/captive_portal/test.rp2040-ard.yaml diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 6c190814c0..cd877fc879 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, ) @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ] ), @@ -103,11 +105,8 @@ async def to_code(config): if config[CONF_COMPRESSION] == "gzip": cg.add_define("USE_CAPTIVE_PORTAL_GZIP") - if CORE.using_arduino: - if CORE.is_esp8266: - cg.add_library("DNSServer", None) - if CORE.is_libretiny: - cg.add_library("DNSServer", None) + if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040): + cg.add_library("DNSServer", None) # Only compile the ESP-IDF DNS server when using ESP-IDF framework diff --git a/tests/components/captive_portal/test.rp2040-ard.yaml b/tests/components/captive_portal/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From f57fa4cc8d66a99f1e6bef885e2f0c8f96280c04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:25:33 -1000 Subject: [PATCH 033/101] [bluetooth_proxy] Add BLE connection parameters API (#14577) --- esphome/components/api/api_connection.cpp | 3 ++ esphome/components/api/api_connection.h | 1 + .../bluetooth_proxy/bluetooth_connection.h | 4 +++ .../bluetooth_proxy/bluetooth_proxy.cpp | 29 +++++++++++++++++++ .../bluetooth_proxy/bluetooth_proxy.h | 3 ++ .../esp32_ble_client/ble_client_base.cpp | 5 ++-- .../esp32_ble_client/ble_client_base.h | 4 +-- 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8721072e49..bd3de02895 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1188,6 +1188,9 @@ void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScanner bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); } +void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_set_connection_params(msg); +} #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 54b6db6800..b075bc83ab 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -148,6 +148,7 @@ class APIConnection final : public APIServerConnectionBase { void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; void on_subscribe_bluetooth_connections_free_request() override; void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 60bbc93e8b..b50ea2d6a2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -24,6 +24,10 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { + return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom"); + } + void set_address(uint64_t address) override; protected: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 21da4ead14..87206996b2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,7 +3,9 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include #include +#include #ifdef USE_ESP32 @@ -361,6 +363,33 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest } } +void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) { + if (this->api_connection_ == nullptr) + return; + + auto *connection = this->get_connection_(msg.address, false); + api::BluetoothSetConnectionParamsResponse resp; + resp.address = msg.address; + + if (connection == nullptr || !connection->connected()) { + ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected", + connection ? static_cast(connection->connection_index_) : -1, + connection ? connection->address_str() : "unknown"); + resp.error = ESP_GATT_NOT_CONNECTED; + this->api_connection_->send_message(resp); + return; + } + + // Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values; + // clamp to uint16_t since the current BLE spec defines these as 16-bit. + constexpr uint32_t max_val = std::numeric_limits::max(); + resp.error = connection->update_connection_params(static_cast(std::min(msg.min_interval, max_val)), + static_cast(std::min(msg.max_interval, max_val)), + static_cast(std::min(msg.latency, max_val)), + static_cast(std::min(msg.timeout, max_val))); + this->api_connection_->send_message(resp); +} + void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 85461755aa..f1b723e719 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -46,6 +46,7 @@ enum BluetoothProxyFeature : uint32_t { FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_STATE_AND_MODE = 1 << 6, + FEATURE_CONNECTION_PARAMS_SETTING = 1 << 7, }; enum BluetoothProxySubscriptionFlag : uint32_t { @@ -82,6 +83,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg); void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); @@ -130,6 +132,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_PAIRING; flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + flags |= BluetoothProxyFeature::FEATURE_CONNECTION_PARAMS_SETTING; } return flags; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6a85c784a..2f17334c77 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -236,8 +236,8 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } -void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout, const char *param_type) { +esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); conn_params.min_int = min_interval; @@ -249,6 +249,7 @@ void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_inte if (err != ESP_OK) { this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); } + return err; } void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c2336b2349..af4f1b3029 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -129,8 +129,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_event_(const char *name); void log_gattc_lifecycle_event_(const char *name); void log_gattc_data_event_(const char *name); - void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, - const char *param_type); + esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void log_gattc_warning_(const char *operation, esp_gatt_status_t status); From 45f20d9c06119e3c02aa8e8be131297c032da4e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:26:01 -1000 Subject: [PATCH 034/101] [core] Merge set_name + set_entity_strings into configure_entity_ (#14444) Co-authored-by: Claude Opus 4.6 --- esphome/core/entity_base.cpp | 15 ++- esphome/core/entity_base.h | 30 ++--- esphome/core/entity_helpers.py | 17 ++- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 2 +- tests/component_tests/sensor/test_sensor.py | 15 ++- tests/component_tests/text/test_text.py | 2 +- .../text_sensor/test_text_sensor.py | 25 ++++- tests/unit_tests/core/test_entity_helpers.py | 103 +++++++----------- 9 files changed, 112 insertions(+), 99 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 5c4e1c4445..3274640eb3 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -10,8 +10,8 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const char *name) { this->set_name(name, 0); } -void EntityBase::set_name(const char *name, uint32_t object_id_hash) { + +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,6 +44,17 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { this->calc_object_id_(); } } + // Unpack entity string table indices. + // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) +#ifdef USE_ENTITY_DEVICE_CLASS + this->device_class_idx_ = entity_strings_packed & 0xFF; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; +#endif +#ifdef USE_ENTITY_ICON + this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; +#endif } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 20eb68b67a..accd532b0d 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -12,6 +12,10 @@ #include "device.h" #endif +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests + namespace esphome { // Extern lookup functions for entity string tables. @@ -54,12 +58,8 @@ enum EntityCategory : uint8_t { // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: - // Get/set the name of this Entity + // Get the name of this Entity const StringRef &get_name() const; - void set_name(const char *name); - /// Set name with pre-computed object_id hash (avoids runtime hash calculation) - /// Use hash=0 for dynamic names that need runtime calculation - void set_name(const char *name, uint32_t object_id_hash); // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } @@ -104,20 +104,6 @@ class EntityBase { this->flags_.entity_category = static_cast(entity_category); } - // Set entity string table indices — one call per entity from codegen. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) - void set_entity_strings([[maybe_unused]] uint32_t packed) { -#ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = packed & 0xFF; -#endif -#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (packed >> 8) & 0xFF; -#endif -#ifdef USE_ENTITY_ICON - this->icon_idx_ = (packed >> 16) & 0xFF; -#endif - } - // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. @@ -239,6 +225,12 @@ class EntityBase { } protected: + friend void ::setup(); + friend void ::original_setup(); + + /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index a46d2466fd..4fa109fb0e 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -31,8 +31,10 @@ DOMAIN = "entity_string_pool" _KEY_DC_IDX = "_entity_dc_idx" _KEY_UOM_IDX = "_entity_uom_idx" _KEY_ICON_IDX = "_entity_icon_idx" +_KEY_ENTITY_NAME = "_entity_name" +_KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h: +# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: # [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) _DC_SHIFT = 0 _UOM_SHIFT = 8 @@ -219,17 +221,18 @@ def setup_unit_of_measurement(config: ConfigType) -> None: def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single set_entity_strings() call with all packed indices. + """Emit a single configure_entity_() call with name, hash, and packed string indices. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. """ + entity_name = config[_KEY_ENTITY_NAME] + object_id_hash = config[_KEY_OBJECT_ID_HASH] dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - if packed != 0: - add(var.set_entity_strings(packed)) + add(var.configure_entity_(entity_name, object_id_hash, packed)) def get_base_entity_object_id( @@ -331,13 +334,15 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) - # Set the entity name with pre-computed object_id hash + # Pre-compute entity name and object_id hash for configure_entity_() + # which is emitted later by finalize_entity_strings(). # For named entities: pre-compute hash from entity name # For empty-name entities: pass 0, C++ calculates hash at runtime from # device name, friendly_name, or app name (bug-for-bug compatibility) entity_name = config[CONF_NAME] object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 - add(var.set_name(entity_name, object_id_hash)) + config[_KEY_ENTITY_NAME] = entity_name + config[_KEY_OBJECT_ID_HASH] = object_id_hash # Only set disabled_by_default if True (default is False) if config[CONF_DISABLED_BY_DEFAULT]: add(var.set_disabled_by_default(True)) diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index ce4e64681f..fbc2f37d9a 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1",' in main_cpp + assert 'bs_1->configure_entity_("test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 797b6fb1a4..9f94d61c8c 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1",' in main_cpp + assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 221e7edf2c..d9ab3a022c 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,5 +1,15 @@ """Tests for the sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_sensor_device_class_set(generate_main): """ @@ -10,5 +20,6 @@ def test_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") - # Then - assert "s_1->set_entity_strings(" in main_cpp + # Then: device_class: voltage means packed value must be non-zero + packed = _extract_packed_value(main_cpp, "s_1") + assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 16f5f980a5..3ceaa9b8f8 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text",' in main_cpp + assert 'it_1->configure_entity_("test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 4aaebe04d1..f30b820e94 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,5 +1,15 @@ """Tests for the text sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_text_sensor_is_setup(generate_main): """ @@ -25,9 +35,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp + assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp + assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp + assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): @@ -53,6 +63,9 @@ def test_text_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_entity_strings(" in main_cpp - assert "ts_3->set_entity_strings(" in main_cpp + # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date + # so their packed values must be non-zero + packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + assert packed_ts_2 != 0 + packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 1392a1d043..3f6faaee54 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -32,9 +32,11 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting names from set_name calls -# Matches: .set_name("name", hash) or .set_name("name") -SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']') +# Pre-compiled regex pattern for extracting names from configure_entity_/set_name calls +# Matches: .configure_entity_("name", ...) or .set_name("name", ...) +ENTITY_NAME_PATTERN = re.compile( + r'\.(?:configure_entity_|set_name)\(["\']([^"\']*)["\']' +) FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -276,15 +278,23 @@ def setup_test_environment() -> Generator[list[str], None, None]: entity_helpers.add = original_add -def extract_object_id_from_expressions(expressions: list[str]) -> str | None: - """Extract the object ID that would be computed from set_name calls. +def extract_object_id_from_config(config: dict[str, Any]) -> str | None: + """Extract the object ID from config keys set by _setup_entity_impl.""" + name = config.get("_entity_name") + if name is None: + return None + if name: + return sanitize(snake_case(name)) + # Empty name - fall back to friendly_name or device name + if CORE.friendly_name: + return sanitize(snake_case(CORE.friendly_name)) + return sanitize(snake_case(CORE.name)) if CORE.name else None - Since object_id is now computed from the name (via snake_case + sanitize), - we extract the name from set_name() calls and compute the expected object_id. - For empty names, we fall back to CORE.friendly_name or CORE.name. - """ + +def extract_object_id_from_expressions(expressions: list[str]) -> str | None: + """Extract the object ID from configure_entity_() calls in generated expressions.""" for expr in expressions: - if match := SET_NAME_PATTERN.search(expr): + if match := ENTITY_NAME_PATTERN.search(expr): name = match.group(1) if name: return sanitize(snake_case(name)) @@ -299,8 +309,6 @@ def extract_object_id_from_expressions(expressions: list[str]) -> str | None: async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: """Test setup_entity with unique names.""" - added_expressions = setup_test_environment - # Create mock entities var1 = MockObj("sensor1") var2 = MockObj("sensor2") @@ -312,13 +320,10 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> } await _setup_entity_impl(var1, config1, "sensor") - # Get object ID from first entity - object_id1 = extract_object_id_from_expressions(added_expressions) + # Get object ID from first entity (stored in config, emitted later by finalize) + object_id1 = extract_object_id_from_config(config1) assert object_id1 == "temperature" - # Clear for next entity - added_expressions.clear() - # Set up second entity with different name config2 = { CONF_NAME: "Humidity", @@ -327,7 +332,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> await _setup_entity_impl(var2, config2, "sensor") # Get object ID from second entity - object_id2 = extract_object_id_from_expressions(added_expressions) + object_id2 = extract_object_id_from_config(config2) assert object_id2 == "humidity" @@ -337,8 +342,6 @@ async def test_setup_entity_different_platforms( ) -> None: """Test that same name on different platforms doesn't conflict.""" - added_expressions = setup_test_environment - # Create mock entities sensor = MockObj("sensor1") binary_sensor = MockObj("binary_sensor1") @@ -356,15 +359,11 @@ async def test_setup_entity_different_platforms( (text_sensor, "text_sensor"), ] - object_ids: list[str] = [] for var, platform in platforms: - added_expressions.clear() await _setup_entity_impl(var, config, platform) - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) - # All should get base object ID without suffix - assert all(obj_id == "status" for obj_id in object_ids) + # All should get the same object ID (name stored in config, not platform-specific) + assert extract_object_id_from_config(config) == "status" @pytest.fixture @@ -389,7 +388,6 @@ async def test_setup_entity_with_devices( setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] ) -> None: """Test that same name on different devices doesn't conflict.""" - added_expressions = setup_test_environment # Create mock devices device1_id = ID("device1", type="Device") @@ -418,24 +416,18 @@ async def test_setup_entity_with_devices( } # Get object IDs - object_ids: list[str] = [] for var, config in [(sensor1, config1), (sensor2, config2)]: - added_expressions.clear() await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) # Both should get base object ID without suffix (different devices) - assert object_ids[0] == "temperature" - assert object_ids[1] == "temperature" + assert extract_object_id_from_config(config1) == "temperature" + assert extract_object_id_from_config(config2) == "temperature" @pytest.mark.asyncio async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: """Test setup_entity with empty entity name.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -445,7 +437,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Should use friendly name assert object_id == "test_device" @@ -456,8 +448,6 @@ async def test_setup_entity_special_characters( ) -> None: """Test setup_entity with names containing special characters.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -466,7 +456,7 @@ async def test_setup_entity_special_characters( } await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Special characters should be sanitized assert object_id == "temperature_sensor_" @@ -476,8 +466,6 @@ async def test_setup_entity_special_characters( async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: """Test setup_entity sets icon correctly.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") config = { @@ -800,10 +788,9 @@ async def test_setup_entity_empty_name_with_device( # Check that set_device was called assert any("sensor1.set_device" in expr for expr in added_expressions) - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -815,7 +802,6 @@ async def test_setup_entity_empty_name_with_mac_suffix( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -831,10 +817,9 @@ async def test_setup_entity_empty_name_with_mac_suffix( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -847,7 +832,6 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( at runtime. In this case C++ will hash the empty friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -863,10 +847,9 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -878,7 +861,6 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from the device name. """ - added_expressions = setup_test_environment # No MAC suffix (either not set or False) CORE.config = {} @@ -896,10 +878,9 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 def test_register_string_overflow() -> None: @@ -976,7 +957,7 @@ async def test_setup_entity_direct_call(setup_test_environment: list[str]) -> No # Direct call mode: await setup_entity(var, config, "camera") await setup_entity(var, config, "camera") - # Should have called set_name + # Should have emitted configure_entity_ object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "my_camera" From 77f2c371b2b20b00d6e064bd5cbc618b2f1e22c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:26:34 -1000 Subject: [PATCH 035/101] [api] Single-pass protobuf encode for BLE proxy advertisements (#14575) --- esphome/components/api/api_pb2.cpp | 28 ++++++------ esphome/components/api/proto.cpp | 71 +++++++++++++++++++++++++++++ esphome/components/api/proto.h | 44 ++++++++---------- script/api_protobuf/api_protobuf.py | 17 +++---- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index b1176de539..1944aac6e8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -120,16 +120,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it); + buffer.encode_sub_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it); + buffer.encode_sub_message(21, it); } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area, false); + buffer.encode_optional_sub_message(22, this->area); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -920,13 +920,13 @@ uint32_t HomeassistantServiceMap::calculate_size() const { void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -1126,7 +1126,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, static_cast(this->supports_response)); } @@ -2133,7 +2133,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it); + buffer.encode_sub_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2264,7 +2264,7 @@ uint32_t BluetoothLERawAdvertisement::calculate_size() const { } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i]); + buffer.encode_sub_message(1, this->advertisements[i]); } } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { @@ -2343,7 +2343,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } @@ -2370,7 +2370,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } @@ -2392,7 +2392,7 @@ uint32_t BluetoothGATTService::calculate_size() const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } } uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { @@ -2673,7 +2673,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings, false); + buffer.encode_optional_sub_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } uint32_t VoiceAssistantRequest::calculate_size() const { @@ -2906,7 +2906,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it); + buffer.encode_sub_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index a252907fd7..1ca6b702ad 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,6 @@ #include "proto.h" #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -87,6 +88,76 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +// Single-pass encode for repeated submessage elements (non-template core). +// Writes field tag, reserves 1 byte for length varint, encodes the submessage body, +// then backpatches the actual length. For the common case (body < 128 bytes), this is +// just a single byte write with no memmove — all current repeated submessage types +// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take +// this fast path. +// +// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT +// characteristic with many descriptors). It is safe because calculate_size() already +// reserved space for the full multi-byte varint — the shift fills that reserved space: +// +// calculate_size() allocates per element: tag + varint_size(body) + body_size +// +// After encode, before memmove (1 byte reserved, body written): +// [tag][__][body ..... body][??] +// ^ ^-- unused byte (v2 space from calculate_size) +// len_pos +// +// After memmove(body_start+1, body_start, body_size): +// [tag][__][__][body ..... body] +// ^ ^-- body shifted forward, fills v2 space exactly +// len_pos +// +// After writing 2-byte varint at len_pos: +// [tag][v1][v2][body ..... body] +// ^-- pos_ = element end, within buffer +void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + this->encode_field_raw(field_id, 2); + // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes) + uint8_t *len_pos = this->pos_; + this->debug_check_bounds_(1); + this->pos_++; + uint8_t *body_start = this->pos_; + encode_fn(value, *this); + uint32_t body_size = static_cast(this->pos_ - body_start); + if (body_size < 128) [[likely]] { + // Common case: 1-byte varint, just backpatch + *len_pos = static_cast(body_size); + return; + } + // Compute extra bytes needed for varint beyond the 1 already reserved + uint8_t extra = ProtoSize::varint(body_size) - 1; + // Shift body forward to make room for the extra varint bytes + this->debug_check_bounds_(extra); + std::memmove(body_start + extra, body_start, body_size); + uint8_t *end = this->pos_ + extra; + // Write the full varint at len_pos + this->pos_ = len_pos; + this->encode_varint_raw(body_size); + this->pos_ = end; +} + +// Non-template core for encode_optional_sub_message. +void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + if (nested_size == 0) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(nested_size); +#ifdef ESPHOME_DEBUG_API + uint8_t *start = this->pos_; + encode_fn(value, *this); + if (static_cast(this->pos_ - start) != nested_size) + this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start); +#else + encode_fn(value, *this); +#endif +} + #ifdef ESPHOME_DEBUG_API void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 702208d9de..bbdd11b29d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -185,7 +185,7 @@ class ProtoVarInt { #endif }; -// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 +// Forward declarations for decode_to_message and related encoding helpers class ProtoDecodableMessage; class ProtoMessage; class ProtoSize; @@ -363,12 +363,18 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - /// Encode a nested message field (force=true for repeated, false for singular) - /// Templated so concrete message type is preserved for direct encode/calculate_size calls. - template void encode_message(uint32_t field_id, const T &value, bool force = true); - // Non-template core for encode_message — all buffer work happens here - void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); + /// Single-pass encode for repeated submessage elements. + /// Thin template wrapper; all buffer work is in the non-template core. + template void encode_sub_message(uint32_t field_id, const T &value); + /// Encode an optional singular submessage field — skips if empty. + /// Thin template wrapper; all buffer work is in the non-template core. + template void encode_optional_sub_message(uint32_t field_id, const T &value); + + // Non-template core for encode_sub_message — backpatch approach. + void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); + // Non-template core for encode_optional_sub_message. + void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)); std::vector *get_buffer() const { return buffer_; } protected: @@ -690,26 +696,14 @@ template void proto_encode_msg(const void *msg, ProtoWriteBuffer &bu static_cast(msg)->encode(buf); } -// Implementation of encode_message - must be after ProtoMessage is defined -template inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { - this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg, force); +// Thin template wrapper; delegates to non-template core in proto.cpp. +template inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { + this->encode_sub_message(field_id, &value, &proto_encode_msg); } -// Non-template core for encode_message -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { - if (msg_length_bytes == 0 && !force) - return; - this->encode_field_raw(field_id, 2); - this->encode_varint_raw(msg_length_bytes); -#ifdef ESPHOME_DEBUG_API - uint8_t *start = this->pos_; - encode_fn(value, *this); - if (static_cast(this->pos_ - start) != msg_length_bytes) - this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); -#else - encode_fn(value, *this); -#endif +// Thin template wrapper; delegates to non-template core. +template inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { + this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg); } // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 85352689e6..7ae7063a41 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -690,15 +690,12 @@ class MessageType(TypeInfo): @property def encode_func(self) -> str: - return "encode_message" + return "encode_optional_sub_message" @property def encode_content(self) -> str: - # Singular message fields pass force=false (skip empty messages) - # The default for encode_nested_message is force=true (for repeated fields) - return ( - f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" - ) + # Singular message fields skip encoding when empty + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property def decode_length(self) -> str: @@ -1322,9 +1319,9 @@ class FixedArrayRepeatedType(TypeInfo): """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property @@ -1650,9 +1647,9 @@ class RepeatedTypeInfo(TypeInfo): """Helper to generate encode call for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property From e7b8ec18f17b23709719c63e1d05a6b9ecfb0054 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:26:50 -1000 Subject: [PATCH 036/101] [api] Inline APIServer::is_connected() for common no-arg path (#14574) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 06816fe3e0..17d69405ad 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -582,11 +582,7 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected(bool state_subscription_only) const { - if (!state_subscription_only) { - return !this->clients_.empty(); - } - +bool APIServer::is_connected_with_state_subscription() const { for (const auto &client : this->clients_) { if (client->flags_.state_subscription) { return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e6c10d1595..e5f371d8a1 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -185,7 +185,8 @@ class APIServer : public Component, void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector *timings); #endif - bool is_connected(bool state_subscription_only = false) const; + bool is_connected() const { return !this->clients_.empty(); } + bool is_connected_with_state_subscription() const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -323,7 +324,10 @@ template class APIConnectedCondition : public Condition { TEMPLATABLE_VALUE(bool, state_subscription_only) public: bool check(const Ts &...x) override { - return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + if (this->state_subscription_only_.value(x...)) { + return global_api_server->is_connected_with_state_subscription(); + } + return global_api_server->is_connected(); } }; From a0cd35c5fc2b8b1e9ab0db69fd70b90c95e8dbad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:27:08 -1000 Subject: [PATCH 037/101] [core] Inline status_clear_warning/error fast path (#14571) --- esphome/core/component.cpp | 8 ++------ esphome/core/component.h | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a9ff3ec1eb..2879d4b5ab 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -422,15 +422,11 @@ void Component::status_set_error(const LogString *message) { store_component_error_message(this, LOG_STR_ARG(message), true); } } -void Component::status_clear_warning() { - if ((this->component_state_ & STATUS_LED_WARNING) == 0) - return; +void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } -void Component::status_clear_error() { - if ((this->component_state_ & STATUS_LED_ERROR) == 0) - return; +void Component::status_clear_error_slow_path_() { this->component_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 59222dc4f4..7266f57e15 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -251,9 +251,17 @@ class Component { void status_set_error(const char *message); void status_set_error(const LogString *message); - void status_clear_warning(); + void status_clear_warning() { + if ((this->component_state_ & STATUS_LED_WARNING) == 0) + return; + this->status_clear_warning_slow_path_(); + } - void status_clear_error(); + void status_clear_error() { + if ((this->component_state_ & STATUS_LED_ERROR) == 0) + return; + this->status_clear_error_slow_path_(); + } /** Set warning status flag and automatically clear it after a timeout. * @@ -505,6 +513,9 @@ class Component { bool cancel_defer(const char *name); // NOLINT bool cancel_defer(uint32_t id); // NOLINT + void status_clear_warning_slow_path_(); + void status_clear_error_slow_path_(); + // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) From 3f700bac1cebf7eb6ff3b20a87d8c8af5cb9fc41 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Sat, 7 Mar 2026 19:50:44 +0100 Subject: [PATCH 038/101] [component] Fix components for compatibility with stricter compilers (#14545) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/ethernet/esp_eth_phy_jl1101.c | 2 +- esphome/components/ethernet/ethernet_component.cpp | 4 ++-- esphome/components/heatpumpir/climate.py | 1 + .../http_request/update/http_request_update.cpp | 3 ++- esphome/components/mlx90393/sensor_mlx90393.cpp | 2 +- esphome/components/whirlpool/whirlpool.cpp | 7 +++++++ esphome/components/whirlpool/whirlpool.h | 12 ++++++------ 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index bbe972b9f3..7fa5370072 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -451,7 +451,7 @@ void ESP32BLE::loop() { ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT { - esp_ble_gap_cb_param_t *param; + esp_ble_gap_cb_param_t *param = NULL; // clang-format off switch (gap_event) { // All three scan complete events have the same structure with just status diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 5e73e99101..a19f7aa6b0 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -27,7 +27,7 @@ #include "esp_rom_sys.h" #include "esp_idf_version.h" -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0bc67c8b03..d6b0d40cd9 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -209,7 +209,7 @@ void EthernetComponent::setup() { break; } #endif -#ifdef USE_ETHERNET_JL1101 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) case ETHERNET_TYPE_JL1101: { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; @@ -374,7 +374,7 @@ void EthernetComponent::dump_config() { eth_type = "IP101"; break; #endif -#ifdef USE_ETHERNET_JL1101 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) case ETHERNET_TYPE_JL1101: eth_type = "JL1101"; break; diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 0d760938d0..7743da77ab 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,6 +125,7 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.40") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 1900f69a69..c40590af95 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -128,7 +128,8 @@ void HttpRequestUpdate::update_task(void *params) { this_update->update_info_.title = root[ESPHOME_F("name")].as(); this_update->update_info_.latest_version = root[ESPHOME_F("version")].as(); - for (auto build : root[ESPHOME_F("builds")].as()) { + auto builds_array = root[ESPHOME_F("builds")].as(); + for (auto build : builds_array) { if (!build[ESPHOME_F("chipFamily")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index ee52f9b9ab..01084e50df 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -160,7 +160,7 @@ bool MLX90393Cls::verify_setting_(MLX90393Setting which) { uint8_t read_value = 0xFF; uint8_t expected_value = 0xFF; uint8_t read_status = -1; - char read_back_str[25] = {0}; + char read_back_str[33] = {0}; switch (which) { case MLX90393_GAIN_SEL: { diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index e9f602e97f..86209cb7a6 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -32,6 +32,13 @@ const uint8_t WHIRLPOOL_SWING_MASK = 128; const uint8_t WHIRLPOOL_POWER = 0x04; +WhirlpoolClimate::WhirlpoolClimate() + : climate_ir::ClimateIR( + WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}), + model_(MODEL_DG11J1_3A) {} + void WhirlpoolClimate::transmit_state() { this->last_transmit_time_ = millis(); // setting the time of the last transmission. uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 992b2a7adf..ada5a36de9 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -19,11 +19,7 @@ const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; class WhirlpoolClimate : public climate_ir::ClimateIR { public: - WhirlpoolClimate() - : climate_ir::ClimateIR(temperature_min_(), temperature_max_(), 1.0f, true, true, - {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH}, - {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + WhirlpoolClimate(); void setup() override { climate_ir::ClimateIR::setup(); @@ -37,7 +33,11 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { climate_ir::ClimateIR::control(call); } - void set_model(Model model) { this->model_ = model; } + void set_model(Model model) { + this->model_ = model; + this->minimum_temperature_ = temperature_min_(); + this->maximum_temperature_ = temperature_max_(); + } // used to track when to send the power toggle command bool powered_on_assumed; From f2dfb5e1dcfd7bd47677f8d0c75b41bbd0e25bb7 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Sun, 8 Mar 2026 00:16:12 +0100 Subject: [PATCH 039/101] [uart][usb_uart] Add debug_prefix option to distinguish multiple defined uarts in log (#14525) --- esphome/components/uart/__init__.py | 13 ++++- esphome/components/uart/uart_debugger.cpp | 18 +++--- esphome/components/uart/uart_debugger.h | 17 ++++-- esphome/components/usb_uart/__init__.py | 5 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- esphome/components/usb_uart/usb_uart.h | 3 + tests/components/uart/test.esp32-c3-idf.yaml | 16 +++++- tests/components/uart/test.esp32-idf.yaml | 60 +++++++++++++++----- tests/components/uart/test.esp8266-ard.yaml | 24 +++++--- tests/components/uart/test.host.yaml | 2 +- tests/components/uart/test.rp2040-ard.yaml | 16 +++++- tests/components/usb_uart/common.yaml | 8 +++ 12 files changed, 138 insertions(+), 48 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 3bc4263b31..1b976b73a9 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -203,8 +203,10 @@ UART_DIRECTIONS = { # round numbers. AFTER_DEFAULTS = {CONF_BYTES: 150, CONF_TIMEOUT: "100ms"} +CONF_DEBUG_PREFIX = "debug_prefix" + # By default, log in hex format when no specific sequence is provided. -DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':');" +DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':', debug_prefix);" DEFAULT_SEQUENCE = [{CONF_LAMBDA: make_data_base(DEFAULT_DEBUG_OUTPUT)}] @@ -242,6 +244,7 @@ DEBUG_SCHEMA = cv.Schema( ): automation.validate_automation(), cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver), + cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, } ) @@ -283,7 +286,11 @@ async def debug_to_code(config, parent): for action in config[CONF_SEQUENCE]: await automation.build_automation( trigger, - [(UARTDirection, "direction"), (cg.std_vector.template(cg.uint8), "bytes")], + [ + (UARTDirection, "direction"), + (cg.std_vector.template(cg.uint8), "bytes"), + (cg.StringRef, "debug_prefix"), + ], action, ) cg.add(trigger.set_direction(config[CONF_DIRECTION])) @@ -299,6 +306,8 @@ async def debug_to_code(config, parent): if config[CONF_DUMMY_RECEIVER]: dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) await cg.register_component(dummy, {}) + if debug_prefix := config[CONF_DEBUG_PREFIX]: + cg.add(trigger.set_debug_prefix(debug_prefix)) cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index 5490154d01..e0be599d8d 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -74,7 +74,7 @@ bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); } void UARTDebugger::fire_trigger_() { this->is_triggering_ = true; - trigger(this->last_direction_, this->bytes_); + trigger(this->last_direction_, this->bytes_, this->debug_prefix_); this->bytes_.clear(); this->is_triggering_ = false; } @@ -94,7 +94,7 @@ void UARTDummyReceiver::loop() { // TCP connection(s). Without these delays, debug log lines could go // missing when UART devices block the main loop for too long. -void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uint8_t separator) { +void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uint8_t separator, StringRef prefix) { std::string res; if (direction == UART_DIRECTION_RX) { res += "<<< "; @@ -110,11 +110,11 @@ void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uin buf_append_printf(buf, sizeof(buf), 0, "%02X", bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_string(UARTDirection direction, std::vector bytes) { +void UARTDebug::log_string(UARTDirection direction, std::vector bytes, StringRef prefix) { std::string res; if (direction == UART_DIRECTION_RX) { res += "<<< \""; @@ -154,11 +154,11 @@ void UARTDebug::log_string(UARTDirection direction, std::vector bytes) } } res += '"'; - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uint8_t separator) { +void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uint8_t separator, StringRef prefix) { std::string res; size_t len = bytes.size(); if (direction == UART_DIRECTION_RX) { @@ -174,11 +174,11 @@ void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uin buf_append_printf(buf, sizeof(buf), 0, "%u", bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, uint8_t separator) { +void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, uint8_t separator, StringRef prefix) { std::string res; size_t len = bytes.size(); if (direction == UART_DIRECTION_RX) { @@ -194,7 +194,7 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, buf_append_printf(buf, sizeof(buf), 0, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index df87655962..da33bea70c 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -5,6 +5,7 @@ #include #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/string_ref.h" #include "uart.h" #include "uart_component.h" @@ -17,7 +18,7 @@ namespace esphome::uart { /// 'appropriate time' means exactly, is determined by a number of /// configurable constraints. E.g. when a given number of bytes is gathered /// and/or when no more data has been seen for a given time interval. -class UARTDebugger : public Component, public Trigger> { +class UARTDebugger : public Component, public Trigger, StringRef> { public: explicit UARTDebugger(UARTComponent *parent); void loop() override; @@ -41,6 +42,8 @@ class UARTDebugger : public Component, public Triggerafter_delimiter_.push_back(byte); } + void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } + protected: UARTDirection for_direction_; UARTDirection last_direction_{}; @@ -51,6 +54,7 @@ class UARTDebugger : public Component, public Trigger after_delimiter_{}; size_t after_delimiter_pos_{}; bool is_triggering_{false}; + StringRef debug_prefix_{}; bool is_my_direction_(UARTDirection direction); bool is_recursive_(); @@ -81,18 +85,21 @@ class UARTDebug { public: /// Log the bytes as hex values, separated by the provided separator /// character. - static void log_hex(UARTDirection direction, std::vector bytes, uint8_t separator); + static void log_hex(UARTDirection direction, std::vector bytes, uint8_t separator, + StringRef prefix = StringRef()); /// Log the bytes as string values, escaping unprintable characters. - static void log_string(UARTDirection direction, std::vector bytes); + static void log_string(UARTDirection direction, std::vector bytes, StringRef prefix = StringRef()); /// Log the bytes as integer values, separated by the provided separator /// character. - static void log_int(UARTDirection direction, std::vector bytes, uint8_t separator); + static void log_int(UARTDirection direction, std::vector bytes, uint8_t separator, + StringRef prefix = StringRef()); /// Log the bytes as ' ()' values, separated by the provided /// separator. - static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator); + static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator, + StringRef prefix = StringRef()); }; } // namespace esphome::uart diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index f0ee53d028..eedf590eca 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS -from esphome.components.uart import UARTComponent +from esphome.components.uart import CONF_DEBUG_PREFIX, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( @@ -90,6 +90,7 @@ def channel_schema(channels, baud_rate_required): ), cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, + cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, } ) ), @@ -129,6 +130,8 @@ async def to_code(config): cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) cg.add(chvar.set_debug(channel[CONF_DEBUG])) + if channel[CONF_DEBUG_PREFIX]: + cg.add(chvar.set_debug_prefix(channel[CONF_DEBUG_PREFIX])) cg.add(var.add_channel(chvar)) if channel[CONF_DEBUG]: cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index e20bbd02db..a1f8738491 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -142,7 +142,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { size_t n = std::min(len - off, BATCH); memcpy(buf, ">>> ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, data + off, n, ','); - ESP_LOGD(TAG, "%s", buf); + ESP_LOGD(TAG, "%s%s", this->debug_prefix_.c_str(), buf); } } #endif @@ -219,7 +219,7 @@ void USBUartComponent::loop() { char buf[4 + format_hex_pretty_size(UsbDataChunk::MAX_CHUNK_SIZE)]; // "<<< " + hex memcpy(buf, "<<< ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, chunk->data, chunk->length, ','); - ESP_LOGD(TAG, "%s", buf); + ESP_LOGD(TAG, "%s%s", channel->debug_prefix_.c_str(), buf); } #endif diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 0d471e46f6..7290f8a958 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -3,6 +3,7 @@ #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/uart/uart_component.h" #include "esphome/components/usb_host/usb_host.h" #include "esphome/core/lock_free_queue.h" @@ -114,6 +115,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedparity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } + void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } /// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Called from USBUartComponent::loop() in the main loop context. @@ -138,6 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented(cmd.begin(), cmd.end()); + - uart.write: + id: uart_id + data: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector(cmd.begin(), cmd.end()); event: - platform: uart - uart_id: uart_uart + uart_id: uart_id name: "UART Event" event_types: - "string_event_A": "*A#" diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index c2670b289a..4c9d6fd736 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 baud_rate: 9600 @@ -13,15 +17,21 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 14 + rx_pin: 12 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " switch: - platform: uart name: "UART Switch Array" - uart_id: uart_uart + uart_id: uart_id data: [0x01, 0x02, 0x03] - platform: uart name: "UART Switch Dual" - uart_id: uart_uart + uart_id: uart_id data: turn_on: [0xA0, 0xA1] turn_off: [0xB0, 0xB1] @@ -29,12 +39,12 @@ switch: button: - platform: uart name: "UART Button" - uart_id: uart_uart + uart_id: uart_id data: [0xFF, 0xEE] event: - platform: uart - uart_id: uart_uart + uart_id: uart_id name: "UART Event" event_types: - "string_event_A": "*A#" diff --git a/tests/components/uart/test.host.yaml b/tests/components/uart/test.host.yaml index 63f0ade084..bc64245c7b 100644 --- a/tests/components/uart/test.host.yaml +++ b/tests/components/uart/test.host.yaml @@ -5,7 +5,7 @@ esphome: - uart.write: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id port: "/dev/ttyS0" baud_rate: 9600 data_bits: 8 diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml index 09178f1663..5eb2b533ea 100644 --- a/tests/components/uart/test.rp2040-ard.yaml +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 baud_rate: 9600 @@ -13,3 +17,9 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 8 + rx_pin: 9 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 474c3f5c8d..5869b9468b 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -34,3 +34,11 @@ usb_uart: - id: channel_4_1 debug: true dummy_receiver: true + debug_prefix: "[ESP_JTAG] " + - id: uart_5 + type: cp210x + channels: + - id: channel_5_1 + baud_rate: 9600 + debug: true + debug_prefix: "[CP210X] " From 545395a6f0d2180c648998f0ded5302f01df89cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 13:16:19 -1000 Subject: [PATCH 040/101] [ci] Add RP2350 to PR template test environment (#14599) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 965b186c31..72013e411e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,7 @@ - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 -- [ ] RP2040 +- [ ] RP2040/RP2350 - [ ] BK72xx - [ ] RTL87xx - [ ] LN882x From 888f3d804b9550036c20153218e4286d38c88775 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 13:22:50 -1000 Subject: [PATCH 041/101] [ld2420] Add integration tests with mock UART (#14471) --- .../uart_mock/uart_mock.cpp | 8 +- .../fixtures/uart_mock_ld2420.yaml | 187 ++++++++++++ .../fixtures/uart_mock_ld2420_simple.yaml | 141 +++++++++ tests/integration/test_uart_mock_ld2420.py | 273 ++++++++++++++++++ 4 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2420.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2420_simple.yaml create mode 100644 tests/integration/test_uart_mock_ld2420.py diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index affcc8d908..e8c07d632d 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -104,12 +104,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { } #endif - if (this->scenario_active_) { - this->try_match_response_(); - } + // Responses are always active - they are request-response pairs triggered by + // component TX, not timed injections. No race condition with test subscription. + this->try_match_response_(); // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. - if (this->tx_hook_ && this->scenario_active_) { + if (this->tx_hook_) { std::vector buf(data, data + len); this->tx_hook_(buf); } diff --git a/tests/integration/fixtures/uart_mock_ld2420.yaml b/tests/integration/fixtures/uart_mock_ld2420.yaml new file mode 100644 index 0000000000..5380b81071 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2420.yaml @@ -0,0 +1,187 @@ +esphome: + name: uart-mock-ld2420-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + auto_start: false + responses: + # Version-specific response: match the complete firmware version command TX. + # CMD_READ_VERSION (0x0000) TX = FD FC FB FA 02 00 00 00 04 03 02 01 + # Returns "v2.0.0" → get_firmware_int = 200 >= 154 → energy mode + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 0C 00 = length 12 + # [6] 00 = cmd (CMD_READ_VERSION) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10] 06 = ver_len = 6 + # [11] 00 = padding + # [12-17] "v2.0.0" = version string + # [18-21] 04 03 02 01 = footer + - expect_tx: + [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x0C, 0x00, + 0x00, 0x01, + 0x00, 0x00, + 0x06, 0x00, + 0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30, + 0x04, 0x03, 0x02, 0x01, + ] + + # Catch-all response: match any command footer (04 03 02 01). + # Returns a generic ACK with cmd=0xFF (CMD_ENABLE_CONF case in switch). + # All commands get unblocked via cmd_reply_.ack = true. + # Data fields stay zeroed (min_gate=0, max_gate=0, timeout=0, thresholds=0). + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 04 00 = length 4 + # [6] FF = cmd (handled as CMD_ENABLE_CONF) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10-13] 04 03 02 01 = footer + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid LD2420 energy mode data frame - happy path + # Buffer is clean (buffer_pos_=0). This frame should parse correctly. + # Presence: 1 (target detected), Distance: 100cm, Gate energies: all 0 + # + # Energy frame layout (45 bytes): + # [0-3] F4 F3 F2 F1 = energy frame header + # [4-5] 23 00 = length 35 (1+2+32) + # [6] 01 = presence (1 = target) + # [7-8] 64 00 = distance 100 (uint16_t LE) + # [9-40] 00 00 x16 = 16 gate energies (uint16_t LE each) + # [41-44] F8 F7 F6 F5 = energy frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2420's readline_ does NOT check frame headers at position 0 (unlike LD2412), + # so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated energy frame WITH footer (13 bytes) + # This tests PR #14458 bug #3: missing length validation in handle_energy_mode_. + # The 7 garbage bytes from Phase 2 are still in the buffer (buffer_pos_=7). + # These 13 bytes are appended at positions 7-19 (buffer_pos_=20). + # Energy footer at positions 16-19 triggers handle_energy_mode_(buffer, 20). + # + # Pre-fix: handle_energy_mode_ reads 32 bytes of gate energy from buffer[9:40], + # which is past the 20 actual bytes. Reads uninitialized data. + # No "Energy frame too short" warning exists. + # Post-fix: len=20 < 41 → logs "Energy frame too short: 20 bytes", returns early. + # + # Frame: header + length + presence + distance + footer (no gate data) + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x64, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 4 (t=600ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50) + # After Phase 3, buffer_pos_=0 (reset after energy footer detection). + # 49 bytes fill positions 0-48 (buffer_pos_=49), 50th byte triggers overflow. + # Logs "Max command length exceeded; ignoring", buffer_pos_=0. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=1500ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Presence: 1 (target), Distance: 50cm + # Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle. + - delay: 900ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x32, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +button: + - platform: template + name: "Start Scenario" + on_press: + - lambda: 'id(mock_uart).start_scenario();' + +ld2420: + id: ld2420_dev + uart_id: mock_uart + +sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms diff --git a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml new file mode 100644 index 0000000000..2ceca5d35d --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml @@ -0,0 +1,141 @@ +esphome: + name: uart-mock-ld2420-simple-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + auto_start: false + responses: + # Catch-all response only (no version-specific response). + # Without a version response, firmware_ver_ stays at default "v0.0.0". + # get_firmware_int("v0.0.0") = 0 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE). + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid simple mode text frame - happy path + # "ON Range 0100\r\n" → presence=true, distance=100 + # Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_. + - delay: 100ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x31, 0x30, 0x30, + 0x0D, 0x0A, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2420's readline_ stores all bytes regardless of header. buffer_pos_ = 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=500ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50) + # buffer_pos_ starts at 7 (from Phase 2 garbage). + # Positions 7-48 fill (42 bytes), byte 43 triggers overflow (buffer_pos_=49). + # After overflow: buffer_pos_=0, remaining 7 bytes fill positions 0-6. + # Final buffer_pos_ = 7. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 4 (t=1400ms): Recovery after overflow + # buffer_pos_ = 7 (from overflow remainder). These 15 bytes fill positions 7-21. + # At position 21 (0x0A), \r\n detected → handle_simple_mode_(buffer, 22). + # Parser skips 0xFF bytes at positions 0-6, finds "ON" at positions 7-8, + # parses digits "0050" → distance=50. + # Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle. + - delay: 900ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x35, 0x30, + 0x0D, 0x0A, + ] + + # Phase 5 (t=2500ms): 16-digit distance - tests PR #14458 bug #1 + # "ON Range 0000000000000000\r\n" has 16 digit characters. + # handle_simple_mode_ outbuf is 16 bytes, can hold 15 digits (index 0-14). + # + # Pre-fix: At the 16th digit, index=15, (index < bufsize-1) is false. + # The digit branch doesn't increment pos. The else branch is skipped. + # pos stays at the 16th digit FOREVER → INFINITE LOOP. + # The binary hangs, no more state updates, test times out. + # Post-fix: pos always increments (moved outside digit branch). + # 16th digit skipped, loop continues to \r\n. distance=0. + - delay: 1100ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x0D, 0x0A, + ] + + # Phase 6 (t=3700ms): Post-bug-trigger recovery + # If Phase 5 didn't hang, this frame should parse correctly. + # "ON Range 0025\r\n" → distance=25 + # Delay=1200ms ensures >1000ms gap from Phase 5 for throttle. + - delay: 1200ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x32, 0x35, + 0x0D, 0x0A, + ] + +button: + - platform: template + name: "Start Scenario" + on_press: + - lambda: 'id(mock_uart).start_scenario();' + +ld2420: + id: ld2420_dev + uart_id: mock_uart + +sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms diff --git a/tests/integration/test_uart_mock_ld2420.py b/tests/integration/test_uart_mock_ld2420.py new file mode 100644 index 0000000000..ae28da4d3e --- /dev/null +++ b/tests/integration/test_uart_mock_ld2420.py @@ -0,0 +1,273 @@ +"""Integration test for LD2420 component with mock UART. + +Tests: +test_uart_mock_ld2420 (energy mode): + 1. Happy path - valid energy frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated energy frame - triggers "Energy frame too short" warning (PR #14458 bug #3) + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2420 sends expected setup commands + +test_uart_mock_ld2420_simple (simple mode): + 1. Happy path - valid simple mode text frame publishes correct values + 2. Garbage resilience + 3. Buffer overflow recovery + 4. 16-digit distance triggers infinite loop pre-fix (PR #14458 bug #1) + 5. Post-bug-trigger recovery proves the parser survived +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2420( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2420 energy mode: happy path, truncated frame, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track "Energy frame too short" warning (PR #14458 bug #3 fix) + # This message ONLY exists after the fix. Pre-fix, handle_energy_mode_ + # silently reads past the buffer without any warning. + truncated_frame_warning_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + if "Energy frame too short" in line and not truncated_frame_warning_seen.done(): + truncated_frame_warning_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + collector = SensorStateCollector( + sensor_names=["moving_distance"], + binary_sensor_names=["has_target"], + ) + + # Signal when we see recovery frame values + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values: moving=100, has_target=true + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.binary_states["has_target"][0] is True + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify truncated frame warning was logged (PR #14458 bug #3) + # This assertion FAILS before PR #14458 because the length check + # and warning message did not exist. + assert truncated_frame_warning_seen.done(), ( + "Expected 'Energy frame too short' warning in logs. " + "This indicates PR #14458 fix for handle_energy_mode_ length " + "validation is missing." + ) + + # Verify LD2420 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD.FC.FB.FA" in tx_data or "FD:FC:FB:FA" in tx_data, ( + "Expected LD2420 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04.03.02.01" in tx_data or "04:03:02:01" in tx_data, ( + "Expected LD2420 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + recovery_values = [ + v + for v in collector.sensor_states["moving_distance"] + if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {collector.sensor_states['moving_distance']}" + ) + + +@pytest.mark.asyncio +async def test_uart_mock_ld2420_simple( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2420 simple mode: happy path, overflow, and 16-digit bug trigger.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + + collector = SensorStateCollector( + sensor_names=["moving_distance"], + binary_sensor_names=["has_target"], + ) + + # Signal for recovery frames + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) + post_bug_received = collector.add_waiter( + lambda: pytest.approx(25.0) in collector.sensor_states["moving_distance"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1: simple mode "ON Range 0100\r\n" → distance=100, presence=true + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.binary_states["has_target"][0] is True + + # Wait for Phase 4 recovery (distance=50) after overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" moving_distance: {collector.sensor_states['moving_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Wait for Phase 6: distance=25 (post-16-digit-bug recovery) + # This assertion FAILS before PR #14458 because the 16-digit frame + # in Phase 5 causes an infinite loop in handle_simple_mode_ pre-fix. + # The binary hangs, Phase 6 never fires, and this wait times out. + try: + await asyncio.wait_for(post_bug_received, timeout=8.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for post-bug recovery (distance=25). " + f"This likely means Phase 5 (16-digit frame) caused an infinite " + f"loop in handle_simple_mode_, indicating PR #14458 bug #1 fix " + f"is missing.\n" + f" moving_distance values: {collector.sensor_states['moving_distance']}" + ) + + # Verify post-bug value + post_bug_values = [ + v + for v in collector.sensor_states["moving_distance"] + if v == pytest.approx(25.0) + ] + assert len(post_bug_values) >= 1, ( + f"Expected moving_distance=25 after 16-digit test, " + f"got: {collector.sensor_states['moving_distance']}" + ) From ea7cfffddaf60c5da30bf9dfa976fcf044477c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:40:13 +0000 Subject: [PATCH 042/101] Bump aioesphomeapi from 44.3.1 to 44.4.0 (#14609) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8e875eba62..a1300a67f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.3.1 +aioesphomeapi==44.4.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 66919ef969574f8e6553d2146089c41ee9cecd27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 18:33:54 -1000 Subject: [PATCH 043/101] [i2s_audio] Include legacy driver IDF component when use_legacy is set (#14613) --- esphome/components/i2s_audio/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 1cd2e97a5e..65b09b93f6 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -281,6 +281,8 @@ async def to_code(config): if use_legacy(): cg.add_define("USE_I2S_LEGACY") + # Legacy I2S API lives in the "driver" shim component (driver/i2s.h) + include_builtin_idf_component("driver") # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) From d55fe9a34b50400347a7d57f6679dbbb514ea8ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 18:34:35 -1000 Subject: [PATCH 044/101] [api] Fix value-initialization of DeviceInfoResponse (#14615) Co-authored-by: Keith Burzinski --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bd3de02895..b9b33ddcc2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1571,7 +1571,7 @@ bool APIConnection::send_ping_response_() { } bool APIConnection::send_device_info_response_() { - DeviceInfoResponse resp{}; + DeviceInfoResponse resp; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); #ifdef USE_AREAS From 9fea8fe01b4c976caced70881cb986729768c305 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 7 Mar 2026 23:50:36 -0500 Subject: [PATCH 045/101] [vbus][rf_bridge][sensirion_common] Add buffer size guards (#14597) Co-authored-by: Claude Opus 4.6 --- esphome/components/rf_bridge/rf_bridge.cpp | 3 +++ esphome/components/rf_bridge/rf_bridge.h | 1 + esphome/components/sensirion_common/i2c_sensirion.cpp | 2 +- esphome/components/vbus/vbus.cpp | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 700e2ba162..5ca629c12b 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -145,6 +145,9 @@ void RFBridgeComponent::loop() { } avail -= to_read; for (size_t i = 0; i < to_read; i++) { + if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) { + this->rx_buffer_.clear(); + } if (this->parse_bridge_byte_(buf[i])) { ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); this->last_bridge_byte_ = now; diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index d2f75c819d..c93b636c38 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -30,6 +30,7 @@ static const uint8_t RF_CODE_RFIN_BUCKET = 0xB1; static const uint8_t RF_CODE_BEEP = 0xC0; static const uint8_t RF_CODE_STOP = 0x55; static const uint8_t RF_DEBOUNCE = 200; +static const size_t MAX_RX_BUFFER_SIZE = 512; struct RFBridgeData { uint16_t sync; diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 26702c148c..0279e08b9f 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -12,7 +12,7 @@ static const char *const TAG = "sensirion_i2c"; static const size_t BUFFER_STACK_SIZE = 16; bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { - const uint8_t num_bytes = len * 3; + const size_t num_bytes = len * 3; uint8_t buf[num_bytes]; this->last_error_ = this->read(buf, num_bytes); diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index 8616da010d..c6786ee31e 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -87,6 +87,9 @@ void VBus::loop() { this->state_ = 0; ESP_LOGD(TAG, "P1 empty message"); } + } else if (this->buffer_.size() > 15) { + ESP_LOGW(TAG, "Unknown protocol 0x%02x, discarding", this->protocol_); + this->state_ = 0; } continue; } From be6c3c52ac89534f00e54dfa26974402d813a4bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 18:59:13 -1000 Subject: [PATCH 046/101] [api] Add force proto field option to skip zero checks on hot path (#14610) Co-authored-by: Claude Opus 4.6 --- esphome/components/api/api.proto | 6 ++-- esphome/components/api/api_options.proto | 6 ++++ esphome/components/api/api_pb2.cpp | 14 ++++----- script/api_protobuf/api_protobuf.py | 38 ++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 618fd1b83c..257a7aaf82 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1604,11 +1604,11 @@ message BluetoothLEAdvertisementResponse { } message BluetoothLERawAdvertisement { - uint64 address = 1; - sint32 rssi = 2; + uint64 address = 1 [(force) = true]; + sint32 rssi = 2 [(force) = true]; uint32 address_type = 3; - bytes data = 4 [(fixed_array_size) = 62]; + bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } message BluetoothLERawAdvertisementsResponse { diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index a863f2c7a8..02600f0977 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions { // - uint16_t _length_{0}; // - uint16_t _count_{0}; optional bool packed_buffer = 50015 [default=false]; + + // force: Always encode this field, even when its value equals the proto3 default. + // Skips the zero/empty check in calculate_size() and encode(), using the _force + // variant of the calc_ method. Use on fields that are almost always non-default + // to eliminate dead branches on hot paths. + optional bool force = 50016 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1944aac6e8..38ebfb9464 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -139,7 +139,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - buffer.encode_message(25, it); + buffer.encode_sub_message(25, it); } #endif } @@ -2249,17 +2249,17 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_sint32(2, this->rssi); + buffer.encode_uint64(1, this->address, true); + buffer.encode_sint32(2, this->rssi, true); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len); + buffer.encode_bytes(4, this->data, this->data_len, true); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint64(1, this->address); - size += ProtoSize::calc_sint32(1, this->rssi); + size += ProtoSize::calc_uint64_force(1, this->address); + size += ProtoSize::calc_sint32_force(1, this->rssi); size += ProtoSize::calc_uint32(1, this->address_type); - size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_length_force(1, this->data_len); return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7ae7063a41..206f8f558b 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -151,6 +151,11 @@ class TypeInfo(ABC): """Check if the field is repeated.""" return self._field.label == FieldDescriptorProto.LABEL_REPEATED + @property + def force(self) -> bool: + """Check if this field should always be encoded (skip zero/empty check).""" + return get_field_opt(self._field, pb.force, False) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -218,6 +223,8 @@ class TypeInfo(ABC): @property def encode_content(self) -> str: + if self.force: + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" encode_func = None @@ -413,6 +420,8 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -437,6 +446,8 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -521,6 +532,8 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -545,6 +558,8 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -607,6 +622,8 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: # Use the StringRef + if self.force: + return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" def dump(self, name): @@ -694,7 +711,7 @@ class MessageType(TypeInfo): @property def encode_content(self) -> str: - # Singular message fields skip encoding when empty + # encode_sub_message always encodes (uses backpatch), no force needed return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property @@ -771,6 +788,8 @@ class BytesType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: @@ -876,6 +895,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @property @@ -923,6 +944,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if self.force: + return ( + f"buffer.encode_string({self.number}, this->{self.field_name}, true);" + ) return f"buffer.encode_string({self.number}, this->{self.field_name});" @property @@ -1086,6 +1111,8 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" def dump(self, name: str) -> str: @@ -1159,6 +1186,8 @@ class EnumType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}), true);" return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" def dump(self, name: str) -> str: @@ -1192,6 +1221,8 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -1216,6 +1247,8 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -2134,7 +2167,8 @@ def build_message_type( encode.extend(wrap_with_ifdef(ti.encode_content, field_ifdef)) size_calc.extend( wrap_with_ifdef( - ti.get_size_calculation(f"this->{ti.field_name}"), field_ifdef + ti.get_size_calculation(f"this->{ti.field_name}", ti.force), + field_ifdef, ) ) From 5e842a8b207230be2fd8db09baf5098f62767a21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 7 Mar 2026 23:23:13 -0600 Subject: [PATCH 047/101] [uart] Return `flush` result, expose timeout via config (#14608) Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ble_nus/ble_nus.cpp | 5 +++-- esphome/components/ble_nus/ble_nus.h | 2 +- esphome/components/uart/__init__.py | 6 ++++++ esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.h | 16 +++++++++++++++- .../components/uart/uart_component_esp8266.cpp | 3 ++- esphome/components/uart/uart_component_esp8266.h | 2 +- .../components/uart/uart_component_esp_idf.cpp | 13 +++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 5 ++++- esphome/components/uart/uart_component_host.cpp | 5 +++-- esphome/components/uart/uart_component_host.h | 2 +- .../components/uart/uart_component_libretiny.cpp | 3 ++- .../components/uart/uart_component_libretiny.h | 2 +- .../components/uart/uart_component_rp2040.cpp | 3 ++- esphome/components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 11 ++++++++--- esphome/components/usb_uart/usb_uart.cpp | 5 ++++- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 5 +++-- esphome/components/weikai/weikai.h | 2 +- tests/components/ld2450/common.h | 2 +- tests/components/uart/common.h | 2 +- .../external_components/uart_mock/uart_mock.cpp | 3 ++- .../external_components/uart_mock/uart_mock.h | 2 +- 25 files changed, 77 insertions(+), 30 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d1710100a0..e38dc99802 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -100,16 +100,17 @@ size_t BLENUS::available() { #endif } -void BLENUS::flush() { +uart::FlushResult BLENUS::flush() { constexpr uint32_t timeout_5sec = 5000; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_5sec) { ESP_LOGW(TAG, "Flush timeout"); - return; + return uart::FlushResult::TIMEOUT; } delay(1); } + return uart::FlushResult::SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index 67e9ae9f97..b482c240e5 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 1b976b73a9..2cb6eac050 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -183,6 +183,7 @@ UART_PARITY_OPTIONS = { "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, } +CONF_FLUSH_TIMEOUT = "flush_timeout" CONF_RX_FULL_THRESHOLD = "rx_full_threshold" CONF_RX_TIMEOUT = "rx_timeout" @@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_RX_TIMEOUT, esp32=2): cv.All( cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=0, max=92) ), + cv.Optional(CONF_FLUSH_TIMEOUT): cv.All( + cv.only_on_esp32, cv.positive_time_period_milliseconds + ), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -345,6 +349,8 @@ async def to_code(config): ) cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) + if CONF_FLUSH_TIMEOUT in config: + cg.add(var.set_flush_timeout(config[CONF_FLUSH_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index bb91e5cd7c..2c4fb34c9a 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - void flush() { this->parent_->flush(); } + FlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 078ce64b30..3d7e71b89f 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -29,6 +29,14 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); +/// Result of a flush() call. +enum class FlushResult { + SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + TIMEOUT, ///< Confirmed: timed out before TX completed. + FAILED, ///< Confirmed: driver or hardware error. + ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +}; + class UARTComponent { public: // Writes an array of bytes to the UART bus. @@ -72,7 +80,13 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - virtual void flush() = 0; + // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual FlushResult flush() = 0; + + // Sets the maximum time to wait for TX to drain during flush(). + // Only meaningful on ESP32 (IDF). Other platforms ignore this value. + // @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely. + virtual void set_flush_timeout(uint32_t flush_timeout_ms) {} // Sets the TX (transmit) pin for the UART bus. // @param tx_pin Pointer to the internal GPIO pin used for transmission. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 3ebf381c84..91218c4300 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,13 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -void ESP8266UartComponent::flush() { +FlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } + return FlushResult::ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index e84cbe386d..ca90dc5964 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 544404448b..47ddf1a38d 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -230,6 +230,9 @@ void IDFUARTComponent::dump_config() { " RX Timeout: %u", this->rx_buffer_size_, this->rx_full_threshold_, this->rx_timeout_); } + if (this->flush_timeout_ms_ > 0) { + ESP_LOGCONFIG(TAG, " Flush Timeout: %" PRIu32 " ms", this->flush_timeout_ms_); + } ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" @@ -332,9 +335,15 @@ size_t IDFUARTComponent::available() { return available; } -void IDFUARTComponent::flush() { +FlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - uart_wait_tx_done(this->uart_num_, portMAX_DELAY); + TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_); + esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks); + if (err == ESP_OK) + return FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return FlushResult::TIMEOUT; + return FlushResult::FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 631bf54cd5..9fa2013cfd 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,9 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; + + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } uint8_t get_hw_serial_number() { return this->uart_num_; } @@ -57,6 +59,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool has_peek_{false}; uint8_t peek_byte_; + uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY). #ifdef USE_UART_WAKE_LOOP_ON_RX // ISR callback for UART RX data notification — wakes the main loop directly. diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 9dce25c500..66026f3ccd 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,12 +274,13 @@ size_t HostUartComponent::available() { return result; }; -void HostUartComponent::flush() { +FlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return; + return FlushResult::ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); + return FlushResult::ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index 89b951093b..c22efdcb92 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 83d2acb332..6a550f296a 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,9 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -void LibreTinyUARTComponent::flush() { +FlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 31f082d31e..77df808067 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index faf8f4d90f..858f1a02dd 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -187,9 +187,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -void RP2040UartComponent::flush() { +FlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 4ca58e8dc6..891212ca74 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index ddcc65232d..624f41cf8c 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parentedhas_peek_ ? 1 : 0); } -void USBCDCACMInstance::flush() { +uart::FlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return; + return uart::FlushResult::ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -341,7 +341,12 @@ void USBCDCACMInstance::flush() { } // Also wait for USB to finish transmitting - tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); + esp_err_t err = tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); + if (err == ESP_OK) + return uart::FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a1f8738491..a0101a5546 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -166,7 +166,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -void USBUartChannel::flush() { +uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The 100 ms timeout guards against a device that stops responding mid-flush; @@ -177,6 +177,9 @@ void USBUartChannel::flush() { this->parent_->start_output(this); yield(); } + if (!this->output_queue_.empty() || this->output_started_.load()) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7290f8a958..671f0cab8c 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -110,7 +110,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 3f5d6c787c..f01d164e9f 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,15 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast(buffer), length); } -void WeikaiChannel::flush() { +uart::FlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return; + return uart::FlushResult::TIMEOUT; } yield(); // reschedule our thread to avoid blocking } + return uart::FlushResult::SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 43c3a1e4f4..715b82bfc7 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - void flush() override; + uart::FlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index d5ffbe1295..9f9e7b3e9f 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(uart::FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index 1f9bfa15e7..f7e2d8a3f7 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index e8c07d632d..1edeb97cf1 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -144,8 +144,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -void MockUartComponent::flush() { +uart::FlushResult MockUartComponent::flush() { // Nothing to flush in mock + return uart::FlushResult::ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 901e371dec..c9afb96357 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; From 04cff1c916ece7a8abe16a17f0c8bc4e07628d56 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 8 Mar 2026 00:04:14 -0600 Subject: [PATCH 048/101] [usb_uart] Return `flush` result, expose timeout via config (#14616) --- esphome/components/usb_uart/__init__.py | 6 +++++- esphome/components/usb_uart/usb_uart.cpp | 10 ++++++---- esphome/components/usb_uart/usb_uart.h | 11 ++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index eedf590eca..2d85723d72 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS -from esphome.components.uart import CONF_DEBUG_PREFIX, UARTComponent +from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( @@ -91,6 +91,9 @@ def channel_schema(channels, baud_rate_required): cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, + cv.Optional( + CONF_FLUSH_TIMEOUT, default="100ms" + ): cv.positive_time_period_milliseconds, } ) ), @@ -129,6 +132,7 @@ async def to_code(config): cg.add(chvar.set_parity(channel[CONF_PARITY])) cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) + cg.add(chvar.set_flush_timeout(channel[CONF_FLUSH_TIMEOUT])) cg.add(chvar.set_debug(channel[CONF_DEBUG])) if channel[CONF_DEBUG_PREFIX]: cg.add(chvar.set_debug_prefix(channel[CONF_DEBUG_PREFIX])) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a0101a5546..83de0b39fc 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,10 +169,10 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. - // The 100 ms timeout guards against a device that stops responding mid-flush; + // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; // in that case the main loop is blocked for the full duration. - uint32_t start = millis(); // 100 ms safety timeout - while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < 100) { + uint32_t start = millis(); + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < this->flush_timeout_ms_) { // Kick start_output() in case data arrived but no transfer is in flight yet. this->parent_->start_output(this); yield(); @@ -260,10 +260,12 @@ void USBUartComponent::dump_config() { " Data Bits: %u\n" " Parity: %s\n" " Stop bits: %s\n" + " Flush Timeout: %" PRIu32 " ms\n" " Debug: %s\n" " Dummy receiver: %s", channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_], - STOP_BITS_NAMES[channel->stop_bits_], YESNO(channel->debug_), YESNO(channel->dummy_receiver_)); + STOP_BITS_NAMES[channel->stop_bits_], channel->flush_timeout_ms_, YESNO(channel->debug_), + YESNO(channel->dummy_receiver_)); } } void USBUartComponent::start_input(USBUartChannel *channel) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 671f0cab8c..b1748aebf2 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -116,6 +116,7 @@ class USBUartChannel : public uart::UARTComponent, public Parenteddebug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } /// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Called from USBUartComponent::loop() in the main loop context. @@ -124,23 +125,23 @@ class USBUartChannel : public uart::UARTComponent, public Parented cb) { this->rx_callback_ = std::move(cb); } protected: - // Larger structures first for better alignment + // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue output_queue_; EventPool output_pool_; std::function rx_callback_{}; CdcEps cdc_dev_{}; - // Enum (likely 4 bytes) + StringRef debug_prefix_{}; + // 4-byte fields UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; - // Group atomics together (each 1 byte) + uint32_t flush_timeout_ms_{100}; + // 1-byte fields (no padding between groups) std::atomic input_started_{true}; std::atomic output_started_{true}; std::atomic initialised_{false}; - // Group regular bytes together to minimize padding const uint8_t index_; bool debug_{}; bool dummy_receiver_{}; - StringRef debug_prefix_{}; }; class USBUartComponent : public usb_host::USBClient { From e4b89a69d4aba18e05b0a965e98e5f242637daf2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 8 Mar 2026 07:32:20 +0100 Subject: [PATCH 049/101] [nrf52, ota] ble and serial OTA based on mcumgr (#11932) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/nrf52/__init__.py | 46 +++++- esphome/components/ota/__init__.py | 9 +- esphome/components/safe_mode/safe_mode.cpp | 15 +- esphome/components/zephyr_mcumgr/__init__.py | 0 .../components/zephyr_mcumgr/ota/__init__.py | 141 +++++++++++++++++ .../zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp | 143 ++++++++++++++++++ .../zephyr_mcumgr/ota/ota_zephyr_mcumgr.h | 29 ++++ esphome/core/defines.h | 1 + script/helpers_zephyr.py | 16 ++ tests/components/ota/test.nrf52-mcumgr.yaml | 27 ++++ 11 files changed, 422 insertions(+), 6 deletions(-) create mode 100644 esphome/components/zephyr_mcumgr/__init__.py create mode 100644 esphome/components/zephyr_mcumgr/ota/__init__.py create mode 100644 esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp create mode 100644 esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h create mode 100644 tests/components/ota/test.nrf52-mcumgr.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8bf896d159..d60dbc729d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -587,6 +587,7 @@ esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 +esphome/components/zephyr_mcumgr/ota/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zigbee/* @tomaszduda23 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 0a9fb5939a..c3a10a9944 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -27,10 +27,15 @@ from esphome.components.zephyr.const import ( ) import esphome.config_validation as cv from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, + CONF_DISABLED, + CONF_ENABLE_OTA_ROLLBACK, CONF_FRAMEWORK, CONF_ID, + CONF_OTA, CONF_RESET_PIN, + CONF_SAFE_MODE, CONF_VERSION, CONF_VOLTAGE, KEY_CORE, @@ -41,6 +46,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority +import esphome.final_validate as fv from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -133,6 +139,7 @@ CONF_UICR_ERASE = "uicr_erase" VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] + CONFIG_SCHEMA = cv.All( _detect_bootloader, set_core_data, @@ -156,9 +163,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, } ), - cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-a"}): cv.Schema( + cv.Optional( + CONF_FRAMEWORK, + default={}, + ): cv.Schema( { - cv.Required(CONF_VERSION): cv.string_strict, + cv.Optional(CONF_VERSION, default="2.6.1-a"): cv.string_strict, + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional( + CONF_ENABLE_OTA_ROLLBACK, default=True + ): cv.boolean, + } + ), } ), cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), @@ -181,6 +198,24 @@ def _final_validate(config): _LOGGER.warning( "Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`" ) + full_config = fv.full_config.get() + conf = config[CONF_FRAMEWORK] + advanced = conf[CONF_ADVANCED] + + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + # "disabled: false" means safe mode *is* enabled. + safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True}) + safe_mode_enabled = not safe_mode_config[CONF_DISABLED] + ota_enabled = CONF_OTA in full_config + # Both need to be enabled for rollback to work + if not (ota_enabled and safe_mode_enabled): + # But only warn if ota is even possible + if ota_enabled: + _LOGGER.warning( + "OTA rollback requires safe_mode, disabling rollback support" + ) + # disable the rollback feature anyway since it can't be used. + advanced[CONF_ENABLE_OTA_ROLLBACK] = False FINAL_VALIDATE_SCHEMA = _final_validate @@ -247,6 +282,11 @@ async def to_code(config: ConfigType) -> None: if reg0_config[CONF_UICR_ERASE]: cg.add_define("USE_NRF52_UICR_ERASE") + conf = config[CONF_FRAMEWORK] + advanced = conf[CONF_ADVANCED] + # Enable OTA rollback support + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + cg.add_define("USE_OTA_ROLLBACK") # c++ support if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("CPLUSPLUS", True) @@ -259,7 +299,7 @@ async def to_code(config: ConfigType) -> None: zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) # disable console zephyr_add_prj_conf("UART_CONSOLE", False) - zephyr_add_prj_conf("CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False, False) # use NFC pins as GPIO if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index ee54d5f8d3..8f31eb5cdd 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -18,7 +18,14 @@ from esphome.coroutine import CoroPriority OTA_STATE_LISTENER_KEY = "ota_state_listener" CODEOWNERS = ["@esphome/core"] -AUTO_LOAD = ["md5", "safe_mode"] + + +def AUTO_LOAD() -> list[str]: + components = ["safe_mode"] + if not CORE.using_zephyr: + components.extend(["md5"]) + return components + IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index fe2acd9612..40fa03392b 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,10 +9,14 @@ #include #include -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#ifdef USE_OTA_ROLLBACK +#ifdef USE_ZEPHYR +#include +#elif defined(USE_ESP32) #include #include #endif +#endif namespace esphome::safe_mode { @@ -66,9 +70,16 @@ float SafeModeComponent::get_setup_priority() const { return setup_priority::AFT void SafeModeComponent::mark_successful() { this->clean_rtc(); this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#if defined(USE_OTA_ROLLBACK) +// Mark OTA partition as valid to prevent rollback +#if defined(USE_ZEPHYR) + if (!boot_is_img_confirmed()) { + boot_write_img_confirmed(); + } +#elif defined(USE_ESP32) // Mark OTA partition as valid to prevent rollback esp_ota_mark_app_valid_cancel_rollback(); +#endif #endif // Disable loop since we no longer need to check this->disable_loop(); diff --git a/esphome/components/zephyr_mcumgr/__init__.py b/esphome/components/zephyr_mcumgr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zephyr_mcumgr/ota/__init__.py b/esphome/components/zephyr_mcumgr/ota/__init__.py new file mode 100644 index 0000000000..b0d86190b8 --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/__init__.py @@ -0,0 +1,141 @@ +import esphome.codegen as cg +from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.components.zephyr import ( + zephyr_add_cdc_acm, + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import BOOTLOADER_MCUBOOT, KEY_BOOTLOADER +import esphome.config_validation as cv +from esphome.const import CONF_HARDWARE_UART, CONF_ID, Framework +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority +from esphome.types import ConfigType + +CODEOWNERS = ["@tomaszduda23"] +DEPENDENCIES = ["zephyr"] + +ZephyrMcumgrOTAComponent = cg.esphome_ns.namespace("zephyr_mcumgr").class_( + "OTAComponent", OTAComponent +) + +CONF_BLE = "ble" +CONF_TRANSPORT = "transport" + + +def _validate_transport(conf: ConfigType) -> ConfigType: + transport = conf[CONF_TRANSPORT] + if transport[CONF_BLE] or CONF_HARDWARE_UART in transport: + return conf + raise cv.Invalid( + f"At least one transport protocol has to be enabled. Set '{CONF_BLE}: true' or '{CONF_HARDWARE_UART}'" + ) + + +UARTS = { + "CDC": ("cdc_acm_uart0", 0), + "CDC1": ("cdc_acm_uart1", 1), + "UART0": ("uart0", -1), + "UART1": ("uart1", -1), +} + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ZephyrMcumgrOTAComponent), + cv.Optional(CONF_TRANSPORT, default={CONF_BLE: True}): cv.Schema( + { + cv.Optional(CONF_BLE, default=False): cv.boolean, + cv.Optional( + CONF_HARDWARE_UART, + ): cv.one_of(*UARTS, upper=True), + } + ), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + _validate_transport, + cv.only_with_framework(Framework.ZEPHYR), +) + + +def _validate_mcumgr_bootloader(config: ConfigType) -> None: + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader != BOOTLOADER_MCUBOOT: + raise cv.Invalid(f"'{bootloader}' bootloader does not support OTA") + + +KEY_ZEPHYR_BLE_SERVER = "zephyr_ble_server" + + +def _validate_ble_server(config: ConfigType) -> None: + if ( + config[CONF_TRANSPORT][CONF_BLE] + and KEY_ZEPHYR_BLE_SERVER not in CORE.loaded_integrations + ): + raise cv.Invalid(f"'{KEY_ZEPHYR_BLE_SERVER}' component is required for BLE OTA") + + +def _final_validate(config: ConfigType) -> None: + _validate_mcumgr_bootloader(config) + _validate_ble_server(config) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +@coroutine_with_priority(CoroPriority.OTA_UPDATES) +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + + await cg.register_component(var, config) + + zephyr_add_prj_conf("NET_BUF", True) + zephyr_add_prj_conf("ZCBOR", True) + zephyr_add_prj_conf("MCUMGR", True) + + zephyr_add_prj_conf("MCUMGR_GRP_IMG", True) + + zephyr_add_prj_conf("IMG_MANAGER", True) + zephyr_add_prj_conf("STREAM_FLASH", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("FLASH", True) + + zephyr_add_prj_conf("IMG_ERASE_PROGRESSIVELY", True) + + zephyr_add_prj_conf("BOOTLOADER_MCUBOOT", True) + + zephyr_add_prj_conf("MCUMGR_MGMT_NOTIFICATION_HOOKS", True) + zephyr_add_prj_conf("MCUMGR_GRP_IMG_STATUS_HOOKS", True) + zephyr_add_prj_conf("MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK", True) + transport = config[CONF_TRANSPORT] + if transport[CONF_BLE]: + zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT", True) + zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT_REASSEMBLY", True) + + zephyr_add_prj_conf("MCUMGR_GRP_OS", True) + zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True) + + zephyr_add_prj_conf("NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP", True) + if CONF_HARDWARE_UART in transport: + uart = UARTS[transport[CONF_HARDWARE_UART]] + uart_name = uart[0] + cdc_id = uart[1] + if cdc_id >= 0: + zephyr_add_cdc_acm(config, cdc_id) + zephyr_add_prj_conf("MCUMGR_TRANSPORT_UART", True) + zephyr_add_prj_conf("BASE64", True) + zephyr_add_prj_conf("CONSOLE", True) + zephyr_add_overlay( + f""" + / {{ + chosen {{ + zephyr,uart-mcumgr = &{uart_name}; + }}; + }}; + """ + ) diff --git a/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp new file mode 100644 index 0000000000..f1eac462bc --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp @@ -0,0 +1,143 @@ +#ifdef USE_ZEPHYR +#include "ota_zephyr_mcumgr.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include +#include +#include + +// It should be from below header but there is problem with internal includes. +// #include +// NOLINTBEGIN(readability-identifier-naming,google-runtime-int) +struct img_mgmt_upload_action { + /** The total size of the image. */ + unsigned long long size; +}; + +struct img_mgmt_upload_req { + uint32_t image; /* 0 by default */ + size_t off; /* SIZE_MAX if unspecified */ +}; +// NOLINTEND(readability-identifier-naming,google-runtime-int) + +namespace esphome::zephyr_mcumgr { + +static_assert(sizeof(struct img_mgmt_upload_action) == 8, "ABI mismatch"); +static_assert(sizeof(struct img_mgmt_upload_req) == 8, "ABI mismatch"); +static_assert(offsetof(struct img_mgmt_upload_req, image) == 0, "ABI mismatch"); +static_assert(offsetof(struct img_mgmt_upload_req, off) == 4, "ABI mismatch"); + +static const char *const TAG = "zephyr_mcumgr"; +static OTAComponent *global_ota_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static enum mgmt_cb_return mcumgr_img_mgmt_cb(uint32_t event, enum mgmt_cb_return prev_status, int32_t *rc, + uint16_t *group, bool *abort_more, void *data, size_t data_size) { + if (MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK == event) { + const img_mgmt_upload_check &upload = *static_cast(data); + global_ota_component->update_chunk(upload); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_STARTED == event) { + global_ota_component->update_started(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK_WRITE_COMPLETE == event) { + global_ota_component->update_chunk_wrote(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_PENDING == event) { + global_ota_component->update_pending(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED == event) { + global_ota_component->update_stopped(); + } else { + ESP_LOGD(TAG, "MCUmgr Image Management Event with the %d ID", u32_count_trailing_zeros(MGMT_EVT_GET_ID(event))); + } + return MGMT_CB_OK; +} + +OTAComponent::OTAComponent() { global_ota_component = this; } + +void OTAComponent::setup() { + this->img_mgmt_callback_.callback = mcumgr_img_mgmt_cb; + this->img_mgmt_callback_.event_id = MGMT_EVT_OP_IMG_MGMT_ALL; + mgmt_callback_register(&this->img_mgmt_callback_); +#ifdef CONFIG_USB_DEVICE_STACK + usb_enable(nullptr); +#endif +// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, +// in which case safe_mode will mark it valid after confirming successful boot. +#ifndef USE_OTA_ROLLBACK + if (!boot_is_img_confirmed()) { + boot_write_img_confirmed(); + } +#endif +} + +#ifdef ESPHOME_LOG_HAS_CONFIG +static const char *swap_type_str(uint8_t type) { + switch (type) { + case BOOT_SWAP_TYPE_NONE: + return "none"; + case BOOT_SWAP_TYPE_TEST: + return "test"; + case BOOT_SWAP_TYPE_PERM: + return "perm"; + case BOOT_SWAP_TYPE_REVERT: + return "revert"; + case BOOT_SWAP_TYPE_FAIL: + return "fail"; + } + + return "unknown"; +} +#endif + +void OTAComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Over-The-Air Updates:\n" + " swap type after reboot: %s\n" + " image confirmed: %s", + swap_type_str(mcuboot_swap_type()), YESNO(boot_is_img_confirmed())); +} + +void OTAComponent::update_chunk(const img_mgmt_upload_check &upload) { + float percentage = (upload.req->off * 100.0f) / upload.action->size; + this->defer([this, percentage]() { this->percentage_ = percentage; }); +} + +void OTAComponent::update_started() { + this->defer([this]() { + ESP_LOGD(TAG, "Starting update"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); +#endif + }); +} + +void OTAComponent::update_chunk_wrote() { + uint32_t now = millis(); + if (now - this->last_progress_ > 1000) { + this->last_progress_ = now; + this->defer([this]() { + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", this->percentage_); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, this->percentage_, 0); +#endif + }); + } +} + +void OTAComponent::update_pending() { + this->defer([this]() { + ESP_LOGD(TAG, "OTA pending"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); +#endif + }); +} + +void OTAComponent::update_stopped() { + this->defer([this]() { + ESP_LOGD(TAG, "OTA stopped"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#endif + }); +} + +} // namespace esphome::zephyr_mcumgr +#endif diff --git a/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h new file mode 100644 index 0000000000..ab98f93598 --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h @@ -0,0 +1,29 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ZEPHYR +#include "esphome/components/ota/ota_backend.h" +#include + +struct img_mgmt_upload_check; + +namespace esphome::zephyr_mcumgr { + +class OTAComponent : public ota::OTAComponent { + public: + OTAComponent(); + void setup() override; + void dump_config() override; + void update_chunk(const img_mgmt_upload_check &upload); + void update_started(); + void update_chunk_wrote(); + void update_pending(); + void update_stopped(); + + protected: + uint32_t last_progress_ = 0; + float percentage_ = 0; + mgmt_callback img_mgmt_callback_{}; +}; + +} // namespace esphome::zephyr_mcumgr +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c5f38ab9aa..48c467f69f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -367,6 +367,7 @@ #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE +#define USE_OTA_ROLLBACK #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 #define USE_ZIGBEE diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index 1242a60cf4..66ef6ffc98 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -28,6 +28,22 @@ extern "C" void zboss_signal_handler() {}; CONFIG_NEWLIB_LIBC=y CONFIG_BT=y CONFIG_ADC=y +#mcumgr begin +CONFIG_NET_BUF=y +CONFIG_ZCBOR=y +CONFIG_MCUMGR=y +CONFIG_MCUMGR_GRP_IMG=y +CONFIG_IMG_MANAGER=y +CONFIG_STREAM_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH=y +CONFIG_IMG_ERASE_PROGRESSIVELY=y +CONFIG_BOOTLOADER_MCUBOOT=y +CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_STATUS_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK=y +CONFIG_MCUMGR_TRANSPORT_UART=y +#mcumgr end #zigbee begin CONFIG_ZIGBEE=y CONFIG_CRYPTO=y diff --git a/tests/components/ota/test.nrf52-mcumgr.yaml b/tests/components/ota/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..1e7986f61a --- /dev/null +++ b/tests/components/ota/test.nrf52-mcumgr.yaml @@ -0,0 +1,27 @@ +zephyr_ble_server: + +ota: + - platform: zephyr_mcumgr + transport: + ble: true + hardware_uart: CDC + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: >- + ESP_LOGD("ota", "State %d", state); From d9e76da8064ba955c2406d13e07e1377ca77ea9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 07:59:25 +0000 Subject: [PATCH 050/101] Bump aioesphomeapi from 44.4.0 to 44.5.0 (#14617) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1300a67f7..4124bf8791 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.4.0 +aioesphomeapi==44.5.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From a530aeec22174300264d58f791518d993658e9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 22:09:12 -1000 Subject: [PATCH 051/101] [api] Inline varint and encode_varint_raw fast paths for hot loop performance (#14607) Co-authored-by: Claude Opus 4.6 --- esphome/components/api/proto.cpp | 12 ++++++++ esphome/components/api/proto.h | 51 ++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 1ca6b702ad..fb229928e5 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -8,6 +8,18 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); } + +void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { + do { + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); + value >>= 7; + } while (value > 0x7F); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); +} + #ifdef USE_API_VARINT64 optional ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index bbdd11b29d..adde0a8a85 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -240,14 +240,13 @@ class ProtoWriteBuffer { ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} ProtoWriteBuffer(std::vector *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} - void encode_varint_raw(uint32_t value) { - while (value > 0x7F) { + inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { + if (value < 128) [[likely]] { this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value | 0x80); - value >>= 7; + *this->pos_++ = static_cast(value); + return; } - this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value); + this->encode_varint_raw_slow_(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { @@ -378,6 +377,9 @@ class ProtoWriteBuffer { std::vector *get_buffer() const { return buffer_; } protected: + // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small + void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline)); + #ifdef ESPHOME_DEBUG_API void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); @@ -511,24 +513,29 @@ class ProtoSize { * @param value The uint32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static constexpr uint32_t varint(uint32_t value) { - // Optimized varint size calculation using leading zeros - // Each 7 bits requires one byte in the varint encoding - if (value < 128) - return 1; // 7 bits, common case for small values - - // For larger values, count bytes needed based on the position of the highest bit set - if (value < 16384) { - return 2; // 14 bits - } else if (value < 2097152) { - return 3; // 21 bits - } else if (value < 268435456) { - return 4; // 28 bits - } else { - return 5; // 32 bits (maximum for uint32_t) - } + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { + if (value < 128) [[likely]] + return 1; // Fast path: 7 bits, most common case + if (__builtin_is_constant_evaluated()) + return varint_wide(value); + return varint_slow(value); } + private: + // Slow path for varint >= 128, outlined to keep fast path small + static uint32_t varint_slow(uint32_t value) __attribute__((noinline)); + // Shared cascade for values >= 128 (used by both constexpr and noinline paths) + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) { + if (value < 16384) + return 2; + if (value < 2097152) + return 3; + if (value < 268435456) + return 4; + return 5; + } + + public: /** * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint * From 2c705810cdaff126c8fa5270e012c8037c62311b Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 8 Mar 2026 09:40:52 +0100 Subject: [PATCH 052/101] [nrf52] allow to update OTA via cmd (#12344) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/nrf52/__init__.py | 38 +++++-- esphome/components/nrf52/ota.py | 164 +++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 esphome/components/nrf52/ota.py diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index c3a10a9944..5054e5e0df 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -392,22 +392,42 @@ def _upload_using_platformio( def upload_program(config: ConfigType, args, host: str) -> bool: from esphome.__main__ import check_permissions, get_port_type - result = 0 - handled = False + mcumgr_device: str | None = None if get_port_type(host) == "SERIAL": check_permissions(host) - result = _upload_using_platformio(config, host, ["-t", "upload"]) - handled = True + if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: + mcumgr_device = host + else: + result = _upload_using_platformio(config, host, ["-t", "upload"]) + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + return True # Handled: platformio serial upload if host == "PYOCD": result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) - handled = True + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + return True # Handled: platformio PYOCD upload - if result != 0: - raise EsphomeError(f"Upload failed with result: {result}") + # Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths + from .ble_logger import is_mac_address + from .ota import smpmgr_scan, smpmgr_upload - return handled + if host == "BLE": + mcumgr_device = asyncio.run(smpmgr_scan(CORE.name)) + + if is_mac_address(host): + mcumgr_device = host + + if mcumgr_device: + firmware = Path( + CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin") + ).resolve() + asyncio.run(smpmgr_upload(mcumgr_device, firmware)) + return True # Handled: mcumgr OTA upload + + return False # Not handled: let caller try default upload methods def show_logs(config: ConfigType, args, devices: list[str]) -> bool: @@ -415,7 +435,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: from .ble_logger import is_mac_address, logger_connect, logger_scan if devices[0] == "BLE": - ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) + ble_device = asyncio.run(logger_scan(CORE.name)) if ble_device: address = ble_device.address else: diff --git a/esphome/components/nrf52/ota.py b/esphome/components/nrf52/ota.py new file mode 100644 index 0000000000..e4b26b45eb --- /dev/null +++ b/esphome/components/nrf52/ota.py @@ -0,0 +1,164 @@ +import asyncio +from dataclasses import asdict +import json +import logging +from pathlib import Path + +from bleak import BleakScanner +from bleak.exc import BleakDeviceNotFoundError +from smp.exceptions import SMPBadStartDelimiter +from smpclient import SMPClient +from smpclient.generics import error, success +from smpclient.mcuboot import IMAGE_TLV, ImageInfo, MCUBootImageError, TLVNotFound +from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite +from smpclient.requests.os_management import ResetWrite +from smpclient.transport import SMPTransportDisconnected +from smpclient.transport.ble import ( + SMPBLETransport, + SMPBLETransportDeviceNotFound, + SMPBLETransportException, +) +from smpclient.transport.serial import SMPSerialTransport + +from esphome.core import EsphomeError +from esphome.espota2 import ProgressBar + +from .ble_logger import is_mac_address + +SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84" +BLE_SCAN_TIMEOUT = 10.0 # seconds +RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute + +_LOGGER = logging.getLogger(__name__) + + +def _json_state(o: object) -> object: + """JSON serializer for SMP image state objects.""" + if isinstance(o, (bytes, bytearray)): + return o.hex() + if hasattr(o, "hex"): + return o.hex() + if hasattr(o, "__dict__"): + return vars(o) + return str(o) + + +async def smpmgr_scan(name: str) -> str: + _LOGGER.info("Scanning bluetooth for %s...", name) + for device in await BleakScanner.discover( + timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID] + ): + if device.name == name: + return device.address + raise EsphomeError(f"BLE device {name} with OTA service not found") + + +async def smpmgr_upload(device: str, firmware: Path) -> None: + try: + await _smpmgr_upload(device, firmware) + except SMPTransportDisconnected as exc: + raise EsphomeError(f"{device} was disconnected.") from exc + except SMPBLETransportDeviceNotFound as exc: + raise EsphomeError(f"{device} was not found.") from exc + + +def _get_image_tlv_sha256(file: Path) -> bytes: + _LOGGER.info("Checking image: %s", str(file)) + try: + image_info = ImageInfo.load_file(str(file)) + _LOGGER.info( + "Image header:\n%s", json.dumps(asdict(image_info.header), indent=2) + ) + _LOGGER.debug(str(image_info)) + except MCUBootImageError as exc: + raise EsphomeError("Inspection of FW image failed") from exc + except FileNotFoundError as exc: + raise EsphomeError( + f"Firmware image file not found: {file}. Build with zephyr_mcumgr enabled" + ) from exc + + try: + image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256) + _LOGGER.info("Image tlv sha256: %s", image_tlv_sha256) + except TLVNotFound as exc: + raise EsphomeError("Could not find IMAGE_TLV_SHA256 in image.") from exc + return image_tlv_sha256.value + + +async def _smpmgr_upload(device: str, firmware: Path) -> None: + image_tlv_sha256 = _get_image_tlv_sha256(firmware) + + if is_mac_address(device): + smp_client = SMPClient(SMPBLETransport(), device) + else: + smp_client = SMPClient(SMPSerialTransport(), device) + + _LOGGER.info("Connecting %s...", device) + try: + await smp_client.connect() + except BleakDeviceNotFoundError as exc: + raise EsphomeError(f"Device {device} not found") from exc + except SMPBLETransportException as exc: + raise EsphomeError(f"Connection error with {device}") from exc + + _LOGGER.info("Connected %s...", device) + try: + await _smpmgr_upload_connected(smp_client, device, firmware, image_tlv_sha256) + finally: + await smp_client.disconnect() + + +async def _smpmgr_upload_connected( + smp_client: SMPClient, device: str, firmware: Path, image_tlv_sha256: bytes +) -> None: + try: + image_state = await smp_client.request(ImageStatesRead(), 2.5) + except (SMPBadStartDelimiter, TimeoutError) as exc: + raise EsphomeError(f"mcumgr is not supported by device ({device})") from exc + + already_uploaded = False + + if error(image_state): + raise EsphomeError(f"Failed to read image state from {device}: {image_state}") + if success(image_state): + if len(image_state.images) == 0: + _LOGGER.warning("No images on device!") + for image in image_state.images: + _LOGGER.info( + "Image state:\n%s", + json.dumps(image, indent=2, default=_json_state), + ) + if image.active and not image.confirmed: + raise EsphomeError("No free slot. Testing mode but not confirmed yet.") + if image.hash == image_tlv_sha256: + if already_uploaded: + raise EsphomeError("Both slots have the same image already") + if image.confirmed: + raise EsphomeError("The same image already confirmed") + _LOGGER.warning("The same image already uploaded") + already_uploaded = True + + if not already_uploaded: + with open(firmware, "rb") as file: + image = file.read() + upload_size = len(image) + progress = ProgressBar() + progress.update(0) + try: + async for offset in smp_client.upload(image): + progress.update(offset / upload_size) + finally: + progress.done() + + _LOGGER.info("Mark image for testing") + r = await smp_client.request(ImageStatesWrite(hash=image_tlv_sha256), 1.0) + + if error(r): + raise EsphomeError(f"Failed to mark image for testing on {device}: {r}") + + await asyncio.sleep(RESET_DELAY) + _LOGGER.info("Reset") + r = await smp_client.request(ResetWrite(), 1.0) + + if error(r): + raise EsphomeError(f"Failed to reset {device}: {r}") diff --git a/requirements.txt b/requirements.txt index 4124bf8791..03a7cac5c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 +smpclient==6.0.0 requests==2.32.5 # esp-idf >= 5.0 requires this From 0c4a44566f9ba56d5f01bb6af2b436a1c859128b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 8 Mar 2026 03:55:49 -0500 Subject: [PATCH 053/101] [serial_proxy] New component (#13944) Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/api/api.proto | 20 ++ esphome/components/api/api_connection.cpp | 93 +++++++++ esphome/components/api/api_connection.h | 10 + esphome/components/api/api_pb2.cpp | 14 ++ esphome/components/api/api_pb2.h | 26 +++ esphome/components/api/api_pb2_dump.cpp | 24 +++ esphome/components/api/api_pb2_service.h | 1 + esphome/components/serial_proxy/__init__.py | 104 ++++++++++ .../components/serial_proxy/serial_proxy.cpp | 188 ++++++++++++++++++ .../components/serial_proxy/serial_proxy.h | 129 ++++++++++++ esphome/core/application.h | 17 ++ esphome/core/defines.h | 2 + tests/components/serial_proxy/common.yaml | 10 + .../serial_proxy/test.esp32-idf.yaml | 8 + .../serial_proxy/test.esp8266-ard.yaml | 8 + .../serial_proxy/test.rp2040-ard.yaml | 8 + 17 files changed, 663 insertions(+) create mode 100644 esphome/components/serial_proxy/__init__.py create mode 100644 esphome/components/serial_proxy/serial_proxy.cpp create mode 100644 esphome/components/serial_proxy/serial_proxy.h create mode 100644 tests/components/serial_proxy/common.yaml create mode 100644 tests/components/serial_proxy/test.esp32-idf.yaml create mode 100644 tests/components/serial_proxy/test.esp8266-ard.yaml create mode 100644 tests/components/serial_proxy/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d60dbc729d..cb415bb625 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -435,6 +435,7 @@ esphome/components/sen5x/* @martgras esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core +esphome/components/serial_proxy/* @kbx81 esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp4x/* @martgras @SenexCrenshaw diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 257a7aaf82..28332d67a5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2618,6 +2618,14 @@ enum SerialProxyRequestType { SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) } +enum SerialProxyStatus { + SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed + SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error + SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance +} + // Generic request message for simple serial proxy operations message SerialProxyRequest { option (id) = 144; @@ -2628,6 +2636,18 @@ message SerialProxyRequest { SerialProxyRequestType type = 2; // Request type } +// Response to a SerialProxyRequest (e.g. flush completion or failure) +message SerialProxyRequestResponse { + option (id) = 147; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Which request type this responds to + SerialProxyStatus status = 3; // Result status + string error_message = 4; // Additional detail on failure (optional) +} + // ==================== BLUETOOTH CONNECTION PARAMS ==================== message BluetoothSetConnectionParamsRequest { option (id) = 145; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b9b33ddcc2..43f5070a40 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1445,6 +1445,89 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif +#ifdef USE_SERIAL_PROXY +void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance, + static_cast(proxies.size())); + return; + } + proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast(msg.parity), msg.stop_bits, + msg.data_size); +} + +void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + proxies[msg.instance]->write_from_client(msg.data, msg.data_len); +} + +void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + proxies[msg.instance]->set_modem_pins(msg.line_states); +} + +void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + SerialProxyGetModemPinsResponse resp{}; + resp.instance = msg.instance; + resp.line_states = proxies[msg.instance]->get_modem_pins(); + this->send_message(resp); +} + +void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + switch (msg.type) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + proxies[msg.instance]->serial_proxy_request(this, msg.type); + break; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: { + SerialProxyRequestResponse resp{}; + resp.instance = msg.instance; + resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; + switch (proxies[msg.instance]->flush_port()) { + case uart::FlushResult::SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_OK; + break; + case uart::FlushResult::ASSUMED_SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; + break; + case uart::FlushResult::TIMEOUT: + resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; + break; + case uart::FlushResult::FAILED: + resp.status = enums::SERIAL_PROXY_STATUS_ERROR; + break; + } + this->send_message(resp); + break; + } + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast(msg.type)); + break; + } +} + +void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); } +#endif + #ifdef USE_INFRARED uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast(entity); @@ -1666,6 +1749,16 @@ bool APIConnection::send_device_info_response_() { resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); #endif +#ifdef USE_SERIAL_PROXY + size_t serial_proxy_index = 0; + for (auto const &proxy : App.get_serial_proxies()) { + if (serial_proxy_index >= SERIAL_PROXY_COUNT) + break; + auto &info = resp.serial_proxies[serial_proxy_index++]; + info.name = StringRef(proxy->get_name()); + info.port_type = proxy->get_port_type(); + } +#endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b075bc83ab..5d1469e419 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -189,6 +189,15 @@ class APIConnection final : public APIServerConnectionBase { void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override; + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override; + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override; + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override; + void on_serial_proxy_request(const SerialProxyRequest &msg) override; + void send_serial_proxy_data(const SerialProxyDataReceived &msg); +#endif + #ifdef USE_EVENT void send_event(event::Event *event); #endif @@ -254,6 +263,7 @@ class APIConnection final : public APIServerConnectionBase { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } + bool is_marked_for_removal() const { return this->flags_.remove; } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } // Get client API version for feature detection diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 38ebfb9464..6fce10ca0f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3840,6 +3840,20 @@ bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } +void SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, static_cast(this->type)); + buffer.encode_uint32(3, static_cast(this->status)); + buffer.encode_string(4, this->error_message); +} +uint32_t SerialProxyRequestResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, static_cast(this->type)); + size += ProtoSize::calc_uint32(1, static_cast(this->status)); + size += ProtoSize::calc_length(1, this->error_message.size()); + return size; +} #endif #ifdef USE_BLUETOOTH_PROXY bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a6167dc810..5c712508b9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -333,6 +333,13 @@ enum SerialProxyRequestType : uint32_t { SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, }; +enum SerialProxyStatus : uint32_t { + SERIAL_PROXY_STATUS_OK = 0, + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1, + SERIAL_PROXY_STATUS_ERROR = 2, + SERIAL_PROXY_STATUS_TIMEOUT = 3, + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4, +}; #endif } // namespace enums @@ -3220,6 +3227,25 @@ class SerialProxyRequest final : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class SerialProxyRequestResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 147; + static constexpr uint8_t ESTIMATED_SIZE = 17; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request_response"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; + enums::SerialProxyStatus status{}; + StringRef error_message{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; #endif #ifdef USE_BLUETOOTH_PROXY class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 086b1bdc2f..740bf2e47f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -789,6 +789,22 @@ template<> const char *proto_enum_to_string(enums return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::SerialProxyStatus value) { + switch (value) { + case enums::SERIAL_PROXY_STATUS_OK: + return "SERIAL_PROXY_STATUS_OK"; + case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: + return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"; + case enums::SERIAL_PROXY_STATUS_ERROR: + return "SERIAL_PROXY_STATUS_ERROR"; + case enums::SERIAL_PROXY_STATUS_TIMEOUT: + return "SERIAL_PROXY_STATUS_TIMEOUT"; + case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: + return "SERIAL_PROXY_STATUS_NOT_SUPPORTED"; + default: + return "UNKNOWN"; + } +} #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { @@ -2609,6 +2625,14 @@ const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { dump_field(out, "type", static_cast(this->type)); return out.c_str(); } +const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequestResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast(this->type)); + dump_field(out, "status", static_cast(this->status)); + dump_field(out, "error_message", this->error_message); + return out.c_str(); +} #endif #ifdef USE_BLUETOOTH_PROXY const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index a031d2d969..10fd88d8e1 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -233,6 +233,7 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_SERIAL_PROXY virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif + #ifdef USE_BLUETOOTH_PROXY virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif diff --git a/esphome/components/serial_proxy/__init__.py b/esphome/components/serial_proxy/__init__.py new file mode 100644 index 0000000000..f9b8c375d2 --- /dev/null +++ b/esphome/components/serial_proxy/__init__.py @@ -0,0 +1,104 @@ +""" +Serial Proxy component for ESPHome. + +WARNING: This component is EXPERIMENTAL. The API (both Python configuration +and C++ interfaces) may change at any time without following the normal +breaking changes policy. Use at your own risk. + +Once the API is considered stable, this warning will be removed. + +Provides a proxy to/from a serial interface on the ESPHome device, allowing +Home Assistant to connect to the serial port and send/receive data to/from +an arbitrary serial device. +""" + +from dataclasses import dataclass + +from esphome import pins +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_NAME +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["api", "uart"] + +MULTI_CONF = True + +serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy") +SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice) + +api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums") +SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType") +SERIAL_PROXY_PORT_TYPES = { + "TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL, + "RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232, + "RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485, +} + +CONF_DTR_PIN = "dtr_pin" +CONF_PORT_TYPE = "port_type" +CONF_RTS_PIN = "rts_pin" + +DOMAIN = "serial_proxy" + + +@dataclass +class SerialProxyData: + count: int = 0 + + +def _get_data() -> SerialProxyData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = SerialProxyData() + return CORE.data[DOMAIN] + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SerialProxy), + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True), + cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_serial_proxy_count_define(): + """Emit the SERIAL_PROXY_COUNT define once with the final instance count.""" + count = _get_data().count + if count > 0: + cg.add_define("SERIAL_PROXY_COUNT", count) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(cg.App.register_serial_proxy(var)) + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_port_type(config[CONF_PORT_TYPE])) + cg.add_define("USE_SERIAL_PROXY") + + # Track instance count for the FINAL priority define + data = _get_data() + if data.count == 0: + # Schedule the count define job only once (on the first instance) + CORE.add_job(_add_serial_proxy_count_define) + data.count += 1 + + if CONF_RTS_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) + cg.add(var.set_rts_pin(rts_pin)) + + if CONF_DTR_PIN in config: + dtr_pin = await cg.gpio_pin_expression(config[CONF_DTR_PIN]) + cg.add(var.set_dtr_pin(dtr_pin)) diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp new file mode 100644 index 0000000000..340f9b0cb8 --- /dev/null +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -0,0 +1,188 @@ +#include "serial_proxy.h" + +#ifdef USE_SERIAL_PROXY + +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#ifdef USE_API +#include "esphome/components/api/api_connection.h" +#include "esphome/components/api/api_server.h" +#endif + +namespace esphome::serial_proxy { + +static const char *const TAG = "serial_proxy"; + +void SerialProxy::setup() { + // Set up modem control pins if configured + if (this->rts_pin_ != nullptr) { + this->rts_pin_->setup(); + this->rts_pin_->digital_write(this->rts_state_); + } + if (this->dtr_pin_ != nullptr) { + this->dtr_pin_->setup(); + this->dtr_pin_->digital_write(this->dtr_state_); + } +#ifdef USE_API + // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data + this->outgoing_msg_.instance = this->instance_index_; +#endif +} + +void SerialProxy::loop() { +#ifdef USE_API + // Detect subscriber disconnect + if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() || + !this->api_connection_->is_connection_setup() || !api_is_connected())) { + ESP_LOGW(TAG, "Subscriber disconnected"); + this->api_connection_ = nullptr; + } + + if (this->api_connection_ == nullptr) + return; + + // Read available data from UART and forward to subscribed client + size_t available = this->available(); + if (available == 0) + return; + + // Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE + uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE]; + size_t to_read = std::min(available, sizeof(buffer)); + + if (!this->read_array(buffer, to_read)) + return; + + this->outgoing_msg_.set_data(buffer, to_read); + this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); +#endif +} + +void SerialProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "Serial Proxy [%u]:\n" + " Name: %s\n" + " Port Type: %s\n" + " RTS Pin: %s\n" + " DTR Pin: %s", + this->instance_index_, this->name_ != nullptr ? this->name_ : "", + this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? "RS485" + : this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232" + : "TTL", + this->rts_pin_ != nullptr ? "configured" : "not configured", + this->dtr_pin_ != nullptr ? "configured" : "not configured"); +} + +void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, + uint8_t data_size) { + ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u", + this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size); + + auto *uart_comp = this->parent_; + if (uart_comp == nullptr) { + ESP_LOGE(TAG, "UART component not available"); + return; + } + + // Validate all parameters before applying any (values come from a remote client) + if (baudrate == 0) { + ESP_LOGW(TAG, "Invalid baud rate: 0"); + return; + } + if (stop_bits < 1 || stop_bits > 2) { + ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits); + return; + } + if (data_size < 5 || data_size > 8) { + ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size); + return; + } + if (parity > 2) { + ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity); + return; + } + + // Apply validated parameters + uart_comp->set_baud_rate(baudrate); + uart_comp->set_stop_bits(stop_bits); + uart_comp->set_data_bits(data_size); + + // Map parity value to UARTParityOptions + static const uart::UARTParityOptions PARITY_MAP[] = { + uart::UART_CONFIG_PARITY_NONE, + uart::UART_CONFIG_PARITY_EVEN, + uart::UART_CONFIG_PARITY_ODD, + }; + uart_comp->set_parity(PARITY_MAP[parity]); + + // load_settings() is available on ESP8266 and ESP32 platforms +#if defined(USE_ESP8266) || defined(USE_ESP32) + uart_comp->load_settings(true); +#endif + + if (flow_control) { + ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported"); + } +} + +void SerialProxy::write_from_client(const uint8_t *data, size_t len) { + if (data == nullptr || len == 0) + return; + this->write_array(data, len); +} + +void SerialProxy::set_modem_pins(uint32_t line_states) { + const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; + const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; + ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); + + if (this->rts_pin_ != nullptr) { + this->rts_state_ = rts; + this->rts_pin_->digital_write(rts); + } + if (this->dtr_pin_ != nullptr) { + this->dtr_state_ = dtr; + this->dtr_pin_->digital_write(dtr); + } +} + +uint32_t SerialProxy::get_modem_pins() const { + return (this->rts_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_RTS : 0u) | + (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); +} + +uart::FlushResult SerialProxy::flush_port() { + ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); + return this->flush(); +} + +#ifdef USE_API +void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) { + switch (type) { + case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); + break; + case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%u]", this->instance_index_); + return; + } + this->api_connection_ = nullptr; + ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); + break; + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast(type)); + break; + } +} +#endif + +} // namespace esphome::serial_proxy + +#endif // USE_SERIAL_PROXY diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h new file mode 100644 index 0000000000..52f0654ff0 --- /dev/null +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -0,0 +1,129 @@ +#pragma once + +// WARNING: This component is EXPERIMENTAL. The API may change at any time +// without following the normal breaking changes policy. Use at your own risk. +// Once the API is considered stable, this warning will be removed. + +#include "esphome/core/defines.h" + +#ifdef USE_SERIAL_PROXY + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/uart/uart.h" + +// Include api_pb2.h only when the API is enabled. The full include is needed +// to hold SerialProxyDataReceived by value as a pre-allocated member. +// Guarding prevents pulling conflicting Zephyr logging macro names into +// translation units that include this header without USE_API defined. +#ifdef USE_API +#include "esphome/components/api/api_pb2.h" +#endif + +// Forward-declare types needed outside the USE_API guard. +namespace esphome::api { +class APIConnection; +namespace enums { +enum SerialProxyPortType : uint32_t; +enum SerialProxyRequestType : uint32_t; +} // namespace enums +} // namespace esphome::api + +namespace esphome::serial_proxy { + +/// Bit flags for the line_states field exchanged with API clients. +/// Bit positions are stable API — new signals must use the next available bit. +enum SerialProxyLineStateFlag : uint32_t { + SERIAL_PROXY_LINE_STATE_FLAG_RTS = 1 << 0, ///< RTS (Request To Send) + SERIAL_PROXY_LINE_STATE_FLAG_DTR = 1 << 1, ///< DTR (Data Terminal Ready) +}; + +/// Maximum bytes to read from UART in a single loop iteration +inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256; + +class SerialProxy : public uart::UARTDevice, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } + + /// Get the instance index (position in Application's serial_proxies_ vector) + uint32_t get_instance_index() const { return this->instance_index_; } + + /// Set the instance index (called by Application::register_serial_proxy) + void set_instance_index(uint32_t index) { this->instance_index_ = index; } + + /// Set the human-readable port name (from YAML configuration) + void set_name(const char *name) { this->name_ = name; } + + /// Get the human-readable port name + const char *get_name() const { return this->name_; } + + /// Set the port type (from YAML configuration) + void set_port_type(api::enums::SerialProxyPortType port_type) { this->port_type_ = port_type; } + + /// Get the port type + api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; } + + /// Configure UART parameters and apply them + /// @param baudrate Baud rate in bits per second + /// @param flow_control True to enable hardware flow control + /// @param parity Parity setting (0=none, 1=even, 2=odd) + /// @param stop_bits Number of stop bits (1 or 2) + /// @param data_size Number of data bits (5-8) + void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + + /// Handle a subscribe/unsubscribe request from an API client + void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); + + /// Write data received from an API client to the serial device + /// @param data Pointer to data buffer + /// @param len Number of bytes to write + void write_from_client(const uint8_t *data, size_t len); + + /// Set modem pin states from a bitmask of SerialProxyLineStateFlag values + void set_modem_pins(uint32_t line_states); + + /// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values + uint32_t get_modem_pins() const; + + /// Flush the serial port (block until all TX data is sent) + uart::FlushResult flush_port(); + + /// Set the RTS GPIO pin (from YAML configuration) + void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } + + /// Set the DTR GPIO pin (from YAML configuration) + void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } + + protected: + /// Instance index for identifying this proxy in API messages + uint32_t instance_index_{0}; + + /// Subscribed API client (only one allowed at a time) + api::APIConnection *api_connection_{nullptr}; + +#ifdef USE_API + /// Pre-allocated outgoing message; instance field is set once in setup() + api::SerialProxyDataReceived outgoing_msg_; +#endif + + /// Human-readable port name (points to a string literal in flash) + const char *name_{nullptr}; + + /// Port type + api::enums::SerialProxyPortType port_type_{}; + + /// Optional GPIO pins for modem control + GPIOPin *rts_pin_{nullptr}; + GPIOPin *dtr_pin_{nullptr}; + + /// Current modem pin states + bool rts_state_{false}; + bool dtr_state_{false}; +}; + +} // namespace esphome::serial_proxy + +#endif // USE_SERIAL_PROXY diff --git a/esphome/core/application.h b/esphome/core/application.h index 87f9fdf59a..49253b6324 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -102,6 +102,9 @@ void socket_wake(); // NOLINT(readability-redundant-declaration) #ifdef USE_INFRARED #include "esphome/components/infrared/infrared.h" #endif +#ifdef USE_SERIAL_PROXY +#include "esphome/components/serial_proxy/serial_proxy.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -267,6 +270,13 @@ class Application { void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); } #endif +#ifdef USE_SERIAL_PROXY + void register_serial_proxy(serial_proxy::SerialProxy *proxy) { + proxy->set_instance_index(this->serial_proxies_.size()); + this->serial_proxies_.push_back(proxy); + } +#endif + #ifdef USE_EVENT void register_event(event::Event *event) { this->events_.push_back(event); } #endif @@ -498,6 +508,10 @@ class Application { GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds) #endif +#ifdef USE_SERIAL_PROXY + auto &get_serial_proxies() const { return this->serial_proxies_; } +#endif + #ifdef USE_EVENT auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) @@ -747,6 +761,9 @@ class Application { #ifdef USE_INFRARED StaticVector infrareds_{}; #endif +#ifdef USE_SERIAL_PROXY + StaticVector serial_proxies_{}; +#endif #ifdef USE_UPDATE StaticVector updates_{}; #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 48c467f69f..51f474d80e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -107,6 +107,7 @@ #define MDNS_SERVICE_COUNT 3 #define USE_MDNS_DYNAMIC_TXT #define MDNS_DYNAMIC_TXT_COUNT 2 +#define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE @@ -119,6 +120,7 @@ #define USE_SELECT #define USE_SENSOR #define USE_SENSOR_FILTER +#define USE_SERIAL_PROXY #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR diff --git a/tests/components/serial_proxy/common.yaml b/tests/components/serial_proxy/common.yaml new file mode 100644 index 0000000000..6f03cf95df --- /dev/null +++ b/tests/components/serial_proxy/common.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +serial_proxy: + - id: serial_proxy_1 + name: Test Serial Port + port_type: RS232 diff --git a/tests/components/serial_proxy/test.esp32-idf.yaml b/tests/components/serial_proxy/test.esp32-idf.yaml new file mode 100644 index 0000000000..b415125e84 --- /dev/null +++ b/tests/components/serial_proxy/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/serial_proxy/test.esp8266-ard.yaml b/tests/components/serial_proxy/test.esp8266-ard.yaml new file mode 100644 index 0000000000..96ab4ef6ac --- /dev/null +++ b/tests/components/serial_proxy/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/serial_proxy/test.rp2040-ard.yaml b/tests/components/serial_proxy/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b28f2b5e05 --- /dev/null +++ b/tests/components/serial_proxy/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml From a9b5f95c768c58aeb4e117a94effe0121fdb77c2 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Sun, 8 Mar 2026 11:24:39 +0100 Subject: [PATCH 054/101] [usb_uart] ch34x chip-type & port-count enumeration (#14544) --- esphome/components/usb_uart/ch34x.cpp | 94 ++++++++++++++++++++++++-- esphome/components/usb_uart/usb_uart.h | 37 ++++++++++ 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index e6e52a9e2a..d5428cc8d7 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -8,13 +8,97 @@ namespace esphome::usb_uart { using namespace bytebuffer; -/** - * CH34x - */ + +struct CH34xEntry { + uint16_t pid; + uint8_t byte_idx; // which status.data[] byte to inspect + uint8_t mask; // bitmask applied before comparison + uint8_t match; // 0xFF = wildcard (default/fallthrough for this PID) + CH34xChipType chiptype; + const char *name; + uint8_t num_ports; +}; + +static const CH34xEntry CH34X_TABLE[] = { + {0x55D2, 1, 0xFF, 0x41, CHIP_CH342K, "CH342K", 2}, + {0x55D2, 1, 0xFF, 0xFF, CHIP_CH342F, "CH342F", 2}, + {0x55D3, 1, 0xFF, 0x02, CHIP_CH343J, "CH343J", 1}, + {0x55D3, 1, 0xFF, 0x01, CHIP_CH343K, "CH343K", 1}, + {0x55D3, 1, 0xFF, 0x18, CHIP_CH343G_AUTOBAUD, "CH343G_AUTOBAUD", 1}, + {0x55D3, 1, 0xFF, 0xFF, CHIP_CH343GP, "CH343GP", 1}, + {0x55D4, 1, 0xFF, 0x09, CHIP_CH9102X, "CH9102X", 1}, + {0x55D4, 1, 0xFF, 0xFF, CHIP_CH9102F, "CH9102F", 1}, + {0x55D5, 1, 0xFF, 0xC0, CHIP_CH344L, "CH344L", 4}, // CH344L vs CH344L_V2 resolved below + {0x55D5, 1, 0xFF, 0xFF, CHIP_CH344Q, "CH344Q", 4}, + {0x55D7, 1, 0xFF, 0xFF, CHIP_CH9103M, "CH9103M", 2}, + {0x55D8, 1, 0xFF, 0x0A, CHIP_CH9101RY, "CH9101RY", 1}, + {0x55D8, 1, 0xFF, 0xFF, CHIP_CH9101UH, "CH9101UH", 1}, + {0x55DB, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 1}, + {0x55DD, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 1}, + {0x55DA, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 2}, + {0x55DE, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 2}, + {0x55E7, 1, 0xFF, 0xFF, CHIP_CH339W, "CH339W", 1}, + {0x55DF, 1, 0xFF, 0xFF, CHIP_CH9104L, "CH9104L", 4}, + {0x55E9, 1, 0xFF, 0xFF, CHIP_CH9111L_M0, "CH9111L_M0", 1}, + {0x55EA, 1, 0xFF, 0xFF, CHIP_CH9111L_M1, "CH9111L_M1", 1}, + {0x55E8, 2, 0xFF, 0x48, CHIP_CH9114L, "CH9114L", 4}, + {0x55E8, 2, 0xFF, 0x49, CHIP_CH9114W, "CH9114W", 4}, + {0x55E8, 2, 0xFF, 0x4A, CHIP_CH9114F, "CH9114F", 4}, + {0x55EB, 4, 0x01, 0x01, CHIP_CH346C_M1, "CH346C_M1", 1}, + {0x55EB, 4, 0x01, 0xFF, CHIP_CH346C_M0, "CH346C_M0", 1}, + {0x55EC, 1, 0xFF, 0xFF, CHIP_CH346C_M2, "CH346C_M2", 2}, +}; void USBUartTypeCH34X::enable_channels() { - // enable the channels - for (auto channel : this->channels_) { + usb_host::transfer_cb_t cb = [this](const usb_host::TransferStatus &status) { + if (!status.success) { + this->defer([this, error_code = status.error_code]() { + ESP_LOGE(TAG, "CH34x chip detection failed: %s", esp_err_to_name(error_code)); + this->apply_line_settings_(); + }); + return; + } + CH34xChipType chiptype = CHIP_UNKNOWN; + uint8_t num_ports = 1; + for (const auto &e : CH34X_TABLE) { + if (e.pid != this->pid_) + continue; + if (e.match != 0xFF && (status.data[e.byte_idx] & e.mask) != e.match) + continue; + chiptype = e.chiptype; + num_ports = e.num_ports; + break; + } + // CH344L vs CH344L_V2 requires chipver (data[0]) in addition to chiptype (data[1]) + if (chiptype == CHIP_CH344L && (status.data[0] & 0xF0) != 0x40) + chiptype = CHIP_CH344L_V2; + const char *name = "unknown"; + for (const auto &e : CH34X_TABLE) { + if (e.chiptype == chiptype) { + name = e.name; + break; + } + } + this->defer([this, chiptype, num_ports, name]() { + this->chiptype_ = chiptype; + this->chip_name_ = name; + this->num_ports_ = num_ports; + ESP_LOGD(TAG, "CH34x chip: %s, ports: %u", name, this->num_ports_); + this->apply_line_settings_(); + }); + }; + // Vendor-specific GET_CHIP_VERSION request (bRequest=0x5F): returns chip ID bytes + // used to distinguish CH34x variants sharing the same PID. + this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_IN, 0x5F, 0, 0, cb, {0, 0, 0, 0, 0, 0, 0, 0}); +} + +void USBUartTypeCH34X::dump_config() { + USBUartTypeCdcAcm::dump_config(); + ESP_LOGCONFIG(TAG, " CH34x chip: %s", this->chip_name_); +} + +void USBUartTypeCH34X::apply_line_settings_() { + for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index b1748aebf2..16469df7f6 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -35,6 +35,36 @@ struct CdcEps { uint8_t interrupt_interface_number; }; +enum CH34xChipType : uint8_t { + CHIP_CH342F = 0, + CHIP_CH342K, + CHIP_CH343GP, + CHIP_CH343G_AUTOBAUD, + CHIP_CH343K, + CHIP_CH343J, + CHIP_CH344L, + CHIP_CH344L_V2, + CHIP_CH344Q, + CHIP_CH347TF, + CHIP_CH9101UH, + CHIP_CH9101RY, + CHIP_CH9102F, + CHIP_CH9102X, + CHIP_CH9103M, + CHIP_CH9104L, + CHIP_CH340B, + CHIP_CH339W, + CHIP_CH9111L_M0, + CHIP_CH9111L_M1, + CHIP_CH9114L, + CHIP_CH9114W, + CHIP_CH9114F, + CHIP_CH346C_M0, + CHIP_CH346C_M1, + CHIP_CH346C_M2, + CHIP_UNKNOWN = 0xFF, +}; + enum UARTParityOptions { UART_CONFIG_PARITY_NONE = 0, UART_CONFIG_PARITY_ODD, @@ -192,10 +222,17 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm { class USBUartTypeCH34X : public USBUartTypeCdcAcm { public: USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} + void dump_config() override; protected: void enable_channels() override; std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; + + private: + void apply_line_settings_(); + CH34xChipType chiptype_{CHIP_UNKNOWN}; + const char *chip_name_{"unknown"}; + uint8_t num_ports_{1}; }; } // namespace esphome::usb_uart From 3f143d9f19ae29a408f1024e202aa869e86a7787 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Sun, 8 Mar 2026 14:50:32 +0100 Subject: [PATCH 055/101] [ethernet] Fix commit 3f700bac1cebf7eb6ff3b20a87d8c8af5cb9fc41 (#14618) --- esphome/components/ethernet/esp_eth_phy_jl1101.c | 2 ++ esphome/components/ethernet/ethernet_component.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index a19f7aa6b0..b81d8227d4 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "esphome/core/defines.h" + #ifdef USE_ESP32 #include diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index e54e1543e3..d9f05be9de 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -214,7 +214,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif From 1b3a7f0b6a8d927d50348b59979cae2aaa437f99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:18:14 +0000 Subject: [PATCH 056/101] Bump aioesphomeapi from 44.5.0 to 44.5.1 (#14624) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 03a7cac5c7..3da2d52b44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.0 +aioesphomeapi==44.5.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 9be1876fae30e97e763480eb4e73ecaf3a4fa445 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 8 Mar 2026 21:52:16 +0100 Subject: [PATCH 057/101] [ble_nus] make ble_nus timeout shorter than watchdog (#14619) Co-authored-by: J. Nick Koston --- esphome/components/ble_nus/ble_nus.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index e38dc99802..d0d37dbf1c 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -71,7 +71,10 @@ bool BLENUS::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; data++; if (--len == 0) { // Decrement len first, then check it... - return true; // No more to read +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif + return true; // No more to read } } @@ -101,10 +104,10 @@ size_t BLENUS::available() { } uart::FlushResult BLENUS::flush() { - constexpr uint32_t timeout_5sec = 5000; + constexpr uint32_t timeout_500ms = 500; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { - if (millis() - start > timeout_5sec) { + if (millis() - start > timeout_500ms) { ESP_LOGW(TAG, "Flush timeout"); return uart::FlushResult::TIMEOUT; } From ad5811280aaa9bce7ecfa49ea4c280522d5ac2c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 10:59:43 -1000 Subject: [PATCH 058/101] =?UTF-8?q?[ci]=20Add=20medium-pr=20label=20for=20?= =?UTF-8?q?PRs=20with=20=E2=89=A4100=20lines=20changed=20(#14628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 7 ++++++- .github/scripts/auto-label-pr/index.js | 3 ++- .github/workflows/auto-label-pr.yml | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index 8c3a62cf19..1c33772c4c 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -14,6 +14,7 @@ module.exports = { 'chained-pr', 'core', 'small-pr', + 'medium-pr', 'dashboard', 'github-actions', 'by-code-owner', diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 832fcb41db..fc63198019 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -103,7 +103,7 @@ async function detectCoreChanges(changedFiles) { } // Strategy: PR size detection -async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) { +async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) { const labels = new Set(); if (totalChanges <= SMALL_PR_THRESHOLD) { @@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange return labels; } + if (totalChanges <= MEDIUM_PR_THRESHOLD) { + labels.add('medium-pr'); + return labels; + } + const testAdditions = prFiles .filter(file => file.filename.startsWith('tests/')) .reduce((sum, file) => sum + (file.additions || 0), 0); diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 483d2cb626..42588c0bc8 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -35,6 +35,7 @@ async function fetchApiData() { module.exports = async ({ github, context }) => { // Environment variables const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD); + const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD); const MAX_LABELS = parseInt(process.env.MAX_LABELS); const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD); const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD); @@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => { detectNewComponents(prFiles), detectNewPlatforms(prFiles, apiData), detectCoreChanges(changedFiles), - detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD), + detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectDashboardChanges(changedFiles), detectGitHubActionsChanges(changedFiles), detectCodeOwner(github, context, changedFiles), diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 6fcb50b70a..6376cf877e 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -12,6 +12,7 @@ permissions: env: SMALL_PR_THRESHOLD: 30 + MEDIUM_PR_THRESHOLD: 100 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 COMPONENT_LABEL_THRESHOLD: 10 From 50b3f9d25cb91b31fb0adc6bad090f2f5bb3d778 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 8 Mar 2026 17:09:06 -0500 Subject: [PATCH 059/101] [mixer_speaker] Add task debounce (#14581) --- .../mixer/speaker/mixer_speaker.cpp | 25 ++++++++++++++----- .../components/mixer/speaker/mixer_speaker.h | 7 +++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 8e1278206f..9d11abb327 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -11,14 +11,14 @@ #include #include -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { static const UBaseType_t MIXER_TASK_PRIORITY = 10; static const uint32_t STOPPING_TIMEOUT_MS = 5000; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TASK_DELAY_MS = 25; +static const uint32_t MIXER_AUTO_STOP_DEBOUNCE_MS = 200; static const size_t TASK_STACK_SIZE = 4096; @@ -471,6 +471,7 @@ void MixerSpeaker::loop() { this->task_.deallocate(); ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); + this->all_stopped_since_ms_ = 0; } if (this->task_.is_created()) { @@ -483,8 +484,18 @@ void MixerSpeaker::loop() { } if (all_stopped) { - // Send stop command signal to the mixer task since no source speakers are active - xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + if (this->all_stopped_since_ms_ == 0) { + this->all_stopped_since_ms_ = millis(); + } else if ((millis() - this->all_stopped_since_ms_) >= MIXER_AUTO_STOP_DEBOUNCE_MS) { + // Send stop command only after a short debounce to avoid stop/start thrash during rapid seeks. + xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } + } else { + this->all_stopped_since_ms_ = 0; + // New activity detected; clear any stale auto-stop request before it can stop the running task. + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } } } else { // Task is fully stopped and cleaned up, check if we can disable loop @@ -515,6 +526,9 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { this->enable_loop_soon_any_context(); // ensure loop processes command + // Starting a new stream supersedes any previously queued stop request. + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & MIXER_TASK_COMMAND_START)) { // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency @@ -755,7 +769,6 @@ void MixerSpeaker::audio_mixer_task(void *params) { vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 0e0b33c39b..29876ea262 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -14,8 +14,7 @@ #include -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { /* Classes for mixing several source speaker audio streams and writing it to another speaker component. * - Volume controls are passed through to the output speaker @@ -200,9 +199,9 @@ class MixerSpeaker : public Component { optional audio_stream_info_; std::atomic frames_in_pipeline_{0}; // Frames written to output but not yet played + uint32_t all_stopped_since_ms_{0}; // Debounce transient all-stopped windows before stopping task }; -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif From d5dc4a39cb6f10fef73d4950fd21db3beca0f83a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:10:43 -0400 Subject: [PATCH 060/101] [i2s_audio] Fix mono sample swap and block 8-bit mono on ESP32 (#14516) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- .../components/i2s_audio/microphone/__init__.py | 6 ++++++ .../microphone/i2s_audio_microphone.cpp | 17 +++++++++-------- .../components/i2s_audio/speaker/__init__.py | 13 +++++++++---- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 16 ++++++++-------- .../speaker/media_player/audio_pipeline.cpp | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index dd23673db5..bf583b9f81 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -46,6 +46,12 @@ def _validate_esp32_variant(config): if config[CONF_ADC_TYPE] == "external": if config[CONF_PDM] and variant not in PDM_VARIANTS: raise cv.Invalid(f"{variant} does not support PDM") + if ( + variant == esp32.VARIANT_ESP32 + and config.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config if config[CONF_ADC_TYPE] == "internal": if variant not in INTERNAL_ADC_VARIANTS: diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index cdebc214e2..eb4506071e 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -281,7 +281,7 @@ bool I2SAudioMicrophone::start_driver_() { } /* Before reading data, start the RX channel first */ - i2s_channel_enable(this->rx_handle_); + err = i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; @@ -454,13 +454,14 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w } this->status_clear_warning(); #if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY) - // For ESP32 8/16 bit standard mono mode samples need to be switched. - if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) { - size_t samples_read = bytes_read / sizeof(int16_t); - for (int i = 0; i < samples_read; i += 2) { - int16_t tmp = buf[i]; - buf[i] = buf[i + 1]; - buf[i + 1] = tmp; + // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped. + if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) { + int16_t *samples = reinterpret_cast(buf); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; } } #endif diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 2e009a1de1..b84cf7de3b 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -100,11 +100,16 @@ def _set_stream_limits(config): def _validate_esp32_variant(config): - if config[CONF_DAC_TYPE] != "internal": - return config variant = esp32.get_esp32_variant() - if variant not in INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") + if config[CONF_DAC_TYPE] == "internal": + if variant not in INTERNAL_DAC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal DAC") + elif ( + variant == esp32.VARIANT_ESP32 + and config.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_MONO, CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index c934d12d65..a996702f8b 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -372,15 +372,15 @@ void I2SAudioSpeaker::speaker_task(void *params) { } #ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 8/16 bit mono mode samples need to be switched. + // For ESP32 16-bit mono mode, adjacent samples need to be swapped. if (this_speaker->current_stream_info_.get_channels() == 1 && - this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { - size_t len = bytes_read / sizeof(int16_t); - int16_t *tmp_buf = (int16_t *) new_data; - for (size_t i = 0; i < len; i += 2) { - int16_t tmp = tmp_buf[i]; - tmp_buf[i] = tmp_buf[i + 1]; - tmp_buf[i + 1] = tmp; + this_speaker->current_stream_info_.get_bits_per_sample() == 16) { + int16_t *samples = reinterpret_cast(new_data); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; } } #endif diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8cea3abcfc..0822d80254 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -504,7 +504,7 @@ void AudioPipeline::decode_task(void *params) { if (!started_playback && has_stream_info) { // Verify enough data is available before starting playback std::shared_ptr temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); - if (temp_ring_buffer->available() >= initial_bytes_to_buffer) { + if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) { started_playback = true; } } From e7730cff0024a9d48680ef6f2a2771f7461b6705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 13:59:40 -1000 Subject: [PATCH 061/101] [esp32_ble] Optimize BLE event hot path performance (#14627) --- esphome/components/esp32_ble/ble_event.h | 18 ++++++------------ esphome/core/lock_free_queue.h | 10 +++++++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 299fd7705f..ba87fd8805 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -155,44 +155,38 @@ class BLEEvent { void release() { switch (this->type_) { case GAP: - // GAP events don't have heap allocations + // GAP events never have heap allocations break; case GATTC: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) { delete[] this->event_.gattc.data.heap_data; + this->event_.gattc.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gattc.is_inline = false; - this->event_.gattc.data.heap_data = nullptr; break; case GATTS: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) { delete[] this->event_.gatts.data.heap_data; + this->event_.gatts.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gatts.is_inline = false; - this->event_.gatts.data.heap_data = nullptr; break; } } // Load new event data for reuse (replaces previous event data) + // Note: release() is NOT called here because EventPool::release() already + // calls event->release() before returning to the free list. Every event + // from allocate() is already in a clean state. void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 522fbd36e1..316186ea54 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -104,7 +104,15 @@ template class LockFreeQueue { } } - uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + uint16_t get_and_reset_dropped_count() { + // Fast path: relaxed load is a single instruction on all platforms. + // The atomic exchange (especially for uint16_t on Xtensa) compiles to + // an expensive sub-word CAS retry loop (~25 instructions + memory barriers). + // Since drops are rare, avoid the exchange in the common case. + if (dropped_count_.load(std::memory_order_relaxed) == 0) + return 0; + return dropped_count_.exchange(0, std::memory_order_relaxed); + } void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } From 76c567a71cec76ca820ee0b23107b4258276b72e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:00:04 -1000 Subject: [PATCH 062/101] [scheduler] Use std::atomic instead of std::atomic for remove flag (#14626) --- esphome/core/scheduler.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index cefbdd1b22..eb6cea4f37 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -178,9 +178,11 @@ class Scheduler { uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic for lock-free access - // Place atomic separately since it can't be packed with bit fields - std::atomic remove{false}; + // Multi-threaded with atomics: use atomic uint8_t for lock-free access. + // std::atomic is not used because GCC on Xtensa generates an indirect + // function call for std::atomic::load() instead of inlining it. + // std::atomic inlines correctly on all platforms. + std::atomic remove{0}; // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; @@ -204,7 +206,7 @@ class Scheduler { next_execution_low_(0), next_execution_high_(0), #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // remove is initialized in the member declaration as std::atomic{false} + // remove is initialized in the member declaration type(TIMEOUT), name_type_(NameType::STATIC_STRING), is_retry(false) { @@ -508,7 +510,7 @@ class Scheduler { // Multi-threaded with atomics: use atomic store with appropriate ordering // Release ordering when setting to true ensures cancellation is visible to other threads // Relaxed ordering when setting to false is sufficient for initialization - item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); + item->remove.store(removed ? 1 : 0, removed ? std::memory_order_release : std::memory_order_relaxed); #else // Single-threaded (ESPHOME_THREAD_SINGLE) or // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write From 771404668d04418b377c660398f6ae1812d8bdd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:01:01 -1000 Subject: [PATCH 063/101] [api] Inline fast path of try_to_clear_buffer (#14630) --- esphome/components/api/api_connection.cpp | 6 +----- esphome/components/api/api_connection.h | 10 +++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 43f5070a40..28a770a4fb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1945,11 +1945,7 @@ void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSet #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; } #endif -bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->flags_.remove) - return false; - if (this->helper_->can_write_without_blocking()) - return true; +bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5d1469e419..ccb51186d6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -305,7 +305,13 @@ class APIConnection final : public APIServerConnectionBase { this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); } - bool try_to_clear_buffer(bool log_out_of_space); + bool try_to_clear_buffer(bool log_out_of_space) { + if (this->flags_.remove) + return false; + if (this->helper_->can_write_without_blocking()) + return true; + return this->try_to_clear_buffer_slow_(log_out_of_space); + } bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; const char *get_name() const { return this->helper_->get_client_name(); } @@ -315,6 +321,8 @@ class APIConnection final : public APIServerConnectionBase { } protected: + bool try_to_clear_buffer_slow_(bool log_out_of_space); + // Helper function to handle authentication completion void complete_authentication_(); From 66a5ad0d75cf90ebc22ca85d139a698090be30ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:06:55 -1000 Subject: [PATCH 064/101] [core] Skip zero-initialization of StaticVector data array (#14592) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6ce5de4975..11e0afe526 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -215,7 +215,7 @@ template class StaticVector { using const_reverse_iterator = std::reverse_iterator; private: - std::array data_{}; + std::array data_; // intentionally not value-initialized to avoid memset size_t count_{0}; public: From 93d7ec4d72dc77df92b01b3ca84cb29264e46ae5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:07:59 -1000 Subject: [PATCH 065/101] [esp32_ble] Inline ble_addr_to_uint64 to eliminate call overhead (#14591) --- esphome/components/esp32_ble/ble.cpp | 11 ----------- esphome/components/esp32_ble/ble.h | 11 ++++++++++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 7fa5370072..ff9d9bb15a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -727,17 +727,6 @@ void ESP32BLE::dump_config() { } } -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2ce17e97be..04bec3f785 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -35,7 +35,16 @@ static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM) #endif -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); +inline uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} // NOLINTNEXTLINE(modernize-use-using) typedef struct { From 88536ff72bce462b8290d48ca7bc2f3f40a5e9b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:31:42 -1000 Subject: [PATCH 066/101] [modbus] Fix timeout for non-hardware UARTs (e.g., USB UART) (#14614) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/modbus/modbus.cpp | 18 ++++- esphome/components/uart/uart_component.h | 6 +- .../external_components/uart_mock/__init__.py | 8 +- .../uart_mock/automation.h | 16 +++- .../uart_mock/uart_mock.cpp | 20 +++++ .../external_components/uart_mock/uart_mock.h | 10 +++ .../uart_mock_modbus_no_threshold.yaml | 64 +++++++++++++++ tests/integration/test_uart_mock_modbus.py | 79 +++++++++++++++++++ 8 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 28e26e307e..82672217c5 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -11,6 +11,11 @@ static const char *const TAG = "modbus"; // Maximum bytes to log for Modbus frames (truncated if larger) static constexpr size_t MODBUS_MAX_LOG_BYTES = 64; +// Approximate bits per character on the wire (depends on parity/stop bit config) +static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11; +// Milliseconds per second +static constexpr uint32_t MS_PER_SEC = 1000; + void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); @@ -19,10 +24,17 @@ void Modbus::setup() { this->frame_delay_ms_ = std::max(2, // 1750us minimum per spec - rounded up to 2ms. // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay) - (uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1); + (uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1); + // When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a + // meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay. + // Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks. + static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50; + size_t rx_threshold = this->parent_->get_rx_full_threshold(); this->long_rx_buffer_delay_ms_ = - (this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1; + rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET + ? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1 + : DEFAULT_LONG_RX_BUFFER_DELAY_MS; } void Modbus::loop() { @@ -290,7 +302,7 @@ void Modbus::send_next_frame_() { this->last_send_tx_offset_ = 0; } else { this->write_array(frame.data.get(), frame.size); - this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1; + this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 3d7e71b89f..853de719fe 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -39,6 +39,8 @@ enum class FlushResult { class UARTComponent { public: + static constexpr size_t RX_FULL_THRESHOLD_UNSET = 0; + // Writes an array of bytes to the UART bus. // @param data A vector of bytes to be written. void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } @@ -201,7 +203,9 @@ class UARTComponent { InternalGPIOPin *rx_pin_{}; InternalGPIOPin *flow_control_pin_{}; size_t rx_buffer_size_{}; - size_t rx_full_threshold_{1}; + // ESP32 (both Arduino and ESP-IDF) always sets this at codegen time via set_rx_full_threshold(). + // Other platforms (USB UART, Arduino, etc.) leave it unset. + size_t rx_full_threshold_{RX_FULL_THRESHOLD_UNSET}; size_t rx_timeout_{0}; uint32_t baud_rate_{0}; uint8_t stop_bits_{0}; diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index c10d73354e..fdd481b397 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -62,6 +62,7 @@ CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MockUartComponent), cv.Required("data"): cv.templatable(validate_raw_data), + cv.Optional(CONF_DELAY): cv.positive_time_period_milliseconds, }, key=CONF_DATA, ) @@ -87,7 +88,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(MockUartComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, - cv.Optional(CONF_RX_FULL_THRESHOLD, default=10): cv.int_range(min=1, max=120), + cv.Optional(CONF_RX_FULL_THRESHOLD): cv.int_range(min=1, max=120), cv.Optional(CONF_RX_TIMEOUT, default=2): cv.int_range(min=0, max=92), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), @@ -126,6 +127,8 @@ async def inject_rx_to_code(config, action_id, template_arg, args): arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) + if CONF_DELAY in config: + cg.add(var.set_delay(config[CONF_DELAY])) return var @@ -135,7 +138,8 @@ async def to_code(config): cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) - cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) + if CONF_RX_FULL_THRESHOLD in config: + cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) diff --git a/tests/integration/fixtures/external_components/uart_mock/automation.h b/tests/integration/fixtures/external_components/uart_mock/automation.h index 83a057d3a0..b2336ad065 100644 --- a/tests/integration/fixtures/external_components/uart_mock/automation.h +++ b/tests/integration/fixtures/external_components/uart_mock/automation.h @@ -22,18 +22,30 @@ template class MockUartInjectRXAction : public Action, pu this->len_ = len; // Length >= 0 indicates static mode } + void set_delay(uint32_t delay_ms) { this->delay_ms_ = delay_ms; } + void play(const Ts &...x) override { if (this->len_ >= 0) { // Static mode: use pointer and length - this->parent_->inject_to_rx_buffer(this->code_.data, static_cast(this->len_)); + if (this->delay_ms_ > 0) { + std::vector data(this->code_.data, this->code_.data + this->len_); + this->parent_->inject_to_rx_buffer_delayed(data, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(this->code_.data, static_cast(this->len_)); + } } else { // Template mode: call function auto val = this->code_.func(x...); - this->parent_->inject_to_rx_buffer(val); + if (this->delay_ms_ > 0) { + this->parent_->inject_to_rx_buffer_delayed(val, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(val); + } } } protected: + uint32_t delay_ms_{0}; ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Code { std::vector (*func)(Ts...); // Function pointer (stateless lambdas) diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 1edeb97cf1..1a15da76d1 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -53,6 +53,15 @@ void MockUartComponent::loop() { } } + // Process staged RX - deliver bytes whose delay has elapsed + uint32_t now_ms = millis(); + while (!this->staged_rx_.empty() && (static_cast(now_ms - this->staged_rx_.front().available_at_ms) >= 0)) { + auto &staged = this->staged_rx_.front(); + ESP_LOGD(TAG, "Delivering %zu staged RX bytes", staged.data.size()); + this->inject_to_rx_buffer(staged.data); + this->staged_rx_.pop_front(); + } + // Process delayed responses for (auto &response : this->responses_) { if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) { @@ -209,4 +218,15 @@ void MockUartComponent::inject_to_rx_buffer(const std::vector &data) { } } +void MockUartComponent::inject_to_rx_buffer_delayed(const std::vector &data, uint32_t delay_ms) { + if (!data.empty() && data.size() <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay: %s", data.size(), delay_ms, + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); + } else if (data.size() > 64) { + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay (too large to log inline)", data.size(), delay_ms); + } + this->staged_rx_.push_back({data, millis() + delay_ms}); +} + } // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index c9afb96357..82e3b3d563 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -43,6 +43,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { void set_tx_hook(std::function &)> &&cb) { this->tx_hook_ = std::move(cb); } void inject_to_rx_buffer(const std::vector &data); void inject_to_rx_buffer(const uint8_t *data, size_t len); + // Stage bytes for delayed delivery - simulates transport-level latency (e.g., USB packets) + void inject_to_rx_buffer_delayed(const std::vector &data, uint32_t delay_ms); protected: void check_logger_conflict() override {} @@ -82,6 +84,14 @@ class MockUartComponent : public uart::UARTComponent, public Component { }; std::vector periodic_rx_; + // Staged RX - bytes that are pending delivery after a delay + // Simulates transport-level latency (e.g., USB packet delivery) + struct StagedRx { + std::vector data; + uint32_t available_at_ms; // millis() time when bytes become available + }; + std::deque staged_rx_; + // Observability uint32_t tx_count_{0}; uint32_t rx_count_{0}; diff --git a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml new file mode 100644 index 0000000000..e3e8c8c8da --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml @@ -0,0 +1,64 @@ +esphome: + name: uart-mock-modbus-no-thresh + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +# Simulate a non-hardware UART (e.g., USB UART) by not setting rx_full_threshold. +# This leaves it at the default sentinel value (0), triggering the 50ms fallback timeout. +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + auto_start: false + debug: + on_tx: + - then: + - if: + condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request) + lambda: "return data == std::vector({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});" + then: + - uart_mock.inject_rx: # First USB packet: SDM meter response part 1 + !lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + - uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency) + delay: 40ms + data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, + 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; + +modbus: + uart_id: virtual_uart_dev + turnaround_time: 10ms + +sensor: + - platform: sdm_meter + address: 2 + update_interval: 1s + phase_a: + voltage: + name: sdm_voltage + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 6901dc27fe..e341d86f53 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -5,6 +5,10 @@ test_uart_mock_modbus : 1. Read a single register and parse successfully (basic_register) 2. Read multiple registers from SDM meter and parse successfully (sdm_voltage), with some intermediate delay to simulate UART buffer time. +test_uart_mock_modbus_no_threshold : + Test modbus with no rx_full_threshold set (simulating USB UART / non-hardware UART). + Verifies the 50ms fallback timeout handles chunked data with USB packet gaps. + """ from __future__ import annotations @@ -218,3 +222,78 @@ async def test_uart_mock_modbus_timing( f"Timeout waiting for SDM voltage change. Received sensor states:\n" f" sdm_voltage: {sensor_states['sdm_voltage']}\n" ) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_no_threshold( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test modbus with no rx_full_threshold (simulating USB UART). + + Without the 50ms fallback timeout, the chunked response with a 40ms gap + between USB packets would cause a false timeout and CRC failure cascade. + """ + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "sdm_voltage": [], + } + + voltage_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is a good voltage reading (243V) + if ( + sensor_name == "sdm_voltage" + and state.state > 200.0 + and not voltage_changed.done() + ): + voltage_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for voltage to be updated with successful parse + try: + await asyncio.wait_for(voltage_changed, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for SDM voltage change. Received sensor states:\n" + f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + ) From c11ad7f0e6fdbce4c65eb714e794c0791a8c7352 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:32:35 -1000 Subject: [PATCH 067/101] [rp2040] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~9.2 KB flash) (#14622) --- esphome/components/rp2040/__init__.py | 17 ++++- esphome/components/rp2040/const.py | 1 + esphome/components/rp2040/printf_stubs.cpp | 74 ++++++++++++++++++++ tests/components/rp2040/test.rp2040-ard.yaml | 3 + 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 esphome/components/rp2040/printf_stubs.cpp diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 1442a0a7f7..54e1db27aa 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -21,7 +21,13 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed -from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns +from .const import ( + CONF_ENABLE_FULL_PRINTF, + KEY_BOARD, + KEY_PIO_FILES, + KEY_RP2040, + rp2040_ns, +) # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa @@ -153,6 +159,7 @@ CONFIG_SCHEMA = cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=8388)), ), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, } ), set_core_data, @@ -190,6 +197,14 @@ async def to_code(config): ], ) + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~9.2 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.core", "earlephilhower") cg.add_platformio_option("board_build.filesystem_size", "1m") diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index ab5f42d757..7eeddffc76 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,5 +1,6 @@ import esphome.codegen as cg +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" KEY_BOARD = "board" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/components/rp2040/printf_stubs.cpp b/esphome/components/rp2040/printf_stubs.cpp new file mode 100644 index 0000000000..c2174a1dec --- /dev/null +++ b/esphome/components/rp2040/printf_stubs.cpp @@ -0,0 +1,74 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The RP2040 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~8.9 KB). + * ESPHome never uses these — all logging goes through the logger component + * which uses snprintf/vsnprintf, so the libc FILE*-based printf path is + * dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary) + * and fwrite(), allowing the linker to dead-code eliminate _vfprintf_r. + * + * Saves ~8.9 KB of flash. + */ + +#if defined(USE_RP2040) && !defined(USE_FULL_PRINTF) +#include +#include +#include + +namespace esphome::rp2040 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome uses its own +// logging through snprintf/vsnprintf, not libc printf. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + // Use fwrite for the message to avoid recursive __wrap_printf call + static const char msg[] = "\nprintf buffer overflow\n"; + fwrite(msg, 1, sizeof(msg) - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_RP2040 && !USE_FULL_PRINTF diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2040/test.rp2040-ard.yaml index 039a261016..1eb315a3b4 100644 --- a/tests/components/rp2040/test.rp2040-ard.yaml +++ b/tests/components/rp2040/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +rp2040: + enable_full_printf: false + logger: level: VERBOSE From e1c849d5d22651a6e7d3457f2d5dbe206d21c386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:32:47 -1000 Subject: [PATCH 068/101] [esp8266] Wrap printf/vprintf/fprintf to eliminate _vfiprintf_r (~1.6 KB flash) (#14621) --- esphome/components/esp8266/__init__.py | 10 +++ esphome/components/esp8266/const.py | 1 + esphome/components/esp8266/printf_stubs.cpp | 71 +++++++++++++++++++ .../components/esp8266/test.esp8266-ard.yaml | 3 + 4 files changed, 85 insertions(+) create mode 100644 esphome/components/esp8266/printf_stubs.cpp diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 927a59fd61..1ef4f5e037 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -23,6 +23,7 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, + CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, @@ -179,6 +180,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, } ), set_core_data, @@ -260,6 +262,14 @@ async def to_code(config): if CORE.testing_mode: cg.add_build_flag("-DESPHOME_TESTING_MODE") + # Wrap FILE*-based printf functions to eliminate newlib's _vfiprintf_r + # (~1.6 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 229ac61f24..57eb54f0f8 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,6 +6,7 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_SERIAL = "enable_serial" CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" diff --git a/esphome/components/esp8266/printf_stubs.cpp b/esphome/components/esp8266/printf_stubs.cpp new file mode 100644 index 0000000000..e6d4a74866 --- /dev/null +++ b/esphome/components/esp8266/printf_stubs.cpp @@ -0,0 +1,71 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The ESP8266 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~900 bytes). + * ESPHome never uses these — all logging writes directly to the UART via + * Arduino's Serial, so the libc FILE*-based printf path is dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary + * for ESPHome's logging) and fwrite(), allowing the linker to dead-code + * eliminate _vfprintf_r. + * + * Saves ~1.6 KB of flash. + */ + +#if defined(USE_ESP8266) && !defined(USE_FULL_PRINTF) +#include +#include +#include + +namespace esphome::esp8266 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome writes directly +// to the UART via Arduino's Serial, and Serial.printf() has its own implementation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP8266 && !USE_FULL_PRINTF diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml index 039a261016..c77218f7a3 100644 --- a/tests/components/esp8266/test.esp8266-ard.yaml +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +esp8266: + enable_full_printf: false + logger: level: VERBOSE From aef2d74e41123f77900bc5cab37a75cfb9b6e2b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:32:59 -1000 Subject: [PATCH 069/101] [ld2450] Add integration tests with mock UART (#14611) --- .../fixtures/uart_mock_ld2450.yaml | 221 ++++++++++++++++++ tests/integration/state_utils.py | 28 ++- tests/integration/test_uart_mock_ld2450.py | 204 ++++++++++++++++ 3 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2450.yaml create mode 100644 tests/integration/test_uart_mock_ld2450.py diff --git a/tests/integration/fixtures/uart_mock_ld2450.yaml b/tests/integration/fixtures/uart_mock_ld2450.yaml new file mode 100644 index 0000000000..269136da68 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2450.yaml @@ -0,0 +1,221 @@ +esphome: + name: uart-mock-ld2450-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2450's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + auto_start: false + responses: + # Catch-all response: match any command footer (04 03 02 01). + # Returns a generic ACK to unblock setup commands. + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 04 00 = length 4 + # [6] FF = cmd (handled as CMD_ENABLE_CONF) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10-13] 04 03 02 01 = footer + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid LD2450 periodic data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # + # Target 1: X=-500mm, Y=1000mm, Speed=-50mm/s (approaching), Res=320mm + # X: magnitude=500 (0x01F4), negative → high=0x01, low=0xF4 + # Y: magnitude=1000 (0x03E8), positive → high=0x83, low=0xE8 + # Speed: raw=5, negative (approaching) → high=0x00, low=0x05, decoded=-50mm/s + # Resolution: 320 → low=0x40, high=0x01 + # Distance: sqrt(500²+1000²) = sqrt(1250000) ≈ 1118mm + # + # Target 2: X=200mm, Y=500mm, Speed=0 (stationary), Res=100mm + # X: magnitude=200 (0x00C8), positive → high=0x80, low=0xC8 + # Y: magnitude=500 (0x01F4), positive → high=0x81, low=0xF4 + # Speed: 0 → 0x00, 0x00 + # Resolution: 100 → low=0x64, high=0x00 + # Distance: sqrt(200²+500²) = sqrt(290000) ≈ 538mm + # + # Target 3: No target (all zeros) + # Distance: 0 → sensors publish unknown/NaN + # + # Counts: target_count=2, moving_target_count=1, still_target_count=1 + # + # Frame layout (30 bytes): + # [0-3] AA FF 03 00 = periodic data header + # [4-11] Target 1 (8 bytes): X_L X_H Y_L Y_H SPD_L SPD_H RES_L RES_H + # [12-19] Target 2 (8 bytes) + # [20-27] Target 3 (8 bytes) + # [28-29] 55 CC = periodic data footer + - delay: 100ms + inject_rx: + [ + 0xAA, 0xFF, 0x03, 0x00, + 0xF4, 0x01, 0xE8, 0x83, 0x05, 0x00, 0x40, 0x01, + 0xC8, 0x80, 0xF4, 0x81, 0x00, 0x00, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xCC, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2450's readline_ does NOT reject bytes at position 0 (unlike LD2412), + # so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated frame (header + partial data, no footer) + # More bytes accumulating in the buffer without a footer match. + # After this, buffer_pos_ = 7 + 8 = 15. + - delay: 100ms + inject_rx: [0xAA, 0xFF, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04] + + # Phase 4 (t=600ms): Overflow - inject 75 bytes of 0xFF (MAX_LINE_LENGTH=45) + # Buffer has 15 bytes from phases 2+3. + # readline_() stores bytes while buffer_pos_ < 44. When buffer_pos_ == 44, + # the next byte triggers overflow: logs warning, resets buffer_pos_ to 0, + # and discards that byte. + # + # First overflow: 29 bytes fill positions 15-43 (buffer_pos_=44), byte 30 + # triggers overflow (discarded). Total consumed: 30 bytes. + # Second overflow: 44 bytes fill positions 0-43 (buffer_pos_=44), byte 45 + # triggers overflow (discarded). Total consumed: 30+45 = 75 bytes. + # After both overflows, buffer_pos_ = 0 (clean state for recovery frame). + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=700ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # + # Target 1: X=300mm, Y=400mm, Speed=30mm/s (moving away), Res=100mm + # X: magnitude=300 (0x012C), positive → high=0x81, low=0x2C + # Y: magnitude=400 (0x0190), positive → high=0x81, low=0x90 + # Speed: raw=3, positive (moving away) → high=0x80, low=0x03, decoded=30mm/s + # Resolution: 100 → low=0x64, high=0x00 + # Distance: sqrt(300²+400²) = 500mm + # + # Target 2 & 3: No target (all zeros) + # Counts: target_count=1, moving_target_count=1, still_target_count=0 + - delay: 100ms + inject_rx: + [ + 0xAA, 0xFF, 0x03, 0x00, + 0x2C, 0x81, 0x90, 0x81, 0x03, 0x80, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xCC, + ] + +ld2450: + id: ld2450_dev + uart_id: mock_uart + +sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + target_count: + name: "Target Count" + filters: &sensor_filters + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_target_count: + name: "Still Target Count" + filters: *sensor_filters + moving_target_count: + name: "Moving Target Count" + filters: *sensor_filters + target_1: + x: + name: "Target 1 X" + filters: *sensor_filters + y: + name: "Target 1 Y" + filters: *sensor_filters + speed: + name: "Target 1 Speed" + filters: *sensor_filters + distance: + name: "Target 1 Distance" + filters: *sensor_filters + resolution: + name: "Target 1 Resolution" + filters: *sensor_filters + angle: + name: "Target 1 Angle" + filters: *sensor_filters + target_2: + x: + name: "Target 2 X" + filters: *sensor_filters + y: + name: "Target 2 Y" + filters: *sensor_filters + speed: + name: "Target 2 Speed" + filters: *sensor_filters + distance: + name: "Target 2 Distance" + filters: *sensor_filters + +binary_sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + has_target: + name: "Has Target" + filters: &binary_sensor_filters + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: *binary_sensor_filters + has_still_target: + name: "Has Still Target" + filters: *binary_sensor_filters + +text_sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + target_1: + direction: + name: "Target 1 Direction" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index e8c2cc5e66..ab9fdb01bb 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -13,6 +13,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, SensorState, + TextSensorState, ) _LOGGER = logging.getLogger(__name__) @@ -244,12 +245,13 @@ class InitialStateHelper: class SensorStateCollector: - """Collects sensor and binary sensor state updates and provides wait helpers. + """Collects sensor, binary sensor, and text sensor state updates with wait helpers. Usage: collector = SensorStateCollector( sensor_names=["moving_distance", "still_distance"], binary_sensor_names=["has_target"], + text_sensor_names=["direction"], ) # Use collector.on_state as the callback (or wrap it) client.subscribe_states(helper.on_state_wrapper(collector.on_state)) @@ -259,18 +261,23 @@ class SensorStateCollector: # Access collected states assert collector.sensor_states["moving_distance"][0] == approx(100.0) + assert collector.text_sensor_states["direction"][0] == "Approaching" """ def __init__( self, sensor_names: list[str], binary_sensor_names: list[str] | None = None, + text_sensor_names: list[str] | None = None, entities: list[EntityInfo] | None = None, ) -> None: self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} self.binary_states: dict[str, list[bool]] = { name: [] for name in (binary_sensor_names or []) } + self.text_sensor_states: dict[str, list[str]] = { + name: [] for name in (text_sensor_names or []) + } self._key_to_sensor: dict[int, str] = {} self._waiters: list[tuple[Callable[[], bool], asyncio.Future[bool]]] = [] @@ -279,7 +286,11 @@ class SensorStateCollector: def build_key_mapping(self, entities: list[EntityInfo]) -> None: """Build key-to-name mapping from entities. Sorted by descending length.""" - all_names = list(self.sensor_states.keys()) + list(self.binary_states.keys()) + all_names = ( + list(self.sensor_states.keys()) + + list(self.binary_states.keys()) + + list(self.text_sensor_states.keys()) + ) all_names.sort(key=len, reverse=True) self._key_to_sensor = build_key_to_entity_mapping(entities, all_names) @@ -295,6 +306,11 @@ class SensorStateCollector: if sensor_name and sensor_name in self.binary_states: self.binary_states[sensor_name].append(state.state) self._check_waiters() + elif isinstance(state, TextSensorState) and not state.missing_state: + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.text_sensor_states: + self.text_sensor_states[sensor_name].append(state.state) + self._check_waiters() def _check_waiters(self) -> None: """Check all pending waiters and resolve any whose condition is met.""" @@ -303,9 +319,11 @@ class SensorStateCollector: future.set_result(True) def _all_have_values(self) -> bool: - """Check if all sensor and binary sensor lists have at least one value.""" - return all(len(v) >= 1 for v in self.sensor_states.values()) and all( - len(v) >= 1 for v in self.binary_states.values() + """Check if all sensor, binary sensor, and text sensor lists have at least one value.""" + return ( + all(len(v) >= 1 for v in self.sensor_states.values()) + and all(len(v) >= 1 for v in self.binary_states.values()) + and all(len(v) >= 1 for v in self.text_sensor_states.values()) ) async def wait_for_all(self, timeout: float = 3.0) -> None: diff --git a/tests/integration/test_uart_mock_ld2450.py b/tests/integration/test_uart_mock_ld2450.py new file mode 100644 index 0000000000..b1aa2f6952 --- /dev/null +++ b/tests/integration/test_uart_mock_ld2450.py @@ -0,0 +1,204 @@ +"""Integration test for LD2450 component with mock UART. + +Tests: +test_uart_mock_ld2450: + 1. Happy path - valid periodic data frame publishes correct target sensor values + 2. Multi-target tracking - verifies target count, moving/still counts + 3. Target coordinate decoding - signed X/Y coordinates with sign-magnitude encoding + 4. Speed decoding - approaching (negative) and stationary (zero) targets + 5. Distance calculation - computed from X/Y via sqrt(x²+y²) + 6. Direction text sensor - "Approaching" for negative speed target + 7. Garbage resilience - random bytes don't crash the component + 8. Truncated frame handling - partial frame doesn't corrupt state + 9. Buffer overflow recovery - overflow resets the parser + 10. Post-overflow parsing - next valid frame after overflow is parsed correctly + 11. TX logging - verifies LD2450 sends expected setup commands +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2450( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2450 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + collector = SensorStateCollector( + sensor_names=[ + "target_1_x", + "target_1_y", + "target_1_speed", + "target_1_distance", + "target_1_resolution", + "target_1_angle", + "target_2_x", + "target_2_y", + "target_2_speed", + "target_2_distance", + "target_count", + "still_target_count", + "moving_target_count", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + text_sensor_names=[ + "target_1_direction", + ], + ) + + # Signal when we see recovery frame values (target 1 distance ≈ 500mm) + recovery_received = collector.add_waiter( + lambda: ( + pytest.approx(500.0, abs=1.0) + in collector.sensor_states["target_1_distance"] + ) + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}\n" + f" text_states: {collector.text_sensor_states}" + ) + + # Phase 1 values: + # Target 1: X=-500, Y=1000, Speed=-50 (approaching), Res=320 + # Distance = sqrt(500²+1000²) ≈ 1118mm + assert collector.sensor_states["target_1_x"][0] == pytest.approx(-500.0) + assert collector.sensor_states["target_1_y"][0] == pytest.approx(1000.0) + assert collector.sensor_states["target_1_speed"][0] == pytest.approx(-50.0) + assert collector.sensor_states["target_1_resolution"][0] == pytest.approx(320.0) + # Distance computed from X/Y + assert collector.sensor_states["target_1_distance"][0] == pytest.approx( + 1118.0, abs=1.0 + ) + + # Target 2: X=200, Y=500, Speed=0 (stationary), Res=100 + # Distance = sqrt(200²+500²) ≈ 538mm + assert collector.sensor_states["target_2_x"][0] == pytest.approx(200.0) + assert collector.sensor_states["target_2_y"][0] == pytest.approx(500.0) + assert collector.sensor_states["target_2_speed"][0] == pytest.approx(0.0) + assert collector.sensor_states["target_2_distance"][0] == pytest.approx( + 538.0, abs=1.0 + ) + + # Target counts: 2 targets total, 1 moving, 1 still + assert collector.sensor_states["target_count"][0] == pytest.approx(2.0) + assert collector.sensor_states["moving_target_count"][0] == pytest.approx(1.0) + assert collector.sensor_states["still_target_count"][0] == pytest.approx(1.0) + + # Binary sensors: all true (targets detected) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True + + # Direction text sensor: Target 1 is approaching (speed < 0) + assert collector.text_sensor_states["target_1_direction"][0] == "Approaching" + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2450 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2450 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2450 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow): + # Target 1: X=300, Y=400, Distance=500, Speed=30 (moving away) + # target_count=1, moving=1, still=0 + # + # Note: throttle filters cause sensor lists to have different lengths, + # so we check each value appeared somewhere rather than using a shared index. + assert ( + pytest.approx(500.0, abs=1.0) + in collector.sensor_states["target_1_distance"] + ) + assert pytest.approx(300.0) in collector.sensor_states["target_1_x"] + assert pytest.approx(400.0) in collector.sensor_states["target_1_y"] + assert pytest.approx(30.0) in collector.sensor_states["target_1_speed"] + assert pytest.approx(1.0) in collector.sensor_states["target_count"] + assert pytest.approx(1.0) in collector.sensor_states["moving_target_count"] + assert pytest.approx(0.0) in collector.sensor_states["still_target_count"] From b05dbfccd31ec769bfa873783506b469ac6def27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:52:21 -1000 Subject: [PATCH 070/101] [api] Bump noise-c to 0.1.11 (#14632) --- .clang-tidy.hash | 2 +- esphome/components/api/__init__.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index adcebadeb4..ff25675918 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf +e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index dd99862cc2..c7dec6e78b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -453,7 +453,7 @@ async def to_code(config: ConfigType) -> None: # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.10") + cg.add_library("esphome/noise-c", "0.1.11") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/platformio.ini b/platformio.ini index 87f992759c..deee23d049 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,7 +46,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} - esphome/noise-c@0.1.10 ; api + esphome/noise-c@0.1.11 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -542,7 +542,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.10 ; used by api + esphome/noise-c@0.1.11 ; used by api build_flags = ${common.build_flags} -DUSE_HOST From 9547a54fac5de13f36a98d670e60ef18b1e88fe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 14:52:40 -1000 Subject: [PATCH 071/101] [const] Move CONF_ENABLE_FULL_PRINTF to const.py (#14633) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp8266/__init__.py | 2 +- esphome/components/esp8266/const.py | 1 - esphome/components/rp2040/__init__.py | 9 ++------- esphome/components/rp2040/const.py | 1 - esphome/const.py | 1 + 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8ad84656ed..52e70501dc 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, @@ -954,7 +955,6 @@ CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 1ef4f5e037..16043b6d69 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -6,6 +6,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_BOARD_FLASH_MODE, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -23,7 +24,6 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, - CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 57eb54f0f8..229ac61f24 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,7 +6,6 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_SERIAL = "enable_serial" CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 54e1db27aa..359337adfb 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -6,6 +6,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -21,13 +22,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed -from .const import ( - CONF_ENABLE_FULL_PRINTF, - KEY_BOARD, - KEY_PIO_FILES, - KEY_RP2040, - rp2040_ns, -) +from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index 7eeddffc76..ab5f42d757 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,6 +1,5 @@ import esphome.codegen as cg -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" KEY_BOARD = "board" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/const.py b/esphome/const.py index 88e3c33fbc..d409514f3c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -356,6 +356,7 @@ CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_IPV6 = "enable_ipv6" CONF_ENABLE_ON_BOOT = "enable_on_boot" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" From d0285cdc41d479ed0aa7dc0984a71215ee4ff696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 15:11:15 -1000 Subject: [PATCH 072/101] [core] Pack entity flags into configure_entity_() and protect setters (#14564) Co-authored-by: Claude Opus 4.6 --- esphome/core/entity_base.cpp | 14 +- esphome/core/entity_base.h | 31 ++- esphome/core/entity_helpers.py | 84 ++++++- .../binary_sensor/test_binary_sensor.py | 8 +- tests/component_tests/button/test_button.py | 8 +- tests/component_tests/helpers.py | 19 ++ tests/component_tests/sensor/test_sensor.py | 12 +- tests/component_tests/text/test_text.py | 10 +- .../text_sensor/test_text_sensor.py | 20 +- tests/unit_tests/core/test_entity_helpers.py | 222 +++++++++++++++++- 10 files changed, 354 insertions(+), 74 deletions(-) create mode 100644 tests/component_tests/helpers.py diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 3274640eb3..818dae06de 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,17 +44,19 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui this->calc_object_id_(); } } - // Unpack entity string table indices. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) + // Unpack entity string table indices and flags from entity_fields. #ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = entity_strings_packed & 0xFF; + this->device_class_idx_ = (entity_fields >> ENTITY_FIELD_DC_SHIFT) & 0xFF; #endif #ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; + this->uom_idx_ = (entity_fields >> ENTITY_FIELD_UOM_SHIFT) & 0xFF; #endif #ifdef USE_ENTITY_ICON - this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; + this->icon_idx_ = (entity_fields >> ENTITY_FIELD_ICON_SHIFT) & 0xFF; #endif + this->flags_.internal = (entity_fields >> ENTITY_FIELD_INTERNAL_SHIFT) & 1; + this->flags_.disabled_by_default = (entity_fields >> ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT) & 1; + this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3; } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index accd532b0d..cccbafd2c3 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -55,6 +55,15 @@ enum EntityCategory : uint8_t { ENTITY_CATEGORY_DIAGNOSTIC = 2, }; +// Bit layout for entity_fields parameter in configure_entity_(). +// Keep in sync with _*_SHIFT constants in esphome/core/entity_helpers.py +static constexpr uint8_t ENTITY_FIELD_DC_SHIFT = 0; +static constexpr uint8_t ENTITY_FIELD_UOM_SHIFT = 8; +static constexpr uint8_t ENTITY_FIELD_ICON_SHIFT = 16; +static constexpr uint8_t ENTITY_FIELD_INTERNAL_SHIFT = 24; +static constexpr uint8_t ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT = 25; +static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26; + // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: @@ -88,21 +97,16 @@ class EntityBase { /// Useful for building compound strings without intermediate buffer size_t write_object_id_to(char *buf, size_t buf_size) const; - // Get/set whether this Entity should be hidden outside ESPHome + // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } - void set_internal(bool internal) { this->flags_.internal = internal; } // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } - void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; } - // Get/set the entity category. + // Get the entity category. EntityCategory get_entity_category() const { return static_cast(this->flags_.entity_category); } - void set_entity_category(EntityCategory entity_category) { - this->flags_.entity_category = static_cast(entity_category); - } // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). @@ -164,14 +168,13 @@ class EntityBase { #endif #ifdef USE_DEVICES - // Get/set this entity's device id + // Get this entity's device id uint32_t get_device_id() const { if (this->device_ == nullptr) { return 0; // No device set, return 0 } return this->device_->get_device_id(); } - void set_device(Device *device) { this->device_ = device; } // Get the device this entity belongs to (nullptr if main device) Device *get_device() const { return this->device_; } #endif @@ -228,8 +231,14 @@ class EntityBase { friend void ::setup(); friend void ::original_setup(); - /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. - void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags. + /// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields); + +#ifdef USE_DEVICES + // Codegen-only setter — only accessible from setup() via friend declaration. + void set_device_(Device *device) { this->device_ = device; } +#endif /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 4fa109fb0e..0589b92364 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -34,11 +34,19 @@ _KEY_ICON_IDX = "_entity_icon_idx" _KEY_ENTITY_NAME = "_entity_name" _KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: -# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) +# Bit layout for entity_fields in configure_entity_(). +# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h _DC_SHIFT = 0 _UOM_SHIFT = 8 _ICON_SHIFT = 16 +_INTERNAL_SHIFT = 24 +_DISABLED_BY_DEFAULT_SHIFT = 25 +_ENTITY_CATEGORY_SHIFT = 26 + +# Private config keys for storing flags +_KEY_INTERNAL = "_entity_internal" +_KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default" +_KEY_ENTITY_CATEGORY = "_entity_category" # Maximum unique strings per category (8-bit index, 0 = not set) _MAX_DEVICE_CLASSES = 0xFF # 255 @@ -220,8 +228,39 @@ def setup_unit_of_measurement(config: ConfigType) -> None: config[_KEY_UOM_IDX] = idx +def _sanitize_comment(text: str) -> str: + r"""Sanitize a string for safe inclusion in a C++ // line comment. + + Dangerous characters: + - \n, \r: break out of line comment, next line becomes code + - \: at end of line, splices next line into comment (eats real code) + """ + return text.replace("\\", "/").replace("\n", " ").replace("\r", "") + + +def _describe_packed_flags(config: ConfigType, entity_category: int) -> str: + """Build a human-readable description of packed entity flags for C++ comments.""" + parts: list[str] = [] + if config.get(_KEY_INTERNAL): + parts.append("internal") + if config.get(_KEY_DISABLED_BY_DEFAULT): + parts.append("disabled_by_default") + entity_cat_keys = list(cv.ENTITY_CATEGORIES) + if entity_category < len(entity_cat_keys) and ( + cat_name := entity_cat_keys[entity_category] + ): + parts.append(f"category:{cat_name}") + if config.get(_KEY_DC_IDX) and (dc := config.get(CONF_DEVICE_CLASS)): + parts.append(f"dc:{_sanitize_comment(dc)}") + if config.get(_KEY_UOM_IDX) and (uom := config.get(CONF_UNIT_OF_MEASUREMENT)): + parts.append(f"uom:{_sanitize_comment(uom)}") + if config.get(_KEY_ICON_IDX) and (icon := config.get(CONF_ICON)): + parts.append(f"icon:{_sanitize_comment(icon)}") + return ", ".join(parts) + + def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single configure_entity_() call with name, hash, and packed string indices. + """Emit a single configure_entity_() call with name, hash, packed string indices, and flags. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. @@ -231,8 +270,24 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) - packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - add(var.configure_entity_(entity_name, object_id_hash, packed)) + internal = config.get(_KEY_INTERNAL, 0) + disabled_by_default = config.get(_KEY_DISABLED_BY_DEFAULT, 0) + entity_category = config.get(_KEY_ENTITY_CATEGORY, 0) + packed = ( + (dc_idx << _DC_SHIFT) + | (uom_idx << _UOM_SHIFT) + | (icon_idx << _ICON_SHIFT) + | (internal << _INTERNAL_SHIFT) + | (disabled_by_default << _DISABLED_BY_DEFAULT_SHIFT) + | (entity_category << _ENTITY_CATEGORY_SHIFT) + ) + # Build inline comment describing the packed flags for readability + comment = _describe_packed_flags(config, entity_category) + expr = var.configure_entity_(entity_name, object_id_hash, packed) + if comment: + add(RawStatement(f"{expr}; // {comment}")) + else: + add(expr) def get_base_entity_object_id( @@ -332,7 +387,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> # Get device info if configured if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) - add(var.set_device(device)) + add(var.set_device_(device)) # Pre-compute entity name and object_id hash for configure_entity_() # which is emitted later by finalize_entity_strings(). @@ -343,18 +398,25 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 config[_KEY_ENTITY_NAME] = entity_name config[_KEY_OBJECT_ID_HASH] = object_id_hash - # Only set disabled_by_default if True (default is False) - if config[CONF_DISABLED_BY_DEFAULT]: - add(var.set_disabled_by_default(True)) + # Store flags for packing into configure_entity_() + config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT]) if CONF_INTERNAL in config: - add(var.set_internal(config[CONF_INTERNAL])) + config[_KEY_INTERNAL] = int(config[CONF_INTERNAL]) icon_idx = 0 if CONF_ICON in config: # Add USE_ENTITY_ICON define when icons are used cg.add_define("USE_ENTITY_ICON") icon_idx = register_icon(config[CONF_ICON]) if CONF_ENTITY_CATEGORY in config: - add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) + # Derive integer value from key position in cv.ENTITY_CATEGORIES + # (must match C++ EntityCategory enum in entity_base.h) + entity_cat_str = str(config[CONF_ENTITY_CATEGORY]) + entity_cat_keys = list(cv.ENTITY_CATEGORIES) + config[_KEY_ENTITY_CATEGORY] = ( + entity_cat_keys.index(entity_cat_str) + if entity_cat_str in entity_cat_keys + else 0 + ) # Store icon index for finalize_entity_strings config[_KEY_ICON_IDX] = icon_idx diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index fbc2f37d9a..10d7f80834 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -1,5 +1,7 @@ """Tests for the binary sensor component.""" +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value + def test_binary_sensor_is_setup(generate_main): """ @@ -44,9 +46,9 @@ def test_binary_sensor_config_value_internal_set(generate_main): "tests/component_tests/binary_sensor/test_binary_sensor.yaml" ) - # Then - assert "bs_1->set_internal(true);" in main_cpp - assert "bs_2->set_internal(false);" in main_cpp + # Then: bs_1 has internal: true, bs_2 has internal: false + assert extract_packed_value(main_cpp, "bs_1") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "bs_2") & INTERNAL_BIT == 0 def test_binary_sensor_config_value_use_raw_set(generate_main): diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 9f94d61c8c..a35994a682 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -1,5 +1,7 @@ """Tests for the button component""" +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value + def test_button_is_setup(generate_main): """ @@ -39,6 +41,6 @@ def test_button_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/button/test_button.yaml") - # Then - assert "wol_1->set_internal(true);" in main_cpp - assert "wol_2->set_internal(false);" in main_cpp + # Then: wol_1 has internal: true, wol_2 has internal: false + assert extract_packed_value(main_cpp, "wol_1") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "wol_2") & INTERNAL_BIT == 0 diff --git a/tests/component_tests/helpers.py b/tests/component_tests/helpers.py new file mode 100644 index 0000000000..568d1639d0 --- /dev/null +++ b/tests/component_tests/helpers.py @@ -0,0 +1,19 @@ +"""Shared helpers for component tests.""" + +from __future__ import annotations + +import re + +INTERNAL_BIT = 1 << 24 + + +def extract_packed_value(main_cpp: str, var_name: str) -> int: + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = ( + rf"{re.escape(var_name)}->configure_entity_\(" + r'"(?:\\.|[^"\\])*"' + r",\s*\w+,\s*(\d+)\)" + ) + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index d9ab3a022c..9d18fa36b8 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,14 +1,6 @@ """Tests for the sensor component.""" -import re - - -def _extract_packed_value(main_cpp, var_name): - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" - return int(match.group(1)) +from tests.component_tests.helpers import extract_packed_value def test_sensor_device_class_set(generate_main): @@ -21,5 +13,5 @@ def test_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") # Then: device_class: voltage means packed value must be non-zero - packed = _extract_packed_value(main_cpp, "s_1") + packed = extract_packed_value(main_cpp, "s_1") assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 3ceaa9b8f8..c74dfb8a47 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,6 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" + +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value def test_text_is_setup(generate_main): @@ -37,9 +39,9 @@ def test_text_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") - # Then - assert "it_2->set_internal(false);" in main_cpp - assert "it_3->set_internal(true);" in main_cpp + # Then: it_2 has internal: false, it_3 has internal: true + assert extract_packed_value(main_cpp, "it_2") & INTERNAL_BIT == 0 + assert extract_packed_value(main_cpp, "it_3") & INTERNAL_BIT != 0 def test_text_config_value_mode_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index f30b820e94..1ff31ab96b 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,14 +1,6 @@ """Tests for the text sensor component.""" -import re - - -def _extract_packed_value(main_cpp, var_name): - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" - return int(match.group(1)) +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value def test_text_sensor_is_setup(generate_main): @@ -49,9 +41,9 @@ def test_text_sensor_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_internal(true);" in main_cpp - assert "ts_3->set_internal(false);" in main_cpp + # Then: ts_2 has internal: true, ts_3 has internal: false + assert extract_packed_value(main_cpp, "ts_2") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "ts_3") & INTERNAL_BIT == 0 def test_text_sensor_device_class_set(generate_main): @@ -65,7 +57,7 @@ def test_text_sensor_device_class_set(generate_main): # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date # so their packed values must be non-zero - packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + packed_ts_2 = extract_packed_value(main_cpp, "ts_2") assert packed_ts_2 != 0 - packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + packed_ts_3 = extract_packed_value(main_cpp, "ts_3") assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 3f6faaee54..d6cbb8c6be 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -9,6 +9,7 @@ import pytest from esphome.config_validation import Invalid from esphome.const import ( + CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, @@ -16,16 +17,20 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, entity_helpers from esphome.core.entity_helpers import ( _register_string, _setup_entity_impl, entity_duplicate_validator, + finalize_entity_strings, get_base_entity_object_id, register_device_class, register_icon, + setup_device_class, setup_entity, + setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj from esphome.helpers import sanitize, snake_case @@ -486,8 +491,6 @@ async def test_setup_entity_disabled_by_default( ) -> None: """Test setup_entity sets disabled_by_default correctly.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -497,10 +500,8 @@ async def test_setup_entity_disabled_by_default( await _setup_entity_impl(var, config, "sensor") - # Check disabled_by_default was set - assert any( - "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions - ) + # disabled_by_default is now packed into config for configure_entity_() + assert config.get("_entity_disabled_by_default") == 1 def test_entity_duplicate_validator() -> None: @@ -785,8 +786,8 @@ async def test_setup_entity_empty_name_with_device( entity_helpers.get_variable = original_get_variable - # Check that set_device was called - assert any("sensor1.set_device" in expr for expr in added_expressions) + # Check that set_device_ was called (separate protected call, accessible via friend) + assert any("sensor1.set_device_" in expr for expr in added_expressions) # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime assert config.get("_entity_name") == "" @@ -928,7 +929,7 @@ def test_register_device_class_max_length() -> None: async def test_setup_entity_with_entity_category( setup_test_environment: list[str], ) -> None: - """Test setup_entity sets entity_category correctly.""" + """Test entity_category is packed correctly through the full setup flow.""" added_expressions = setup_test_environment var = MockObj("sensor1") config = { @@ -937,9 +938,10 @@ async def test_setup_entity_with_entity_category( CONF_ENTITY_CATEGORY: "diagnostic", } await _setup_entity_impl(var, config, "sensor") - assert any( - 'set_entity_category("diagnostic")' in expr for expr in added_expressions - ) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "category:diagnostic" in added_expressions[0] @pytest.mark.asyncio @@ -988,3 +990,199 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) -> assert body_called object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "temperature" + + +# Tests for finalize_entity_strings packing +# +# These tests verify that flags and string indices produce non-zero packed values +# and correct inline comments. The actual bit layout correctness (Python _*_SHIFT +# matching C++ ENTITY_FIELD_*_SHIFT) is verified end-to-end by the integration +# test test_host_mode_entity_fields, which compiles firmware and checks values +# via the native API. + + +def _extract_packed_value(expressions: list[str]) -> int: + """Extract the third argument (packed value) from a configure_entity_() call.""" + for expr in expressions: + if "configure_entity_" in expr: + # Match the last integer argument before the closing ");" + match = re.search(r",\s*(\d+)\s*\)", expr) + if match: + return int(match.group(1)) + raise AssertionError("No configure_entity_ call found") + + +@pytest.mark.asyncio +async def test_finalize_no_flags(setup_test_environment: list[str]) -> None: + """Test entity with no special flags — packed value is 0, no comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed == 0 + assert "//" not in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_internal(setup_test_environment: list[str]) -> None: + """Test entity with internal=True packs the internal flag.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_INTERNAL: True, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "// internal" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_disabled_by_default( + setup_test_environment: list[str], +) -> None: + """Test entity with disabled_by_default=True packs the flag.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: True, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "// disabled_by_default" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_entity_category( + setup_test_environment: list[str], +) -> None: + """Test entity_category values are packed and described in comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + + # Test diagnostic + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "diagnostic", + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed_diag = _extract_packed_value(added_expressions) + assert packed_diag != 0 + assert "category:diagnostic" in added_expressions[0] + + # Test config — different packed value + added_expressions.clear() + config2 = { + CONF_NAME: "Test2", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "config", + } + await _setup_entity_impl(var, config2, "sensor") + finalize_entity_strings(var, config2) + packed_cfg = _extract_packed_value(added_expressions) + assert packed_cfg != 0 + assert packed_cfg != packed_diag + assert "category:config" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_string_indices( + setup_test_environment: list[str], +) -> None: + """Test device_class, unit_of_measurement, and icon produce non-zero packed value.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_DEVICE_CLASS: "temperature", + CONF_UNIT_OF_MEASUREMENT: "°C", + CONF_ICON: "mdi:thermometer", + } + await _setup_entity_impl(var, config, "sensor") + setup_device_class(config) + setup_unit_of_measurement(config) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + comment = added_expressions[0] + assert "dc:temperature" in comment + assert "uom:°C" in comment + assert "icon:mdi:thermometer" in comment + + +@pytest.mark.asyncio +async def test_finalize_all_fields( + setup_test_environment: list[str], +) -> None: + """Test all fields set: flags, string indices, and comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: True, + CONF_INTERNAL: True, + CONF_ENTITY_CATEGORY: "diagnostic", + CONF_DEVICE_CLASS: "temperature", + CONF_UNIT_OF_MEASUREMENT: "°C", + CONF_ICON: "mdi:thermometer", + } + await _setup_entity_impl(var, config, "sensor") + setup_device_class(config) + setup_unit_of_measurement(config) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + # Verify comment contains all flags with actual string values + comment_line = added_expressions[0] + assert ( + "// internal, disabled_by_default, category:diagnostic," + " dc:temperature, uom:°C, icon:mdi:thermometer" in comment_line + ) + + +@pytest.mark.asyncio +async def test_finalize_comment_sanitization( + setup_test_environment: list[str], +) -> None: + """Test that user strings in comments are sanitized against injection.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + # Backslash at end would cause line splice eating next code line + CONF_ICON: "mdi:evil\\", + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + comment_line = added_expressions[0] + # Backslash must be replaced to prevent line splice + assert "\\" not in comment_line + assert "mdi:evil/" in comment_line + + added_expressions.clear() + config2 = { + CONF_NAME: "Test2", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:evil\nINJECTED_CODE();", + } + await _setup_entity_impl(var, config2, "sensor") + finalize_entity_strings(var, config2) + comment_line = added_expressions[0] + # Newline must be replaced to prevent breaking out of comment + assert "\n" not in comment_line + assert "INJECTED_CODE" in comment_line # still visible but safe in comment From c681dc8872f88f894bb3b71026ffbec013c1bdc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 15:11:24 -1000 Subject: [PATCH 073/101] [socket] Add socket wake support for RP2040 (#14498) --- .../components/socket/lwip_raw_tcp_impl.cpp | 73 ++++++++++++++++++- esphome/components/socket/socket.h | 12 ++- esphome/core/application.cpp | 8 +- esphome/core/application.h | 8 +- esphome/core/component.cpp | 4 +- 5 files changed, 93 insertions(+), 12 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d697bd47a5..445a57809d 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -11,6 +11,9 @@ #ifdef USE_ESP8266 #include // For esp_schedule() +#elif defined(USE_RP2040) +#include // For __sev(), __wfe() +#include // For add_alarm_in_ms(), cancel_alarm() #endif namespace esphome::socket { @@ -40,6 +43,72 @@ void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } +#elif defined(USE_RP2040) +// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. +// +// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, +// then sleep with __wfe(). Wake on either: +// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout +// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake +// +// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, +// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept +// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_socket_woke = false; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + // Wake the main loop from __wfe() sleep — timeout expired. + __sev(); + // Return 0 = don't reschedule (one-shot) + return 0; +} + +void socket_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + // If a wake was already signalled, consume it and return immediately + // instead of going to sleep. This avoids losing a wake that arrived + // between loop iterations. + if (s_socket_woke) { + s_socket_woke = false; + return; + } + s_socket_woke = false; + s_delay_expired = false; + // Set a one-shot timer to wake us after the timeout. + // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + // Sleep until woken by either the timer alarm or socket_wake(). + // __wfe() may return spuriously (stale event register, other interrupts), + // so we loop checking both flags. + while (!s_socket_woke && !s_delay_expired) { + __wfe(); + } + // Cancel timer if we woke early (socket data arrived before timeout) + if (!s_delay_expired) + cancel_alarm(alarm); +} + +// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP +// callbacks via pendsv (not hard IRQ), so they execute from flash safely. +void socket_wake() { + s_socket_woke = true; + // Wake the main loop from __wfe() sleep. __sev() is a global event that + // wakes any core sleeping in __wfe(). This is ISR-safe. + __sev(); +} #endif static const char *const TAG = "socket.lwip"; @@ -371,7 +440,7 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. socket_wake(); #endif @@ -650,7 +719,7 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { sock->init(); this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. socket_wake(); #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 0884e4ba3e..a21bd64730 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -120,13 +120,17 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span buf); -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Delay that can be woken early by socket activity. -/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. +/// On ESP8266, uses esp_delay() with a callback that checks socket activity. +/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt +/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. void socket_delay(uint32_t ms); -/// Signal socket/IO activity and wake the main loop from esp_delay() early. -/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule(). +/// Signal socket/IO activity and wake the main loop early. +/// On ESP8266: sets flag + esp_schedule(). +/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). +/// ISR-safe on both platforms. void socket_wake(); // NOLINT(readability-redundant-declaration) #endif diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index db1c8a0c0a..8685bff360 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -32,7 +32,7 @@ #include "esphome/components/status_led/status_led.h" #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) #include "esphome/components/socket/socket.h" #endif @@ -713,8 +713,10 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity via esp_schedule() +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake socket::socket_delay(delay_ms); #else // No select support, use regular delay diff --git a/esphome/core/application.h b/esphome/core/application.h index 49253b6324..f357c6b1a3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -34,7 +34,7 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { void socket_wake(); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket @@ -565,8 +565,12 @@ class Application { #if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Wake the main event loop from any context (ISR, thread, or main loop). - /// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. + /// Sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } +#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context. + /// Sets the socket wake flag and calls __sev() to exit __wfe() early. + static void wake_loop_any_context() { socket::socket_wake(); } #endif protected: diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2879d4b5ab..cce0c7b3e0 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -322,11 +322,13 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ + ((defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP)) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. + // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. Application::wake_loop_any_context(); #endif } From 6ba5c9a7056a07fe431ebf3689dbda307892ea85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 15:22:39 -1000 Subject: [PATCH 074/101] [api] Skip state_action_() call in noise data path (#14629) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- esphome/components/api/api_frame_helper.h | 11 +++++++++++ .../components/api/api_frame_helper_noise.cpp | 18 ++++-------------- .../api/api_frame_helper_plaintext.cpp | 14 +++++++------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 2b4e9ea3cd..151314658e 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -232,6 +232,17 @@ class APIFrameHelper { EXPLICIT_REJECT = 8, // Noise only }; + // Fast inline state check for read_packet/write_protobuf_messages hot path. + // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any + // other intermediate state to WOULD_BLOCK. + inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const { + if (this->state_ == State::DATA) + return APIError::OK; + if (this->state_ == State::CLOSED || this->state_ == State::FAILED) + return APIError::BAD_STATE; + return APIError::WOULD_BLOCK; + } + // Containers (size varies, but typically 12+ bytes on 32-bit) std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector rx_buf_; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index ba4f2f0642..62523fb835 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -397,14 +397,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr = this->state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } aerr = this->try_read_frame_(); if (aerr != APIError::OK) @@ -461,14 +456,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { - APIError aerr = state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } if (messages.empty()) { return APIError::OK; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index e2bb56e0ac..3c54ed7c70 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -195,11 +195,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { } APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; - APIError aerr = this->try_read_frame_(); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { // Make sure to tell the remote that we don't @@ -244,9 +244,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; if (messages.empty()) { return APIError::OK; From cac751e9e8d1a185ede1f001bb19cc397a3b6a7f Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:29:41 +0100 Subject: [PATCH 075/101] [nextion] Add configurable HTTP parameters for TFT upload (#14234) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/nextion/base_component.py | 3 + esphome/components/nextion/display.py | 66 +++++++++++++++++-- esphome/components/nextion/nextion.cpp | 12 ++++ esphome/components/nextion/nextion.h | 31 +++++++++ .../nextion/nextion_upload_arduino.cpp | 10 +-- .../nextion/nextion_upload_esp32.cpp | 8 ++- .../components/nextion/common_tft_upload.yaml | 5 ++ .../nextion/common_tft_upload_watchdog.yaml | 3 + tests/components/nextion/test.esp32-ard.yaml | 6 +- tests/components/nextion/test.esp32-idf.yaml | 6 +- .../components/nextion/test.esp8266-ard.yaml | 5 +- 11 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 tests/components/nextion/common_tft_upload.yaml create mode 100644 tests/components/nextion/common_tft_upload_watchdog.yaml diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 86551cbe23..7705b21b0b 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -27,6 +27,9 @@ CONF_PRECISION = "precision" CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake" CONF_START_UP_PAGE = "start_up_page" CONF_STARTUP_OVERRIDE_MS = "startup_override_ms" +CONF_TFT_UPLOAD_HTTP_RETRIES = "tft_upload_http_retries" +CONF_TFT_UPLOAD_HTTP_TIMEOUT = "tft_upload_http_timeout" +CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT = "tft_upload_watchdog_timeout" CONF_TFT_URL = "tft_url" CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" CONF_VARIABLE_NAME = "variable_name" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 3bfcc95995..b8fcd5d8cf 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -33,15 +33,24 @@ from .base_component import ( CONF_SKIP_CONNECTION_HANDSHAKE, CONF_START_UP_PAGE, CONF_STARTUP_OVERRIDE_MS, + CONF_TFT_UPLOAD_HTTP_RETRIES, + CONF_TFT_UPLOAD_HTTP_TIMEOUT, + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] - DEPENDENCIES = ["uart"] -AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] + + +def AUTO_LOAD() -> list[str]: + base = ["binary_sensor", "switch", "sensor", "text_sensor"] + if CORE.is_esp32: + base.append("watchdog") + return base + NextionSetBrightnessAction = nextion_ns.class_( "NextionSetBrightnessAction", automation.Action @@ -55,7 +64,24 @@ BufferOverflowTrigger = nextion_ns.class_( "BufferOverflowTrigger", automation.Trigger.template() ) -CONFIG_SCHEMA = ( + +def _validate_tft_upload(config): + has_tft_url = CONF_TFT_URL in config + for conf_key in ( + CONF_TFT_UPLOAD_HTTP_TIMEOUT, + CONF_TFT_UPLOAD_HTTP_RETRIES, + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, + ): + if conf_key in config and not has_tft_url: + raise cv.Invalid(f"{conf_key} requires {CONF_TFT_URL} to be set") + if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config and not CORE.is_esp32: + raise cv.Invalid( + f"{CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT} is only available on ESP32" + ) + return config + + +CONFIG_SCHEMA = cv.All( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), @@ -115,6 +141,14 @@ CONFIG_SCHEMA = ( ), ), cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, + cv.Optional(CONF_TFT_UPLOAD_HTTP_RETRIES): cv.int_range(min=1, max=255), + cv.Optional(CONF_TFT_UPLOAD_HTTP_TIMEOUT): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + cv.Optional( + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( 0, cv.int_range(min=3, max=65535) @@ -123,7 +157,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("5s")) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + _validate_tft_upload, ) @@ -176,6 +211,29 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) + + # TFT upload HTTP timeout (default: 4.5s) + if CONF_TFT_UPLOAD_HTTP_TIMEOUT in config: + cg.add( + var.set_tft_upload_http_timeout( + config[CONF_TFT_UPLOAD_HTTP_TIMEOUT].total_milliseconds + ) + ) + + # TFT upload HTTP retries (default: 5) + if CONF_TFT_UPLOAD_HTTP_RETRIES in config: + cg.add( + var.set_tft_upload_http_retries(config[CONF_TFT_UPLOAD_HTTP_RETRIES]) + ) + + # TFT upload watchdog timeout (default: 0 = no adjustment) + if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config: + cg.add( + var.set_tft_upload_watchdog_timeout( + config[CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT].total_milliseconds + ) + ) + if CORE.is_esp32: # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_http_client") diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index c8c1b6fa41..cb20c34005 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -191,6 +191,18 @@ void Nextion::dump_config() { #ifdef USE_NEXTION_MAX_QUEUE_SIZE ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_); #endif +#ifdef USE_NEXTION_TFT_UPLOAD + ESP_LOGCONFIG(TAG, + " TFT URL: %s\n" + " TFT upload HTTP timeout: %" PRIu16 "ms\n" + " TFT upload HTTP retries: %u", + this->tft_url_.c_str(), this->tft_upload_http_timeout_, this->tft_upload_http_retries_); +#ifdef USE_ESP32 + if (this->tft_upload_watchdog_timeout_ > 0) { + ESP_LOGCONFIG(TAG, " TFT upload WDT timeout: %" PRIu32 "ms", this->tft_upload_watchdog_timeout_); + } +#endif // USE_ESP32 +#endif // USE_NEXTION_TFT_UPLOAD } void Nextion::update() { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c42ddba9b5..7999e3c4e3 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1071,6 +1071,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); #ifdef USE_NEXTION_TFT_UPLOAD + /** + * @brief Set the HTTP timeout for TFT upload requests. + * @param timeout_ms Timeout in milliseconds. Defaults to 4500ms (4.5s). + */ + void set_tft_upload_http_timeout(uint16_t timeout_ms) { this->tft_upload_http_timeout_ = timeout_ms; } + +#ifdef USE_ESP32 + /** + * @brief Set the watchdog timeout during TFT upload. + * + * The system watchdog timeout is temporarily adjusted to this value + * during the entire TFT transfer process and restored to the original + * value after the transfer completes (whether successful or not). + * + * A value of 0 means no watchdog adjustment (default). + * + * @param timeout_ms Watchdog timeout in milliseconds. 0 = no adjustment. + */ + void set_tft_upload_watchdog_timeout(uint32_t timeout_ms) { this->tft_upload_watchdog_timeout_ = timeout_ms; } +#endif // USE_ESP32 + + /** + * @brief Set the number of HTTP retries for TFT upload requests. + * @param retries Number of retries. Defaults to 5. Range: 1-255. + */ + void set_tft_upload_http_retries(uint8_t retries) { this->tft_upload_http_retries_ = retries; } + /** * Set the tft file URL. */ @@ -1439,8 +1466,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe int tft_size_ = 0; uint32_t original_baud_rate_ = 0; bool upload_first_chunk_sent_ = false; + uint16_t tft_upload_http_timeout_{4500}; ///< HTTP timeout in ms (default: 4.5s) + uint8_t tft_upload_http_retries_{5}; ///< HTTP retry count (default: 5) #ifdef USE_ESP32 + uint32_t tft_upload_watchdog_timeout_{0}; ///< WDT timeout in ms (0 = no adjustment) + /** * will request 4096 bytes chunks from the web server * and send each to Nextion diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 46a04c1b2e..e03f1f470b 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -166,7 +166,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Define the configuration for the HTTP client ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; - http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + http_client.setTimeout(this->tft_upload_http_timeout_); bool begin_status = false; #ifdef USE_ESP8266 @@ -192,15 +192,15 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.collectHeaders(header_names, 1); ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); http_client.setReuse(true); - // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; int code = http_client.GET(); delay(100); // NOLINT App.feed_wdt(); - while (code != 200 && code != 206 && tries <= 5) { - ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/5", this->tft_url_.c_str(), - HTTPClient::errorToString(code).c_str(), tries); + while (code != 200 && code != 206 && tries <= this->tft_upload_http_retries_) { + ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/%u", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries, this->tft_upload_http_retries_); delay(250); // NOLINT App.feed_wdt(); diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 43f59a8d4b..1014c728a8 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -7,6 +7,7 @@ #include #include #include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -68,7 +69,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r int partial_read_len = 0; uint8_t retries = 0; // Attempt to read the chunk with retries. - while (retries < 5 && read_len < buffer_size) { + while (retries < this->tft_upload_http_retries_ && read_len < buffer_size) { partial_read_len = esp_http_client_read(http_client, reinterpret_cast(buffer) + read_len, buffer_size - read_len); if (partial_read_len > 0) { @@ -167,6 +168,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return false; } + // Temporarily adjust watchdog timeout for the duration of the TFT upload + watchdog::WatchdogManager wdm(this->tft_upload_watchdog_timeout_); + this->connection_state_.is_updating_ = true; if (exit_reparse) { @@ -190,7 +194,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { .url = this->tft_url_.c_str(), .cert_pem = nullptr, .method = HTTP_METHOD_HEAD, - .timeout_ms = 15000, + .timeout_ms = static_cast(this->tft_upload_http_timeout_), .disable_auto_redirect = false, .max_redirection_count = 10, }; diff --git a/tests/components/nextion/common_tft_upload.yaml b/tests/components/nextion/common_tft_upload.yaml new file mode 100644 index 0000000000..190abbc7b1 --- /dev/null +++ b/tests/components/nextion/common_tft_upload.yaml @@ -0,0 +1,5 @@ +display: + - id: !extend main_lcd + tft_url: http://esphome.io/default35.tft + tft_upload_http_timeout: 20s + tft_upload_http_retries: 10 diff --git a/tests/components/nextion/common_tft_upload_watchdog.yaml b/tests/components/nextion/common_tft_upload_watchdog.yaml new file mode 100644 index 0000000000..385fee359e --- /dev/null +++ b/tests/components/nextion/common_tft_upload_watchdog.yaml @@ -0,0 +1,3 @@ +display: + - id: !extend main_lcd + tft_upload_watchdog_timeout: 30s diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml index 7e94a9b4a5..a4f71628da 100644 --- a/tests/components/nextion/test.esp32-ard.yaml +++ b/tests/components/nextion/test.esp32-ard.yaml @@ -1,7 +1,5 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-ard.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml + tft_upload_watchdog: !include common_tft_upload_watchdog.yaml diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml index 99820f0f8d..259b71c9d0 100644 --- a/tests/components/nextion/test.esp32-idf.yaml +++ b/tests/components/nextion/test.esp32-idf.yaml @@ -1,7 +1,5 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-idf.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml + tft_upload_watchdog: !include common_tft_upload_watchdog.yaml diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml index 49f79b2f4c..a2b0e727cc 100644 --- a/tests/components/nextion/test.esp8266-ard.yaml +++ b/tests/components/nextion/test.esp8266-ard.yaml @@ -1,7 +1,4 @@ packages: uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml From 5b9cab02bee18da518c522fbdcf26b0b12fb17f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:37:54 -0400 Subject: [PATCH 076/101] [multiple] Add default initializers to uninitialized member variables (#14636) Co-authored-by: Claude Opus 4.6 --- esphome/components/duty_time/duty_time_sensor.h | 6 +++--- esphome/components/growatt_solar/growatt_solar.h | 4 ++-- esphome/components/hte501/hte501.h | 4 ++-- esphome/components/select/select_call.h | 2 +- esphome/components/uponor_smatrix/uponor_smatrix.h | 4 ++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 2 +- esphome/components/x9c/x9c.h | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index d9fb2a6d60..d21802ebb6 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -41,9 +41,9 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { sensor::Sensor *last_duty_time_sensor_{nullptr}; ESPPreferenceObject pref_; - uint32_t total_sec_; - uint32_t last_time_; - uint32_t edge_time_; + uint32_t total_sec_{0}; + uint32_t last_time_{0}; + uint32_t edge_time_{0}; bool last_state_{false}; bool restore_; }; diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index b0ddd4b99d..833d6a36dd 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -56,8 +56,8 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: - bool waiting_to_update_; - uint32_t last_send_; + bool waiting_to_update_{false}; + uint32_t last_send_{0}; struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index b47daf9157..7f29885f49 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -18,8 +18,8 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index c9abbc69a0..fbe7b82e92 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -43,7 +43,7 @@ class SelectCall { Select *const parent_; optional index_; SelectOperation operation_{SELECT_OP_NONE}; - bool cycle_; + bool cycle_{false}; }; } // namespace esphome::select diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index bd760f0d77..bd20e9b6a0 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -89,8 +89,8 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { std::vector rx_buffer_; std::queue> tx_queue_; - uint32_t last_rx_; - uint32_t last_tx_; + uint32_t last_rx_{0}; + uint32_t last_tx_{0}; #ifdef USE_TIME time::RealTimeClock *time_id_{nullptr}; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 8ef35a5f5d..7ade170c02 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -23,7 +23,7 @@ class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor const network::IPAddress &dns2) override; protected: - std::array ip_sensors_; + std::array ip_sensors_{}; }; class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h index 66c3df14e1..7dcd79bb7c 100644 --- a/esphome/components/x9c/x9c.h +++ b/esphome/components/x9c/x9c.h @@ -25,9 +25,9 @@ class X9cOutput : public output::FloatOutput, public Component { InternalGPIOPin *cs_pin_; InternalGPIOPin *inc_pin_; InternalGPIOPin *ud_pin_; - float initial_value_; - float pot_value_; - int step_delay_; + float initial_value_{0.0f}; + float pot_value_{0.0f}; + int step_delay_{0}; }; } // namespace x9c From 5d3893368d003a0904554e9170a1645f5c687efe Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:16:32 -0400 Subject: [PATCH 077/101] [multiple] Add array bounds checks (#14635) Co-authored-by: Claude Opus 4.6 --- .../addressable_light/addressable_light_display.cpp | 5 ++++- esphome/components/bme680_bsec/bme680_bsec.cpp | 2 +- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 1 + esphome/components/dac7678/dac7678_output.cpp | 2 ++ esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 5 +++-- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 6 ++++++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp index 16fab15b17..329620bcf0 100644 --- a/esphome/components/addressable_light/addressable_light_display.cpp +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col if (this->pixel_mapper_f_.has_value()) { // Params are passed by reference, so they may be modified in call. - this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; + int index = (*this->pixel_mapper_f_)(x, y); + if (index < 0 || static_cast(index) >= this->addressable_light_buffer_.size()) + return; + this->addressable_light_buffer_[index] = color; } else { this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 392d071b31..454be0c0fe 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -383,7 +383,7 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: case BSEC_OUTPUT_STATIC_IAQ: { - uint8_t accuracy = outputs[i].accuracy; + uint8_t accuracy = std::min(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1); this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 1a42c9d54b..0210d1e67d 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -438,6 +438,7 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou } } if (update_accuracy) { + max_accuracy = std::min(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1); #ifdef USE_SENSOR this->queue_push_( [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp index 83f8722e7f..27ab54f0be 100644 --- a/esphome/components/dac7678/dac7678_output.cpp +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -62,6 +62,8 @@ void DAC7678Output::register_channel(DAC7678Channel *channel) { } void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { + if (channel >= std::size(this->dac_input_reg_)) + return; if (this->dac_input_reg_[channel] != value) { ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 99d519b434..263603704a 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -452,7 +452,8 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da } break; case 0x83: - if (this->custom_presence_of_detection_sensor_ != nullptr) { + if (this->custom_presence_of_detection_sensor_ != nullptr && + data[FRAME_DATA_INDEX] < std::size(S_PRESENCE_OF_DETECTION_RANGE_STR)) { this->custom_presence_of_detection_sensor_->publish_state( S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]); } @@ -646,7 +647,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { #ifdef USE_BINARY_SENSOR case 0x01: case 0x81: - if (this->has_target_binary_sensor_ != nullptr) { + if (this->has_target_binary_sensor_ != nullptr && data[FRAME_DATA_INDEX] < std::size(S_SOMEONE_EXISTS_STR)) { this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]); } break; diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index c6527a948e..e24e9b338e 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -334,6 +334,8 @@ void MR60FDA2Component::process_frame_() { // Send Heartbeat Packet Command void MR60FDA2Component::set_install_height(uint8_t index) { + if (index >= std::size(INSTALL_HEIGHT)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x04, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00}; float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); @@ -345,6 +347,8 @@ void MR60FDA2Component::set_install_height(uint8_t index) { } void MR60FDA2Component::set_height_threshold(uint8_t index) { + if (index >= std::size(HEIGHT_THRESHOLD)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x08, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00}; float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); @@ -356,6 +360,8 @@ void MR60FDA2Component::set_height_threshold(uint8_t index) { } void MR60FDA2Component::set_sensitivity(uint8_t index) { + if (index >= std::size(SENSITIVITY)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x0A, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00}; int_to_bytes(SENSITIVITY[index], &send_data[8]); From 088a8a4338940c061fa181b55845a5d25c6268ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 17:23:58 -1000 Subject: [PATCH 078/101] [ci] Match symbols with changed signatures in memory impact analysis (#14600) Co-authored-by: Claude Opus 4.6 --- script/ci_memory_impact_comment.py | 75 ++++++++++++++ tests/unit_tests/analyze_memory/__init__.py | 0 .../test_ci_memory_impact_comment.py | 99 +++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 tests/unit_tests/analyze_memory/__init__.py create mode 100644 tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index a296130645..01316da27f 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -160,6 +160,76 @@ def format_change(before: int, after: int, threshold: float | None = None) -> st return f"{emoji} {delta_str} ({pct_str})" +def _sig_base(sym: str) -> str: + """Strip argument types from a symbol name for fuzzy matching. + + Removes the entire outermost parenthesized argument list (including + the parentheses) from the symbol string. + + This makes, for example, "foo(int)::nested" and "foo(float)::nested" + share the same key "foo::nested", while "foo(int)" maps to "foo" and + therefore does NOT collide with "foo(int)::nested". + """ + start = sym.find("(") + if start == -1: + return sym + end = sym.rfind(")") + if end == -1: + return sym + return sym[:start] + sym[end + 1 :] + + +_AMBIGUOUS = object() + + +def _match_signature_changes( + changed_symbols: list[tuple[str, int, int, int]], + new_symbols: list[tuple[str, int]], + removed_symbols: list[tuple[str, int]], +) -> tuple[ + list[tuple[str, int, int, int]], + list[tuple[str, int]], + list[tuple[str, int]], +]: + """Match new/removed symbol pairs that only differ in argument types. + + When a function's argument types change (e.g. foo(vector<>&) -> foo(Buffer&)), + it appears as a new + removed symbol. This matches them by base name and moves + them to changed_symbols. Only matches unambiguous 1:1 pairs. + """ + if not new_symbols or not removed_symbols: + return changed_symbols, new_symbols, removed_symbols + + # Build base -> entry maps; mark ambiguous bases with sentinel + new_by_base: dict[str, tuple[str, int] | object] = {} + for entry in new_symbols: + base = _sig_base(entry[0]) + new_by_base[base] = _AMBIGUOUS if base in new_by_base else entry + removed_by_base: dict[str, tuple[str, int] | object] = {} + for entry in removed_symbols: + base = _sig_base(entry[0]) + removed_by_base[base] = _AMBIGUOUS if base in removed_by_base else entry + + matched: set[str] = set() # matched base keys + for base, new_entry in new_by_base.items(): + if new_entry is _AMBIGUOUS: + continue + rem_entry = removed_by_base.get(base) + if rem_entry is None or rem_entry is _AMBIGUOUS: + continue + pr_sym, pr_size = new_entry + _rm_sym, target_size = rem_entry + delta = pr_size - target_size + if delta != 0: + changed_symbols.append((pr_sym, target_size, pr_size, delta)) + matched.add(base) + + if matched: + new_symbols = [e for e in new_symbols if _sig_base(e[0]) not in matched] + removed_symbols = [e for e in removed_symbols if _sig_base(e[0]) not in matched] + return changed_symbols, new_symbols, removed_symbols + + def prepare_symbol_changes_data( target_symbols: dict | None, pr_symbols: dict | None ) -> dict | None: @@ -200,6 +270,11 @@ def prepare_symbol_changes_data( delta = pr_size - target_size changed_symbols.append((symbol, target_size, pr_size, delta)) + # Match new/removed symbols that only differ in argument types + changed_symbols, new_symbols, removed_symbols = _match_signature_changes( + changed_symbols, new_symbols, removed_symbols + ) + if not changed_symbols and not new_symbols and not removed_symbols: return None diff --git a/tests/unit_tests/analyze_memory/__init__.py b/tests/unit_tests/analyze_memory/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py b/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py new file mode 100644 index 0000000000..8399ac0303 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py @@ -0,0 +1,99 @@ +"""Tests for script/ci_memory_impact_comment.py symbol matching.""" + +from pathlib import Path +import sys + +# Add script directory to path so we can import the module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "script")) + +from ci_memory_impact_comment import prepare_symbol_changes_data # noqa: E402 + + +def test_prepare_symbol_changes_signature_match() -> None: + """Symbols with same base name but different args are matched as changed.""" + target = { + "Foo::bar(std::vector&, int)": 300, + "unchanged()": 50, + } + pr = { + "Foo::bar(ProtoByteBuffer&, int)": 320, + "unchanged()": 50, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 1 + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(ProtoByteBuffer&, int)" + assert t_size == 300 + assert p_size == 320 + assert delta == 20 + + +def test_prepare_symbol_changes_ambiguous_overloads_not_matched() -> None: + """Multiple overloads with same base name stay as new/removed.""" + target = { + "Foo::bar(int)": 100, + "Foo::bar(float)": 200, + } + pr = { + "Foo::bar(double)": 150, + "Foo::bar(long)": 250, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 0 + assert len(result["new_symbols"]) == 2 + assert len(result["removed_symbols"]) == 2 + + +def test_prepare_symbol_changes_no_parens_not_matched() -> None: + """Symbols without parens (variables) are not fuzzy-matched.""" + target = {"my_global_var": 100} + pr = {"my_global_var_v2": 120} + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 0 + assert len(result["new_symbols"]) == 1 + assert len(result["removed_symbols"]) == 1 + + +def test_prepare_symbol_changes_nested_symbols_matched_separately() -> None: + """Nested symbols like ::__pstr__ don't collide with parent function.""" + target = { + "Foo::bar(std::vector&, int)": 300, + "Foo::bar(std::vector&, int)::__pstr__": 19, + } + pr = { + "Foo::bar(ProtoByteBuffer&, int)": 320, + "Foo::bar(ProtoByteBuffer&, int)::__pstr__": 19, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + # Both the function and its nested __pstr__ should be matched (not new/removed) + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + # __pstr__ has delta=0 so it's silently dropped, only the function shows + assert len(result["changed_symbols"]) == 1 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(ProtoByteBuffer&, int)" + assert delta == 20 + + +def test_prepare_symbol_changes_exact_match_preferred() -> None: + """Exact name matches are found before fuzzy matching runs.""" + target = { + "Foo::bar(int)": 100, + } + pr = { + "Foo::bar(int)": 120, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 1 + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(int)" + assert delta == 20 From f3ca86b67017991a13a1cb27b242b373e64ed29e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:48:03 +1100 Subject: [PATCH 079/101] [ci-custom] Directions on constant hoisting (#14637) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 8e1652b505..06fcdadb8c 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -519,7 +519,7 @@ def lint_constants_usage(): continue errs.append( f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " - f"constant to const.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " + f"constant to esphome/components/const/__init__.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " "See https://developers.esphome.io/contributing/code/#python" ) return errs From 0db9137d9101a04c4b2a8836e13308de60280c35 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:10:48 -0400 Subject: [PATCH 080/101] [multiple] Add division by zero guards (#14634) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/bl0942/bl0942.cpp | 2 +- esphome/components/combination/combination.cpp | 11 ++++++++++- esphome/components/graph/graph.cpp | 2 +- esphome/components/mics_4514/mics_4514.cpp | 6 ++++++ esphome/components/tsl2561/tsl2561.cpp | 4 ++++ esphome/components/ufire_ec/ufire_ec.cpp | 14 +++++++++++--- esphome/components/ufire_ec/ufire_ec.h | 2 +- esphome/components/xxtea/xxtea.cpp | 4 ++++ 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 16ad33141d..7d38597423 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -173,7 +173,7 @@ void BL0942::received_package_(DataPacket *data) { float i_rms = (uint24_t) data->i_rms / current_reference_; float watt = (int24_t) data->watt / power_reference_; float total_energy_consumption = cf_cnt / energy_reference_; - float frequency = 1000000.0f / data->frequency; + float frequency = data->frequency != 0 ? 1000000.0f / data->frequency : NAN; if (voltage_sensor_ != nullptr) { voltage_sensor_->publish_state(v_rms); diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index ece7cca482..2f0bd26a02 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -163,7 +163,7 @@ void MeanCombinationComponent::handle_new_value(float value) { return; float sum = 0.0; - size_t count = 0.0; + size_t count = 0; for (const auto &sensor : this->sensors_) { if (std::isfinite(sensor->state)) { @@ -172,6 +172,10 @@ void MeanCombinationComponent::handle_new_value(float value) { } } + if (count == 0) { + this->publish_state(NAN); + return; + } float mean = sum / count; this->publish_state(mean); @@ -238,6 +242,11 @@ void RangeCombinationComponent::handle_new_value(float value) { } } + if (sensor_states.empty()) { + this->publish_state(NAN); + return; + } + sort(sensor_states.begin(), sensor_states.end()); float range = sensor_states.back() - sensor_states.front(); diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c43cd07fe0..801c97e3f5 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -171,7 +171,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo bool prev_b = false; int16_t prev_y = 0; for (uint32_t i = 0; i < this->width_; i++) { - float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; + float v = yrange != 0 ? (trace->get_tracedata()->get_value(i) - ymin) / yrange : NAN; if (!std::isnan(v) && (thick > 0)) { int16_t x = this->width_ - 1 - i + x_offset; uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 60413b32d7..ce63a7d062 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -59,6 +59,12 @@ void MICS4514Component::update() { return; } + if (this->red_calibration_ == 0 || this->ox_calibration_ == 0) { + ESP_LOGW(TAG, "Calibration values are zero, retrying"); + this->status_set_warning(); + this->initial_ = true; + return; + } float red_f = (float) (power - red) / this->red_calibration_; float ox_f = (float) (power - ox) / this->ox_calibration_; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index cb4c38a83c..bccff1fb26 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -70,6 +70,10 @@ float TSL2561Sensor::calculate_lx_(uint16_t ch0, uint16_t ch1) { return NAN; } + if (ch0 == 0) { + ESP_LOGVV(TAG, "No light detected"); + return 0.0f; + } float d0 = ch0, d1 = ch1; float ratio = d1 / d0; diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index a1c3568a1a..40e3be2757 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -1,5 +1,6 @@ #include "esphome/core/log.h" #include "ufire_ec.h" +#include namespace esphome { namespace ufire_ec { @@ -60,9 +61,15 @@ float UFireECComponent::measure_temperature_() { return this->read_data_(REGISTE float UFireECComponent::measure_ms_() { return this->read_data_(REGISTER_MS); } -void UFireECComponent::set_solution_(float solution, float temperature) { - solution /= (1 - (this->temperature_coefficient_ * (temperature - 25))); +bool UFireECComponent::set_solution_(float solution, float temperature) { + float denom = 1 - (this->temperature_coefficient_ * (temperature - 25)); + if (std::abs(denom) < 1e-6f) { + ESP_LOGE(TAG, "Temperature compensation denominator is zero"); + return false; + } + solution /= denom; this->write_data_(REGISTER_SOLUTION, solution); + return true; } void UFireECComponent::set_compensation_(float temperature) { this->write_data_(REGISTER_COMPENSATION, temperature); } @@ -72,7 +79,8 @@ void UFireECComponent::set_coefficient_(float coefficient) { this->write_data_(R void UFireECComponent::set_temperature_(float temperature) { this->write_data_(REGISTER_TEMP, temperature); } void UFireECComponent::calibrate_probe(float solution, float temperature) { - this->set_solution_(solution, temperature); + if (!this->set_solution_(solution, temperature)) + return; this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_PROBE); } diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index bfbed1b43e..8a648b5038 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -44,7 +44,7 @@ class UFireECComponent : public PollingComponent, public i2c::I2CDevice { protected: float measure_temperature_(); float measure_ms_(); - void set_solution_(float solution, float temperature); + bool set_solution_(float solution, float temperature); void set_compensation_(float temperature); void set_coefficient_(float coefficient); void set_temperature_(float temperature); diff --git a/esphome/components/xxtea/xxtea.cpp b/esphome/components/xxtea/xxtea.cpp index aae663ee01..ba17530b24 100644 --- a/esphome/components/xxtea/xxtea.cpp +++ b/esphome/components/xxtea/xxtea.cpp @@ -7,6 +7,8 @@ static const uint32_t DELTA = 0x9e3779b9; #define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) void encrypt(uint32_t *v, size_t n, const uint32_t *k) { + if (n == 0) + return; uint32_t z, y, sum, e; size_t p; size_t q = 6 + 52 / n; @@ -25,6 +27,8 @@ void encrypt(uint32_t *v, size_t n, const uint32_t *k) { } void decrypt(uint32_t *v, size_t n, const uint32_t *k) { + if (n == 0) + return; uint32_t z, y, sum, e; size_t p; size_t q = 6 + 52 / n; From 31f4b4d00d5242979ca83a98acc3b60b0bf0c84b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:33:08 -0400 Subject: [PATCH 081/101] [multiple] Fix undefined behavior across components (#14639) Co-authored-by: Claude Opus 4.6 --- esphome/components/e131/e131_packet.cpp | 3 ++- .../components/globals/globals_component.h | 2 +- .../grove_tb6612fng/grove_tb6612fng.cpp | 3 ++- esphome/components/midea/appliance_base.h | 6 +++-- esphome/components/nextion/nextion.cpp | 12 +++------ esphome/components/ruuvi_ble/ruuvi_ble.cpp | 26 +++++++++---------- .../components/tormatic/tormatic_protocol.h | 2 +- 7 files changed, 26 insertions(+), 28 deletions(-) diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index b90e6d5c91..600793f5d3 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,3 +1,4 @@ +#include #include #include "e131.h" #ifdef USE_NETWORK @@ -57,7 +58,7 @@ union E131RawPacket { // We need to have at least one `1` value // Get the offset of `property_values[1]` -const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); +const size_t E131_MIN_PACKET_SIZE = offsetof(E131RawPacket, property_values) + sizeof(uint8_t); bool E131Component::join_igmp_groups_() { if (this->listen_method_ != E131_MULTICAST) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 3db29bea35..520c068e6f 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -84,7 +84,7 @@ template class RestoringGlobalStringComponent : public P this->rtc_ = global_preferences->make_preference(1944399030U ^ this->name_hash_); bool hasdata = this->rtc_.load(&temp); if (hasdata) { - this->value_.assign(temp + 1, temp[0]); + this->value_.assign(temp + 1, static_cast(temp[0])); } this->last_checked_value_.assign(this->value_); } diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index 428c8ec4a8..c10fa4cf25 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -139,7 +139,8 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, } void GroveMotorDriveTB6612FNG::stepper_stop() { - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, &status, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Send stop stepper failed!"); this->status_set_warning(); return; diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 060cbd996b..660d185b49 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -28,12 +28,14 @@ class UARTStream : public Stream { int available() override { return this->uart_->available(); } int read() override { uint8_t data; - this->uart_->read_byte(&data); + if (!this->uart_->read_byte(&data)) + return -1; return data; } int peek() override { uint8_t data; - this->uart_->peek_byte(&data); + if (!this->uart_->peek_byte(&data)) + return -1; return data; } size_t write(uint8_t data) override { diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index cb20c34005..7ae4d50fc8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -651,11 +651,7 @@ void Nextion::process_nextion_commands_() { break; } - int value = 0; - - for (int i = 0; i < 4; ++i) { - value += to_process[i] << (8 * i); - } + int value = static_cast(encode_uint32(to_process[3], to_process[2], to_process[1], to_process[0])); NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { @@ -751,10 +747,8 @@ void Nextion::process_nextion_commands_() { index = to_process.find('\0'); variable_name = to_process.substr(0, index); // // Get variable name - int value = 0; - for (int i = 0; i < 4; ++i) { - value += to_process[i + index + 1] << (8 * i); - } + int value = static_cast( + encode_uint32(to_process[index + 4], to_process[index + 3], to_process[index + 2], to_process[index + 1])); ESP_LOGN(TAG, "Sensor: %s=%d", variable_name.c_str(), value); diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index bf088873ce..07f870b60c 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -21,11 +21,11 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP const float temperature = temp_sign == 0 ? temp_val : -1 * temp_val; const float humidity = data[0] * 0.5f; - const float pressure = (uint16_t(data[3] << 8) + uint16_t(data[4]) + 50000.0f) / 100.0f; - const float acceleration_x = (int16_t(data[5] << 8) + int16_t(data[6])) / 1000.0f; - const float acceleration_y = (int16_t(data[7] << 8) + int16_t(data[8])) / 1000.0f; - const float acceleration_z = (int16_t(data[9] << 8) + int16_t(data[10])) / 1000.0f; - const float battery_voltage = (uint16_t(data[11] << 8) + uint16_t(data[12])) / 1000.0f; + const float pressure = (encode_uint16(data[3], data[4]) + 50000.0f) / 100.0f; + const float acceleration_x = static_cast(encode_uint16(data[5], data[6])) / 1000.0f; + const float acceleration_y = static_cast(encode_uint16(data[7], data[8])) / 1000.0f; + const float acceleration_z = static_cast(encode_uint16(data[9], data[10])) / 1000.0f; + const float battery_voltage = encode_uint16(data[11], data[12]) / 1000.0f; result.humidity = humidity; result.temperature = temperature; @@ -43,19 +43,19 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP if (adv_data.size() != 24) return false; - const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; - const float humidity = (uint16_t(data[2] << 8) | uint16_t(data[3])) / 400.0f; - const float pressure = ((uint16_t(data[4] << 8) | uint16_t(data[5])) + 50000.0f) / 100.0f; - const float acceleration_x = (int16_t(data[6] << 8) + int16_t(data[7])) / 1000.0f; - const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; - const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; + const float temperature = static_cast(encode_uint16(data[0], data[1])) * 0.005f; + const float humidity = encode_uint16(data[2], data[3]) / 400.0f; + const float pressure = (encode_uint16(data[4], data[5]) + 50000.0f) / 100.0f; + const float acceleration_x = static_cast(encode_uint16(data[6], data[7])) / 1000.0f; + const float acceleration_y = static_cast(encode_uint16(data[8], data[9])) / 1000.0f; + const float acceleration_z = static_cast(encode_uint16(data[10], data[11])) / 1000.0f; - const uint16_t power_info = (uint16_t(data[12] << 8) | data[13]); + const uint16_t power_info = encode_uint16(data[12], data[13]); const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; const float movement_counter = float(data[14]); - const float measurement_sequence_number = float(uint16_t(data[15] << 8) | uint16_t(data[16])); + const float measurement_sequence_number = float(encode_uint16(data[15], data[16])); result.temperature = data[0] == 0x7F && data[1] == 0xFF ? NAN : temperature; result.humidity = data[2] == 0xFF && data[3] == 0xFF ? NAN : humidity; diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index 057713b884..26a634b630 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -99,7 +99,7 @@ struct MessageHeader { // payload_size returns the amount of payload bytes to be read from the uart // buffer after reading the header. - uint32_t payload_size() { return this->len - sizeof(this->type); } + uint32_t payload_size() { return this->len > sizeof(this->type) ? this->len - sizeof(this->type) : 0; } } __attribute__((packed)); // StatusType denotes which 'page' of information needs to be retrieved. From 019db745828289f5d105475a816f6dc1cabd814d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:44:27 +0000 Subject: [PATCH 082/101] Bump setuptools from 82.0.0 to 82.0.1 (#14665) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6a2c22a5d..2e3a247768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"] +requires = ["setuptools==82.0.1", "wheel>=0.43,<0.47"] build-backend = "setuptools.build_meta" [project] From a379e5a6357340a696d9cbc67a8ad48a0bad0924 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:16:29 -0400 Subject: [PATCH 083/101] [runtime_image][st7701s] Fix BMP decoder and LCD init bugs (#14663) Co-authored-by: Claude Opus 4.6 --- .../components/runtime_image/bmp_decoder.cpp | 18 +++++++++++++++--- esphome/components/st7701s/st7701s.cpp | 6 ++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 1a56484c60..7003f4da2f 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -63,7 +63,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { switch (this->bits_per_pixel_) { case 1: - this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); + this->width_bytes_ = (this->width_ + 7) / 8; + this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; break; case 24: this->width_bytes_ = this->width_ * 3; @@ -92,15 +93,26 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { case 1: { while (index < size) { uint8_t current_byte = buffer[index]; + bool end_of_row = false; for (uint8_t i = 0; i < 8; i++) { - size_t x = (this->paint_index_ % static_cast(this->width_)) + i; + size_t x = this->paint_index_ % static_cast(this->width_); size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; this->draw(x, y, 1, 1, c); + this->paint_index_++; + // End of pixel row: skip remaining bits in this byte + if (x + 1 >= static_cast(this->width_)) { + end_of_row = true; + break; + } } - this->paint_index_ += 8; this->current_index_++; index++; + // End of pixel row: skip row padding bytes (4-byte alignment) + if (end_of_row && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } } break; } diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 221fe39b9d..ecce4eb4b2 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -36,11 +36,13 @@ void ST7701S::setup() { config.de_gpio_num = this->de_pin_->get_pin(); config.pclk_gpio_num = this->pclk_pin_->get_pin(); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); - ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); - ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); if (err != ESP_OK) { esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; } + ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); } void ST7701S::loop() { From 75f55adbfa0cd1a75c9e33a16cc6a95d7195c50c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:17:31 -0400 Subject: [PATCH 084/101] [api][at581x][vl53l0x] Fix bounds check issues in 3 components (#14660) Co-authored-by: Claude Opus 4.6 --- esphome/components/api/api_frame_helper_noise.cpp | 2 ++ esphome/components/at581x/at581x.cpp | 5 +++++ esphome/components/vl53l0x/vl53l0x_sensor.cpp | 4 ++-- esphome/components/vl53l0x/vl53l0x_sensor.h | 3 +-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 62523fb835..256357ce6a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -375,6 +375,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso #ifdef USE_STORE_LOG_STR_IN_FLASH // On ESP8266 with flash strings, we need to use PROGMEM-aware functions size_t reason_len = strlen_P(reinterpret_cast(reason)); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { memcpy_P(data + 1, reinterpret_cast(reason), reason_len); } @@ -382,6 +383,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso // Normal memory access const char *reason_str = LOG_STR_ARG(reason); size_t reason_len = strlen(reason_str); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string std::memcpy(data + 1, reason_str, reason_len); diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 728fbe20c6..6fc85b0790 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() { } // Set gain + if (this->gain_ < 0 || static_cast(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) || + static_cast(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) { + ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_); + return false; + } if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) || !this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) { ESP_LOGE(TAG, "Failed to write AT581X gain registers"); diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index e833657fc4..0b2b40d723 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -87,9 +87,9 @@ void VL53L0XSensor::setup() { reg(0x94) = 0x6B; reg(0x83) = 0x00; - this->timeout_start_us_ = micros(); + uint32_t timeout_start_us = micros(); while (reg(0x83).get() == 0x00) { - if (this->timeout_us_ > 0 && ((uint16_t) (micros() - this->timeout_start_us_) > this->timeout_us_)) { + if (this->timeout_us_ > 0 && (micros() - timeout_start_us > this->timeout_us_)) { ESP_LOGE(TAG, "'%s' - setup timeout", this->name_.c_str()); this->mark_failed(); return; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 2bf90015fe..f533005b5b 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -64,8 +64,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c bool waiting_for_interrupt_{false}; uint8_t stop_variable_; - uint16_t timeout_start_us_; - uint16_t timeout_us_{}; + uint32_t timeout_us_{}; static std::list vl53_sensors; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool enable_pin_setup_complete; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From b721cd48e5d8d41ad12eedf9e4ec941483cd99c3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:07 -0400 Subject: [PATCH 085/101] [hmc5883l][mmc5603][honeywellabp2][xgzp68xx][max9611] Fix uninitialized members (#14659) Co-authored-by: Claude Opus 4.6 --- esphome/components/hmc5883l/hmc5883l.h | 2 +- esphome/components/honeywellabp2_i2c/honeywellabp2.h | 4 ++-- esphome/components/max9611/sensor.py | 4 +++- esphome/components/mmc5603/mmc5603.h | 4 ++-- esphome/components/xgzp68xx/sensor.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 8eae0f7a50..b5cf93e62b 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -61,7 +61,7 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index 274de847ac..d29ebb855d 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -45,8 +45,8 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333) const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC) - float max_count_; - float min_count_; + float max_count_{max_count_a_}; + float min_count_{min_count_a_}; bool measurement_running_ = false; uint8_t raw_data_[7]; // holds output data diff --git a/esphome/components/max9611/sensor.py b/esphome/components/max9611/sensor.py index 8405a3f75a..b3a73d8c10 100644 --- a/esphome/components/max9611/sensor.py +++ b/esphome/components/max9611/sensor.py @@ -35,7 +35,9 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MAX9611Component), - cv.Required(CONF_SHUNT_RESISTANCE): cv.resistance, + cv.Required(CONF_SHUNT_RESISTANCE): cv.All( + cv.resistance, cv.Range(min=1e-6) + ), cv.Required(CONF_GAIN): cv.enum(MAX9611_GAIN, upper=True), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index f827e27e04..9a77b78bc1 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -27,7 +27,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; } protected: - MMC5603Datarate datarate_; + MMC5603Datarate datarate_{MMC5603_DATARATE_75_0_HZ}; sensor::Sensor *x_sensor_{nullptr}; sensor::Sensor *y_sensor_{nullptr}; sensor::Sensor *z_sensor_{nullptr}; @@ -37,7 +37,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; }; } // namespace mmc5603 diff --git a/esphome/components/xgzp68xx/sensor.py b/esphome/components/xgzp68xx/sensor.py index 2b38392a02..6b83012eb4 100644 --- a/esphome/components/xgzp68xx/sensor.py +++ b/esphome/components/xgzp68xx/sensor.py @@ -56,7 +56,7 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_K_VALUE, default=4096): cv.uint16_t, + cv.Optional(CONF_K_VALUE, default=4096): cv.int_range(min=1, max=65535), } ) .extend(cv.polling_component_schema("60s")) From 08a0608a48965edfd7ef11301d2e38faefe2f5ab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:21 -0400 Subject: [PATCH 086/101] [wifi][captive_portal][heatpumpir][es8388] Fix wrong behavior in 4 components (#14657) Co-authored-by: Claude Opus 4.6 --- .../captive_portal/dns_server_esp32_idf.cpp | 5 +++-- esphome/components/es8388/es8388.cpp | 4 ++-- esphome/components/heatpumpir/heatpumpir.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 20 ++++--------------- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index bd9989a40c..7b75f04241 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "captive_portal.dns"; // DNS constants static constexpr uint16_t DNS_PORT = 53; static constexpr uint16_t DNS_QR_FLAG = 1 << 15; +static constexpr uint16_t DNS_AA_FLAG = 1 << 10; static constexpr uint16_t DNS_OPCODE_MASK = 0x7800; static constexpr uint16_t DNS_QTYPE_A = 0x0001; static constexpr uint16_t DNS_QCLASS_IN = 0x0001; @@ -162,8 +163,8 @@ void DNSServer::process_next_request() { } // Build DNS response by modifying the request in-place - header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative - header->an_count = htons(1); // One answer + header->flags = htons(DNS_QR_FLAG | DNS_AA_FLAG); // Response + Authoritative + header->an_count = htons(1); // One answer // Add answer section after the question size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader); diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 72026a2a84..c252cdb707 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -152,7 +152,7 @@ void ES8388::dump_config() { bool ES8388::set_volume(float volume) { volume = clamp(volume, 0.0f, 1.0f); - uint8_t value = remap(volume, 0.0f, 1.0f, -96, 0); + uint8_t value = remap(volume, 0.0f, 1.0f, 192, 0); ESP_LOGD(TAG, "Setting ES8388_DACCONTROL4 / ES8388_DACCONTROL5 to 0x%02X (volume: %f)", value, volume); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL4, value)); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL5, value)); @@ -163,7 +163,7 @@ bool ES8388::set_volume(float volume) { float ES8388::volume() { uint8_t value; ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL4, &value)); - return remap(value, -96, 0, 0.0f, 1.0f); + return remap(value, 192, 0, 0.0f, 1.0f); } bool ES8388::set_mute_state_(bool mute_state) { diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 6b73a24dc4..11e7672dc1 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -114,7 +114,7 @@ void HeatpumpIRClimate::setup() { this->current_temperature = state; IRSenderESPHome esp_sender(this->transmitter_); - this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature + 0.5))); + this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature))); // current temperature changed, publish state this->publish_state(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a9b26c5935..0bf7934878 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -638,8 +638,6 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { - static bool first_scan = false; - // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -656,23 +654,13 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; - if (first_scan) { - if (passive) { - config.scan_time.passive = 200; - } else { - config.scan_time.active.min = 100; - config.scan_time.active.max = 200; - } + if (passive) { + config.scan_time.passive = 500; } else { - if (passive) { - config.scan_time.passive = 500; - } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; - } + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; } #endif - first_scan = false; bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { ESP_LOGV(TAG, "wifi_station_scan failed"); From 9418f35cc32e0a6216b09b813664aa40bcc3e216 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:44 -0400 Subject: [PATCH 087/101] [multiple] Remove unnecessary heap allocations in 4 components (#14656) Co-authored-by: Claude Opus 4.6 --- esphome/components/daikin_arc/daikin_arc.cpp | 9 +++++---- esphome/components/pn7150_i2c/pn7150_i2c.cpp | 3 ++- esphome/components/pn7160_i2c/pn7160_i2c.cpp | 3 ++- esphome/components/toshiba/toshiba.cpp | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index c45fa307a7..adb7b9fec7 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -350,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; size_t bytes_count = data.size() / 2 / 8; - size_t buf_size = bytes_count * 3 + 1; - std::unique_ptr buf(new char[buf_size]()); // value-initialize (zero-fill) + // Header (20) + state (19) = 39 bytes max; truncates gracefully via buf_append_printf + char buf[40 * 3 + 1] = {}; + constexpr size_t buf_size = sizeof(buf); size_t buf_pos = 0; for (size_t i = 0; i < bytes_count; i++) { uint8_t byte = 0; @@ -363,9 +364,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { break; } } - buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte); + buf_pos = buf_append_printf(buf, buf_size, buf_pos, "%02x ", byte); } - ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); + ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf, data.size()); } if (!valid_daikin_frame) { char sbuf[16 * 10 + 1] = {0}; diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp index 38b3102b37..4ae884595b 100644 --- a/esphome/components/pn7150_i2c/pn7150_i2c.cpp +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -34,7 +34,8 @@ uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { } uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { - if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + auto encoded = tx.encode(); + if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) { return nfc::STATUS_OK; } return nfc::STATUS_FAILED; diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp index 7c6da9dd06..e33c6c793d 100644 --- a/esphome/components/pn7160_i2c/pn7160_i2c.cpp +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -34,7 +34,8 @@ uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { } uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { - if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + auto encoded = tx.encode(); + if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) { return nfc::STATUS_OK; } return nfc::STATUS_FAILED; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index e0c150537a..53114cc50f 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -951,10 +951,10 @@ void ToshibaClimate::transmit_ras_2819t_() { } uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) { - const std::vector header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, - RAC_PT1411HWRU_SWING_HEADER}; + static constexpr uint8_t HEADERS[] = {RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, + RAC_PT1411HWRU_SWING_HEADER}; - for (auto i : header) { + for (auto i : HEADERS) { if ((message[0] == i) && (message[1] == static_cast(~i))) return i; } From fecedeb01833d01cbc26ad331597a554e7904086 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:20:09 -0400 Subject: [PATCH 088/101] [multiple] Fix crashes from malformed external input (batch 2) (#14651) Co-authored-by: Claude Opus 4.6 --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 5 ++++ .../modbus_controller/modbus_controller.cpp | 23 +++++++++++++++++++ .../nextion/nextion_upload_arduino.cpp | 6 +++++ .../nextion/nextion_upload_esp32.cpp | 6 +++++ .../seeed_mr60bha2/seeed_mr60bha2.cpp | 2 +- .../components/usb_host/usb_host_client.cpp | 5 ++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 73a298d279..0e2a515b40 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -514,6 +514,11 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { continue; // Possible zero padded advertisement data } + // Validate field fits in remaining payload + if (offset + field_length > len) { + break; + } + // first byte of adv record is adv record type const uint8_t record_type = payload[offset++]; const uint8_t *record = &payload[offset]; diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 7f0eb230e0..f77f51a20d 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -59,6 +59,10 @@ bool ModbusController::send_next_command_() { // Queue incoming response void ModbusController::on_modbus_data(const std::vector &data) { + if (this->command_queue_.empty()) { + ESP_LOGW(TAG, "Received modbus data but command queue is empty"); + return; + } auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { if (this->module_offline_) { @@ -92,6 +96,9 @@ void ModbusController::process_modbus_data_(const ModbusCommandItem *response) { void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) { ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code); + if (this->command_queue_.empty()) { + return; + } // Remove pending command waiting for a response auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { @@ -175,6 +182,11 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st uint16_t payload_offset; if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + if (data.size() < 5) { + ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); @@ -188,8 +200,19 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } + if (data.size() < 5 + payload_size) { + ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(), + 5 + payload_size); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } payload_offset = 5; } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { + if (data.size() < 4) { + ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } number_of_registers = 1; payload_offset = 2; } else { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index e03f1f470b..6c454ab745 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -86,6 +86,12 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); upload_first_chunk_sent_ = true; + if (recv_string.empty()) { + ESP_LOGW(TAG, "No response from display during upload"); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD( diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 1014c728a8..166bbcc86a 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -108,6 +108,12 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r static_cast(esp_get_free_heap_size())); #endif upload_first_chunk_sent_ = true; + if (recv_string.empty()) { + ESP_LOGW(TAG, "No response from display during upload"); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD( diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 8628faac5a..1b5eaf6367 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -199,7 +199,7 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } break; case DISTANCE_TYPE_BUFFER: - if (data[0] != 0) { + if (length >= 1 && data[0] != 0) { if (this->distance_sensor_ != nullptr && length >= 8) { uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]); float distance_float; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index c77d738ace..2a460d1a07 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -492,6 +492,11 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u ESP_LOGE(TAG, "Too many requests queued"); return false; } + if (length > trq->transfer->data_buffer_size) { + ESP_LOGE(TAG, "transfer_in: data length %u exceeds buffer size %u", length, trq->transfer->data_buffer_size); + this->release_trq(trq); + return false; + } trq->callback = callback; trq->transfer->callback = transfer_callback; trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN; From 7c1b9f0cb4b7862764ace94d4ccb2119b1024419 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:22:06 -0400 Subject: [PATCH 089/101] [multiple] Fix wrong behavior in 5 components (#14647) Co-authored-by: Claude Opus 4.6 --- esphome/components/anova/anova.cpp | 9 ++++++--- esphome/components/binary_sensor/filter.cpp | 1 - esphome/components/bl0906/bl0906.cpp | 8 +++----- .../components/esp32_ble_tracker/esp32_ble_tracker.cpp | 2 +- esphome/components/ledc/ledc_output.cpp | 3 +++ 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index b625f92115..f21230b075 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -144,9 +144,12 @@ void Anova::update() { return; if (this->current_request_ < 2) { - auto *pkt = this->codec_->get_read_device_status_request(); - if (this->current_request_ == 0) - this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + AnovaPacket *pkt; + if (this->current_request_ == 0) { + pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + } else { + pkt = this->codec_->get_read_device_status_request(); + } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 25a69c413a..5d525e967d 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -136,7 +136,6 @@ optional SettleFilter::new_value(bool value) { return {}; } else { this->steady_ = false; - this->output(value); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index 7b643bba98..dcae4a2591 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -190,11 +190,9 @@ void BL0906::bias_correction_(uint8_t address, float measurements, float correct float i_rms0 = measurements * ki; float i_rms = correction * ki; int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; - data.l = value << 24 >> 24; - data.m = value << 16 >> 24; - if (value < 0) { - data.h = (value << 8 >> 24) | 0b10000000; - } + data.l = value & 0xFF; + data.m = (value >> 8) & 0xFF; + data.h = (value >> 16) & 0xFF; data.address = bl0906_checksum(address, &data); ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); this->write_byte(BL0906_WRITE_COMMAND); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0e2a515b40..5a43cf7e49 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -549,7 +549,7 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { // CSS 1.5 TX POWER LEVEL // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." // CSS 1: Optional in this context (may appear more than once in a block). - this->tx_powers_.push_back(*payload); + this->tx_powers_.push_back(*record); break; } case ESP_BLE_AD_TYPE_APPEARANCE: { diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 21e0682257..763de851da 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -76,6 +76,9 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n init_result = ledc_timer_config(&timer_conf); if (init_result != ESP_OK) { ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth); + if (bit_depth <= 1) { + break; + } // try again with a lower bit depth timer_conf.duty_resolution = static_cast(--bit_depth); } From 9902447834e8b997bf8831b569484f0c164c8f33 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:51:50 -0400 Subject: [PATCH 090/101] [multiple] Fix minor bugs in 8 components (#14650) Co-authored-by: Claude Opus 4.6 --- esphome/components/bl0906/bl0906.cpp | 35 ++++++++++--------- esphome/components/bmi160/bmi160.cpp | 3 +- .../components/esp32_camera/esp32_camera.cpp | 7 ++++ esphome/components/ld2450/ld2450.cpp | 2 +- esphome/components/ld2450/ld2450.h | 2 +- .../light/addressable_light_effect.h | 2 ++ .../mopeka_std_check/mopeka_std_check.cpp | 18 +++++----- .../mopeka_std_check/mopeka_std_check.h | 2 +- esphome/components/rtttl/rtttl.cpp | 13 ++++--- 9 files changed, 49 insertions(+), 35 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index dcae4a2591..70db235a37 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -138,23 +138,24 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se this->write_byte(BL0906_READ_COMMAND); this->write_byte(address); - if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { - if (bl0906_checksum(address, &buffer) == buffer.checksum) { - if (signed_result) { - data_s24.l = buffer.l; - data_s24.m = buffer.m; - data_s24.h = buffer.h; - } else { - data_u24.l = buffer.l; - data_u24.m = buffer.m; - data_u24.h = buffer.h; - } - } else { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); - while (read() >= 0) - ; - return; - } + if (!this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { + ESP_LOGW(TAG, "Read failed"); + return; + } + if (bl0906_checksum(address, &buffer) != buffer.checksum) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + return; + } + if (signed_result) { + data_s24.l = buffer.l; + data_s24.m = buffer.m; + data_s24.h = buffer.h; + } else { + data_u24.l = buffer.l; + data_u24.m = buffer.m; + data_u24.h = buffer.h; } // Power if (reference == BL0906_PREF) { diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 1e8c91d7b7..ed92979d24 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -6,6 +6,7 @@ namespace esphome { namespace bmi160 { static const char *const TAG = "bmi160"; +static constexpr uint32_t GYRO_WAKEUP_TIMEOUT_MS = 100; const uint8_t BMI160_REGISTER_CHIPID = 0x00; @@ -144,7 +145,7 @@ void BMI160Component::internal_setup_(int stage) { } ESP_LOGV(TAG, " Waiting for gyroscope to wake up"); // wait between 51 & 81ms, doing 100 to be safe - this->set_timeout(10, [this]() { this->internal_setup_(2); }); + this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); }); break; case 2: diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 655ae54f0a..085feb8c8a 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -146,6 +146,10 @@ void ESP32Camera::dump_config() { } sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + ESP_LOGE(TAG, " Camera sensor not available"); + return; + } auto st = s->status; ESP_LOGCONFIG(TAG, " JPEG Quality: %u\n" @@ -483,6 +487,9 @@ void ESP32Camera::request_image(camera::CameraRequester requester) { this->singl camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + return; + } /* update image */ s->set_vflip(s, this->vertical_flip_); s->set_hmirror(s, this->horizontal_mirror_); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index eb17cc7de7..f9701cbdf6 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -133,7 +133,7 @@ static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC}; // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; +static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; }; static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { for (uint8_t i = 0; i < 4; i++) { diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 39b0ebd9da..9409dfc21d 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -168,7 +168,7 @@ class LD2450Component : public Component, public uart::UARTDevice { uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; - uint16_t timeout_ = 5; + uint32_t timeout_ = 5; uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 461ddbc085..283b037aca 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -324,6 +324,8 @@ class AddressableFireworksEffect : public AddressableLightEffect { target *= 170; view = target; } + if (it.size() < 2) + return; int last = it.size() - 1; it[0].set(it[0].get() + (it[1].get() * 128)); for (int i = 1; i < last; i++) { diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 88bd7b02fd..a4a31b8260 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -126,18 +126,18 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Copy measurements over into my array. { u_int8_t measurements_index = 0; - for (u_int8_t i = 0; i < 3; i++) { - measurements_time[measurements_index] = mopeka_data->val[i].time_0 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_0; + for (const auto &val : mopeka_data->val) { + measurements_time[measurements_index] = val.time_0 + 1; + measurements_value[measurements_index] = val.value_0; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_1 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_1; + measurements_time[measurements_index] = val.time_1 + 1; + measurements_value[measurements_index] = val.value_1; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_2 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_2; + measurements_time[measurements_index] = val.time_2 + 1; + measurements_value[measurements_index] = val.value_2; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_3 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_3; + measurements_time[measurements_index] = val.time_3 + 1; + measurements_value[measurements_index] = val.value_3; measurements_index++; } } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 45588988c5..c0a02f27f2 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -40,7 +40,7 @@ struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-stru bool slow_update_rate : 1; bool sync_pressed : 1; - mopeka_std_values val[4]; + mopeka_std_values val[3]; } __attribute__((packed)); class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 9bf0450993..01f5aad810 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -146,16 +146,19 @@ void Rtttl::loop() { } #endif // USE_SPEAKER + // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case + while (this->position_ < this->rtttl_.length()) { + char c = this->rtttl_[this->position_]; + if (c != ',' && c != ' ') + break; + this->position_++; + } + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } - // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case - while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { - this->position_++; - } - // First, get note duration, if available uint8_t note_denominator = this->get_integer_(); From 470d9160a512b41042710bf4eb48455dbbd17007 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:57:02 -0400 Subject: [PATCH 091/101] [demo] Fix alarm control panel auth bypass when code is omitted (#14645) Co-authored-by: Claude Opus 4.6 --- esphome/components/demo/demo_alarm_control_panel.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 76cb24c2f4..9976e5c7f0 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -32,8 +32,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { auto code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: - if (this->get_requires_code_to_arm() && code.has_value()) { - if (*code != "1234") { + if (this->get_requires_code_to_arm()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } @@ -41,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { this->publish_state(ACP_STATE_ARMED_AWAY); break; case ACP_STATE_DISARMED: - if (this->get_requires_code() && code.has_value()) { - if (*code != "1234") { + if (this->get_requires_code()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } From 308e8e78cd0fb549b64b1ed16a30dcd1be26c9e3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:59:36 -0400 Subject: [PATCH 092/101] [ble_scanner] Escape special characters in JSON output (#14664) Co-authored-by: Claude Opus 4.6 --- esphome/components/ble_scanner/ble_scanner.h | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index 7061b6d336..c6d7f24cce 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -16,12 +16,27 @@ namespace ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - // Format JSON using stack buffer to avoid heap allocations from string concatenation - char buf[128]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + // Escape special characters in the device name for valid JSON + const char *name = device.get_name().c_str(); + char escaped_name[128]; + size_t pos = 0; + for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) { + uint8_t c = static_cast(*name); + if (c == '"' || c == '\\') { + escaped_name[pos++] = '\\'; + escaped_name[pos++] = c; + } else if (c < 0x20) { + pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c); + } else { + escaped_name[pos++] = c; + } + } + escaped_name[pos] = '\0'; + + char buf[256]; snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", - static_cast(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), - device.get_name().c_str()); + static_cast(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), escaped_name); this->publish_state(buf); return true; } From b3fc43c13c5bee4b60c0bae0dbc1b244e4f4c60c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:00:17 -0400 Subject: [PATCH 093/101] [multiple] Fix wrong behavior in sensor calculations and drivers (#14644) Co-authored-by: Claude Opus 4.6 --- esphome/components/bme280_base/bme280_base.cpp | 7 +++++-- esphome/components/bme680/bme680.h | 4 ++-- esphome/components/cse7766/cse7766.cpp | 9 +++++---- esphome/components/hitachi_ac344/hitachi_ac344.h | 2 +- esphome/components/rx8130/rx8130.cpp | 4 ++-- esphome/components/usb_uart/cp210x.cpp | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index f396888fd1..addbfe618d 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -147,8 +147,11 @@ void BME280Component::setup() { this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1); this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2); this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3); - this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); - this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + // h4 and h5 are signed 12-bit values; shift left then arithmetic right shift to sign-extend + int16_t h4_raw = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); + this->calibration_.h4 = static_cast(h4_raw << 4) >> 4; + int16_t h5_raw = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + this->calibration_.h5 = static_cast(h5_raw << 4) >> 4; this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6); uint8_t humid_control_val = 0; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index d48a42823b..239823fa8c 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -32,8 +32,8 @@ enum BME680Oversampling { /// Struct for storing calibration data for the BME680. struct BME680CalibrationData { uint16_t t1; - uint16_t t2; - uint8_t t3; + int16_t t2; + int8_t t3; uint16_t p1; int16_t p2; diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 806b79e19e..ce77b62b7b 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include namespace esphome::cse7766 { @@ -192,12 +193,12 @@ void CSE7766Component::parse_data_() { this->apparent_power_sensor_->publish_state(apparent_power); } if (have_power && this->reactive_power_sensor_ != nullptr) { - const float reactive_power = apparent_power - power; - if (reactive_power < 0.0f) { - ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power); + const float q_squared = apparent_power * apparent_power - power * power; + if (q_squared < 0.0f) { + ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared); this->reactive_power_sensor_->publish_state(0.0f); } else { - this->reactive_power_sensor_->publish_state(reactive_power); + this->reactive_power_sensor_->publish_state(std::sqrt(q_squared)); } } if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index c34f033d92..0877b83261 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -96,7 +96,7 @@ class HitachiClimate : public climate_ir::ClimateIR { void set_power_(bool on); uint8_t get_mode_(); void set_mode_(uint8_t mode); - void set_temp_(uint8_t celsius, bool set_previous = false); + void set_temp_(uint8_t celsius, bool set_previous = true); uint8_t get_fan_(); void set_fan_(uint8_t speed); void set_swing_v_toggle_(bool on); diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index ba092a4834..9e6f05ee15 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -75,7 +75,7 @@ void RX8130Component::read_time() { .second = bcd2dec(date[0] & 0x7f), .minute = bcd2dec(date[1] & 0x7f), .hour = bcd2dec(date[2] & 0x3f), - .day_of_week = bcd2dec(date[3] & 0x7f), + .day_of_week = static_cast((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1), .day_of_month = bcd2dec(date[4] & 0x3f), .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = bcd2dec(date[5] & 0x1f), @@ -103,7 +103,7 @@ void RX8130Component::write_time() { buff[0] = dec2bcd(now.second); buff[1] = dec2bcd(now.minute); buff[2] = dec2bcd(now.hour); - buff[3] = dec2bcd(now.day_of_week); + buff[3] = 1 << (now.day_of_week - 1); buff[4] = dec2bcd(now.day_of_month); buff[5] = dec2bcd(now.month); buff[6] = dec2bcd(now.year % 100); diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index ae9170c5fb..261f40c0db 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -65,7 +65,7 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev } for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) { - auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset); + auto data_desc = usb_parse_interface_descriptor(config_desc, i, 0, &conf_offset); if (!data_desc) { ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed"); break; From 468ce74c8e2822a87c5367f22ca2c8732732e940 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 9 Mar 2026 17:04:47 -0500 Subject: [PATCH 094/101] [api][serial_proxy] Fix dangling pointer (#14640) --- esphome/components/api/api_connection.cpp | 12 ++++++++++++ esphome/components/serial_proxy/serial_proxy.h | 3 +++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 28a770a4fb..7bd5d5120b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -155,6 +155,18 @@ APIConnection::~APIConnection() { voice_assistant::global_voice_assistant->client_subscription(this, false); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) { + zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } +#endif +#ifdef USE_SERIAL_PROXY + for (auto *proxy : App.get_serial_proxies()) { + if (proxy->get_api_connection() == this) { + proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } + } +#endif } void APIConnection::destroy_active_iterator_() { diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 52f0654ff0..62f942b19d 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -74,6 +74,9 @@ class SerialProxy : public uart::UARTDevice, public Component { /// @param data_size Number of data bits (5-8) void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + /// Get the currently subscribed API connection (nullptr if none) + api::APIConnection *get_api_connection() { return this->api_connection_; } + /// Handle a subscribe/unsubscribe request from an API client void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); From d2686b49bef3fdf1d56f7f222e1532e8c25d4380 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:15:33 -0400 Subject: [PATCH 095/101] [canbus] Fix multiple MCP component bugs (#14461) Co-authored-by: Claude Opus 4.6 --- esphome/components/canbus/canbus.cpp | 3 ++- esphome/components/mcp23x08_base/mcp23x08_base.cpp | 4 ++-- esphome/components/mcp23x17_base/mcp23x17_base.cpp | 4 ++-- esphome/components/mcp2515/mcp2515.cpp | 1 + esphome/components/mcp4461/mcp4461.cpp | 12 ++++++------ esphome/components/mcp4728/mcp4728.h | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index e208b0fd66..ce48bfbba5 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -1,4 +1,5 @@ #include "canbus.h" +#include #include "esphome/core/log.h" namespace esphome { @@ -82,7 +83,7 @@ void Canbus::loop() { std::vector data; // show data received - for (int i = 0; i < can_message.can_data_length_code; i++) { + for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) { ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); data.push_back(can_message.data[i]); } diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 1593c376cd..92228be62c 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -47,12 +47,12 @@ void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr case mcp23xxx_base::MCP23XXX_RISING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, true, defval); + this->update_reg(pin, false, defval); break; case mcp23xxx_base::MCP23XXX_FALLING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, false, defval); + this->update_reg(pin, true, defval); break; case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: this->update_reg(pin, false, gpinten); diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index b1f1f260b4..6f95ee98fd 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -59,12 +59,12 @@ void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr case mcp23xxx_base::MCP23XXX_RISING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, true, defval); + this->update_reg(pin, false, defval); break; case mcp23xxx_base::MCP23XXX_FALLING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, false, defval); + this->update_reg(pin, true, defval); break; case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: this->update_reg(pin, false, gpinten); diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index 77bfaf9224..c2db9228c8 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -506,6 +506,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo cfg3 = MCP_12MHZ_40KBPS_CFG3; break; case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_12MHZ_50KBPS_CFG1; cfg2 = MCP_12MHZ_50KBPS_CFG2; cfg3 = MCP_12MHZ_50KBPS_CFG3; break; diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index dc7e7019aa..48d90377df 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -79,8 +79,8 @@ void Mcp4461Component::dump_config() { // reworked to be a one-line intentionally, as output would not be in order if (i < 4) { ESP_LOGCONFIG(TAG, " ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, A: %s, B: %s, W: %s", i, - this->reg_[i].state, ONOFF(this->reg_[i].terminal_hw), ONOFF(this->reg_[i].terminal_a), - ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w), ONOFF(this->reg_[i].enabled)); + this->reg_[i].state, ONOFF(this->reg_[i].enabled), ONOFF(this->reg_[i].terminal_hw), + ONOFF(this->reg_[i].terminal_a), ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w)); } else { ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state); } @@ -315,9 +315,9 @@ void Mcp4461Component::disable_wiper_(Mcp4461WiperIdx wiper) { return; } ESP_LOGV(TAG, "Disabling wiper %u", wiper_idx); - this->reg_[wiper_idx].enabled = true; + this->reg_[wiper_idx].enabled = false; if (wiper_idx < 4) { - this->reg_[wiper_idx].terminal_hw = true; + this->reg_[wiper_idx].terminal_hw = false; this->reg_[wiper_idx].update_terminal = true; } } @@ -490,7 +490,7 @@ void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) { ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); return; } - this->reg_[wiper_idx].update_terminal = false; + this->reg_[wiper_idx].update_terminal = true; } void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { @@ -517,7 +517,7 @@ void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); return; } - this->reg_[wiper_idx].update_terminal = false; + this->reg_[wiper_idx].update_terminal = true; } uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { diff --git a/esphome/components/mcp4728/mcp4728.h b/esphome/components/mcp4728/mcp4728.h index f2262f4a35..d657408081 100644 --- a/esphome/components/mcp4728/mcp4728.h +++ b/esphome/components/mcp4728/mcp4728.h @@ -58,7 +58,7 @@ class MCP4728Component : public Component, public i2c::I2CDevice { void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); private: - DACInputData reg_[4]; + DACInputData reg_[4]{}; bool store_in_eeprom_ = false; bool update_ = false; }; From d96be88ff58d3dac9454364f6da24ff4ff0e3561 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:32:57 -0400 Subject: [PATCH 096/101] [multiple] Fix reliability issues in 5 components (#14655) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/bme680_bsec/bme680_bsec.cpp | 8 +++++++- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 16 ++++++++++++++++ esphome/components/lvgl/lvgl_esphome.cpp | 6 ++++++ esphome/components/mqtt/mqtt_client.cpp | 4 +++- esphome/components/usb_uart/usb_uart.cpp | 5 +++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 454be0c0fe..75efb6835a 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -271,10 +271,16 @@ void BME680BSECComponent::read_() { int64_t curr_time_ns = this->get_time_ns_(); if (this->bme680_settings_.trigger_measurement) { + uint32_t start = millis(); while (this->bme680_.power_mode != BME680_SLEEP_MODE) { + if (millis() - start > 50) { + ESP_LOGE(TAG, "Timeout waiting for BME680 to enter sleep mode"); + return; + } this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); if (this->bme680_status_ != BME680_OK) { - ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + ESP_LOGE(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; } } } diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 7c7c8782de..7a0dc0690c 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -6,6 +6,7 @@ namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; +static constexpr uint32_t PAYLOAD_TIMEOUT_MS = 20; void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); @@ -133,6 +134,21 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; + // Wait for remaining data (payload + checksum) to arrive. + // Header bytes are already consumed, so we must finish reading this message. + uint32_t start = millis(); + while (this->available() < length + 1) { + if (millis() - start > PAYLOAD_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length); + // Drain any partial payload bytes to resync the parser + while (this->available() > 0) { + this->read(); + } + return; + } + delay(1); + } + // Read up to buffer size; discard excess bytes while still computing checksum // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) // but handlers only need the first few bytes diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 66cb25b864..5400054bb1 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -163,8 +163,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; + size_t start = this->current_page_; do { this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); + if (this->current_page_ == start) + return; // all pages have skip=true (guaranteed not to happen by YAML validation) } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } @@ -172,8 +175,11 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; + size_t start = this->current_page_; do { this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); + if (this->current_page_ == start) + return; // all pages have skip=true (guaranteed not to happen by YAML validation) } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1a03c5329e..38daf8f8f6 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -44,8 +44,10 @@ MQTTClientComponent::MQTTClientComponent() { void MQTTClientComponent::setup() { this->mqtt_backend_.set_on_message( [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) { - if (index == 0) + if (index == 0) { + this->payload_buffer_.clear(); this->payload_buffer_.reserve(total); + } // append new payload, may contain incomplete MQTT message this->payload_buffer_.append(payload, len); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 83de0b39fc..3d35f368fb 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -106,10 +106,15 @@ std::vector USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev } void RingBuffer::push(uint8_t item) { + if (this->get_free_space() == 0) + return; this->buffer_[this->insert_pos_] = item; this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; } void RingBuffer::push(const uint8_t *data, size_t len) { + size_t free = this->get_free_space(); + if (len > free) + len = free; for (size_t i = 0; i != len; i++) { this->buffer_[this->insert_pos_] = *data++; this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; From dadbdd0f7b2081031d395a2778f92706ab98647a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Mar 2026 12:34:31 -1000 Subject: [PATCH 097/101] [ci] Make codeowner label update non-fatal for fork PRs (#14668) --- .../codeowner-approved-label-update.yml | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index c2eb886913..0bce33ebe2 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -55,18 +55,24 @@ jobs: return; } - if (action === LabelAction.ADD) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr_number, labels: [LABEL_NAME] - }); - console.log(`Added '${LABEL_NAME}' label`); - } else if (action === LabelAction.REMOVE) { - try { + try { + if (action === LabelAction.ADD) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (action === LabelAction.REMOVE) { await github.rest.issues.removeLabel({ owner, repo, issue_number: pr_number, name: LABEL_NAME }); console.log(`Removed '${LABEL_NAME}' label`); - } catch (error) { - if (error.status !== 404) throw error; + } + } catch (error) { + if (error.status === 403) { + console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`); + } else if (error.status === 404) { + console.log(`Label '${LABEL_NAME}' not present, nothing to remove`); + } else { + throw error; } } From d6ce5dda81d6fdf966392e80b68a8c8cac0a3192 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Mar 2026 12:54:56 -1000 Subject: [PATCH 098/101] [ci] Skip YAML anchor keys in integration fixture component extraction (#14670) --- script/helpers.py | 6 ++++-- tests/script/test_helpers.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 202ac9b5fc..d372d2a7ec 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -705,8 +705,10 @@ def get_components_from_integration_fixtures() -> set[str]: if not config: continue - # Add all top-level component keys - components.update(config.keys()) + # Add all top-level component keys (skip YAML anchor keys starting with '.') + components.update( + k for k in config if isinstance(k, str) and not k.startswith(".") + ) # Add platform components (e.g., output.template) for value in config.values(): diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 7e60ba41fc..2953a9fd42 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1057,6 +1057,30 @@ def test_get_components_from_integration_fixtures() -> None: assert components == expected_components +def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None: + """Test that YAML anchor keys (starting with '.') are excluded.""" + yaml_content = { + "sensor": [{"platform": "template", "name": "test"}], + "esphome": {"name": "test"}, + ".sensor_filters": {"filters": [{"timeout": "50ms"}]}, + ".binary_filters": {"filters": [{"settle": "50ms"}]}, + } + + mock_yaml_file = Mock() + + with ( + patch("pathlib.Path.glob") as mock_glob, + patch("esphome.yaml_util.load_yaml", return_value=yaml_content), + ): + mock_glob.return_value = [mock_yaml_file] + + components = helpers.get_components_from_integration_fixtures() + + assert ".sensor_filters" not in components + assert ".binary_filters" not in components + assert components == {"sensor", "esphome", "template"} + + @pytest.mark.parametrize( "output,expected", [ From c31ac662bd08cdb99859819dc92a8a9295fb828c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:39:58 -0400 Subject: [PATCH 099/101] [multiple] Fix crashes from malformed external input (#14643) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/b_parasite/b_parasite.cpp | 10 ++ .../components/kamstrup_kmp/kamstrup_kmp.cpp | 12 +- esphome/components/ld2412/ld2412.cpp | 39 ++-- esphome/components/nextion/nextion.cpp | 4 +- esphome/components/pipsolar/pipsolar.cpp | 9 +- esphome/components/smt100/smt100.cpp | 25 ++- .../uart_mock_ld2412_engineering.yaml | 93 +++------- ...art_mock_ld2412_engineering_truncated.yaml | 167 ++++++++++++++++++ tests/integration/test_uart_mock_ld2412.py | 125 +++++++++++++ 9 files changed, 390 insertions(+), 94 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 356f396476..7be26efa7f 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; + if (data.size() < 10) { + ESP_LOGW(TAG, "Service data too short: %zu", data.size()); + return false; + } + const uint8_t protocol_version = data[0] >> 4; if (protocol_version != 1 && protocol_version != 2) { ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); @@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Some b-parasite versions have an (optional) illuminance sensor. bool has_illuminance = data[0] & 0x1; + if (has_illuminance && data.size() < 18) { + ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size()); + return false; + } + // Counter for deduplicating messages. uint8_t counter = data[1] & 0x0f; if (last_processed_counter_ == counter) { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 29de651255..9f2557243c 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -110,9 +110,17 @@ void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) { for (int i = 0; i < buffer_len; i++) { if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) { + if (tx_msg_len + 2 >= static_cast(sizeof(tx_msg))) { + ESP_LOGE(TAG, "TX message overflow"); + return; + } tx_msg[tx_msg_len++] = 0x1b; tx_msg[tx_msg_len++] = buffer[i] ^ 0xff; } else { + if (tx_msg_len + 1 >= static_cast(sizeof(tx_msg))) { + ESP_LOGE(TAG, "TX message overflow"); + return; + } tx_msg[tx_msg_len++] = buffer[i]; } } @@ -216,8 +224,8 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ uint8_t unit_idx = msg[4]; uint8_t mantissa_range = msg[5]; - if (mantissa_range > 4) { - ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range); + if (mantissa_range > 4 || msg_len < 7 + mantissa_range) { + ESP_LOGE(TAG, "Received invalid message (mantissa size %d, msg_len %d)", mantissa_range, msg_len); return; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index ef0915d0bc..37578dd8da 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -413,24 +413,29 @@ void LD2412Component::handle_periodic_data_() { this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); } if (engineering_mode) { - /* - Moving distance range: 18th byte - Still distance range: 19th byte - Moving energy: 20~28th bytes - */ - for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + // Engineering mode needs at least LIGHT_SENSOR + 1 bytes + if (this->buffer_pos_ < LIGHT_SENSOR + 1) { + ESP_LOGW(TAG, "Engineering mode packet too short: %u", this->buffer_pos_); + } else { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor value + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } - /* - Still energy: 29~37th bytes - */ - for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) - } - /* - Light sensor: 38th bytes - */ - SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } else { for (auto &gate_move_sensor : this->gate_move_sensors_) { SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 7ae4d50fc8..01ceb3d765 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -646,8 +646,8 @@ void Nextion::process_nextion_commands_() { break; } - if (to_process_length == 0) { - ESP_LOGE(TAG, "Numeric return but no data"); + if (to_process_length < 4) { + ESP_LOGE(TAG, "Numeric return but insufficient data (need 4, got %zu)", to_process_length); break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index eb6d3931e0..c304d206c0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -192,8 +192,13 @@ bool Pipsolar::send_next_command_() { if (!this->command_queue_[this->command_queue_position_].empty()) { const char *command = this->command_queue_[this->command_queue_position_].c_str(); uint8_t byte_command[16]; - uint8_t length = this->command_queue_[this->command_queue_position_].length(); - for (uint8_t i = 0; i < length; i++) { + size_t length = this->command_queue_[this->command_queue_position_].length(); + if (length > sizeof(byte_command)) { + ESP_LOGE(TAG, "Command too long: %zu", length); + this->command_queue_[this->command_queue_position_].clear(); + return false; + } + for (size_t i = 0; i < length; i++) { byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i); } this->state_ = STATE_COMMAND; diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 105cc06edb..6eb6416447 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -14,11 +14,26 @@ void SMT100Component::update() { void SMT100Component::loop() { while (this->available() != 0) { if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); - float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); - float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); - float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); - float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); + char *token = strtok(this->readline_buffer_, ","); + if (!token) + continue; + int counts = (int) strtol(token, nullptr, 10); + token = strtok(nullptr, ","); + if (!token) + continue; + float permittivity = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float moisture = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float temperature = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float voltage = (float) strtod(token, nullptr); if (this->counts_sensor_ != nullptr) { counts_sensor_->publish_state(counts); diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index 103dbed132..a69e18888e 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -102,6 +102,18 @@ uart_mock: 0xF8, 0xF7, 0xF6, 0xF5, ] +# Common filter definitions +.sensor_filters: &sensor_filters + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +.binary_filters: &binary_filters + filters: + - settle: 50ms + ld2412: id: ld2412_dev uart_id: mock_uart @@ -111,107 +123,56 @@ sensor: ld2412_id: ld2412_dev moving_distance: name: "Moving Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_distance: name: "Still Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters moving_energy: name: "Moving Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters detection_distance: name: "Detection Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters light: name: "Light" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_0: move_energy: name: "Gate 0 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 0 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_1: move_energy: name: "Gate 1 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 1 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_2: move_energy: name: "Gate 2 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 2 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters binary_sensor: - platform: ld2412 ld2412_id: ld2412_dev has_target: name: "Has Target" - filters: - - settle: 50ms + <<: *binary_filters has_moving_target: name: "Has Moving Target" - filters: - - settle: 50ms + <<: *binary_filters has_still_target: name: "Has Still Target" - filters: - - settle: 50ms + <<: *binary_filters button: - platform: template diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml new file mode 100644 index 0000000000..c0bd514762 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml @@ -0,0 +1,167 @@ +esphome: + name: uart-mock-ld2412-eng-trunc + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + auto_start: false + injections: + # Phase 1 (t=100ms): Valid engineering mode frame (52 bytes, buffer_pos_=52) + # Establishes baseline: gate_0_move=100, light=87 + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Truncated engineering mode frame (24 bytes, buffer_pos_=24) + # This frame has data_type=0x01 (engineering) but only enough data for the + # basic target fields, not the gate energies or light sensor. + # buffer_pos_=24 passes the old check (>= 12) but fails the new check (< 46). + # Without the fix, indices 17-45 would read stale buffer data from Phase 1. + # + # Layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0E 00 = length 14 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 + # [11] 50 = moving energy 80 + # [12-13] 1E 00 = still distance 30 + # [14] 50 = still energy 80 + # [15-16] FF FF = garbage detection distance bytes + # [17] FF = padding (would be gate data in full frame) + # [18] 55 = data footer marker (at buffer_pos_ - 6) + # [19] 00 = check byte + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0E, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x50, + 0x1E, 0x00, + 0x50, + 0xFF, 0xFF, + 0xFF, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Valid recovery frame with different values + # gate_0_move=50, light=42 — proves component recovered + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x32, 0x20, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x28, 0x20, 0x18, 0x10, 0x08, + 0x2A, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +# Common filter definitions +.sensor_filters: &sensor_filters + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +.binary_filters: &binary_filters + filters: + - settle: 50ms + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + <<: *sensor_filters + still_distance: + name: "Still Distance" + <<: *sensor_filters + moving_energy: + name: "Moving Energy" + <<: *sensor_filters + still_energy: + name: "Still Energy" + <<: *sensor_filters + detection_distance: + name: "Detection Distance" + <<: *sensor_filters + light: + name: "Light" + <<: *sensor_filters + gate_0: + move_energy: + name: "Gate 0 Move Energy" + <<: *sensor_filters + still_energy: + name: "Gate 0 Still Energy" + <<: *sensor_filters + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + <<: *binary_filters + has_moving_target: + name: "Has Moving Target" + <<: *binary_filters + has_still_target: + name: "Has Still Target" + <<: *binary_filters + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index a964ba0073..9b928ef14f 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -14,6 +14,12 @@ test_uart_mock_ld2412_engineering (engineering mode): 2. Multi-byte still distance (291cm) using high byte > 0 3. Gate energy sensor values 4. Detection distance computed from target state + +test_uart_mock_ld2412_engineering_truncated (truncated engineering mode): + 1. Valid engineering frame establishes baseline sensor values + 2. Truncated engineering frame (24 bytes) is rejected — gate/light sensors + must not receive garbage from stale buffer data or frame footer bytes + 3. Recovery frame with different values proves the component survived """ from __future__ import annotations @@ -273,3 +279,122 @@ async def test_uart_mock_ld2412_engineering( ) assert pytest.approx(291.0) in collector.sensor_states["detection_distance"] + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412_engineering_truncated( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that truncated engineering mode frames don't corrupt sensor values. + + Without the fix, a 24-byte engineering mode frame passes the old buffer_pos_ >= 12 + check but reads indices 17-45 from stale buffer data, publishing garbage values + (e.g. frame footer bytes 0xF8=248 as gate energy). + """ + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track the truncated frame warning + truncated_warning_seen = loop.create_future() + + def line_callback(line: str) -> None: + if ( + "Engineering mode packet too short" in line + and not truncated_warning_seen.done() + ): + truncated_warning_seen.set_result(True) + + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_0_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) + + # Signal when we see Phase 3 recovery values (gate_0_move=50) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 — valid engineering frame establishes baseline + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 baseline: gate_0_move=100, light=87 + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) + + # Wait for Phase 3 recovery frame (gate_0_move=50) + try: + await asyncio.wait_for(recovery_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" gate_0_move_energy: {collector.sensor_states['gate_0_move_energy']}\n" + f" light: {collector.sensor_states['light']}" + ) + + # Verify the truncated frame warning was logged + assert truncated_warning_seen.done(), ( + "Expected 'Engineering mode packet too short' warning in logs" + ) + + # Phase 3 recovery: gate_0_move=50, light=42 + assert pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + assert pytest.approx(42.0) in collector.sensor_states["light"] + + # The critical assertion: gate_0_move_energy must never have received + # garbage values from the truncated frame. Without the fix, + # buffer_data_[17] = 0xFF = 255 would be published as gate_0_move. + for value in collector.sensor_states["gate_0_move_energy"]: + assert value == pytest.approx(100.0) or value == pytest.approx(50.0), ( + f"gate_0_move_energy got unexpected value {value} — " + f"truncated frame likely leaked stale buffer data. " + f"All values: {collector.sensor_states['gate_0_move_energy']}" + ) From 00f809f5f001a1428f099f3e0846d2c5d848cdf3 Mon Sep 17 00:00:00 2001 From: Tobias Stanzel Date: Tue, 10 Mar 2026 02:45:20 +0100 Subject: [PATCH 100/101] [sen6x] fix memory leak issue (#14623) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sen6x/sen6x.cpp | 338 ++++++++++++++++------------- esphome/components/sen6x/sen6x.h | 6 + 2 files changed, 188 insertions(+), 156 deletions(-) diff --git a/esphome/components/sen6x/sen6x.cpp b/esphome/components/sen6x/sen6x.cpp index baaadd6463..2a6ea64735 100644 --- a/esphome/components/sen6x/sen6x.cpp +++ b/esphome/components/sen6x/sen6x.cpp @@ -2,13 +2,16 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include -#include -#include namespace esphome::sen6x { static const char *const TAG = "sen6x"; +static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts +static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete +static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts +// Single numeric timeout ID — the chain is sequential so only one is active at a time. +static constexpr uint32_t TIMEOUT_POLL = 1; static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202; static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100; static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014; @@ -182,179 +185,202 @@ void SEN6XComponent::update() { return; } - uint16_t read_cmd; - uint8_t read_words; - set_read_command_and_words(this->sen6x_type_, read_cmd, read_words); + // Cancel any in-flight polling from a previous update() cycle. + this->cancel_timeout(TIMEOUT_POLL); - const uint8_t poll_retries = 24; - auto poll_ready = std::make_shared>(); - *poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) { - const uint8_t attempt = static_cast(poll_retries - retries_left + 1); - ESP_LOGV(TAG, "Data ready polling attempt %u", attempt); + set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_); - if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + // Polling uses chained timeouts to guarantee each I2C operation completes + // before the next begins. The flow is: + // + // poll_data_ready_() + // -> write_command (data ready status) + // -> timeout I2C_READ_DELAY + // -> read_data (check ready flag) + // -> if not ready: timeout POLL_INTERVAL -> poll_data_ready_() (retry) + // -> if ready: read_measurements_() + // -> write_command (read measurement) + // -> timeout I2C_READ_DELAY + // -> parse_and_publish_measurements_() + // + // All timeouts share a single ID (TIMEOUT_POLL) since only one is active + // at a time. cancel_timeout in update() stops any in-flight chain. + this->poll_retries_remaining_ = POLL_RETRIES; + this->poll_data_ready_(); +} + +void SEN6XComponent::poll_data_ready_() { + if (this->poll_retries_remaining_ == 0) { + this->status_set_warning(); + ESP_LOGD(TAG, "Data not ready"); + return; + } + ESP_LOGV(TAG, "Data ready polling attempt %u", + static_cast(POLL_RETRIES - this->poll_retries_remaining_ + 1)); + this->poll_retries_remaining_--; + + if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + return; + } + + this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { + uint16_t raw_read_status; + if (!this->read_data(&raw_read_status, 1)) { this->status_set_warning(); - ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); return; } - this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() { - uint16_t raw_read_status; - if (!this->read_data(&raw_read_status, 1)) { - this->status_set_warning(); - ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); - return; - } + if ((raw_read_status & 0x0001) == 0) { + // Not ready yet; schedule next attempt after POLL_INTERVAL. + this->set_timeout(TIMEOUT_POLL, POLL_INTERVAL, [this]() { this->poll_data_ready_(); }); + return; + } - if ((raw_read_status & 0x0001) == 0) { - if (retries_left == 0) { - this->status_set_warning(); - ESP_LOGD(TAG, "Data not ready"); - return; - } - this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); }); - return; - } + this->read_measurements_(); + }); +} - if (!this->write_command(read_cmd)) { - this->status_set_warning(); - ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); - return; - } +void SEN6XComponent::read_measurements_() { + if (!this->write_command(this->read_cmd_)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); + return; + } - this->set_timeout(20, [this, read_words]() { - uint16_t measurements[10]; + this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_measurements_(); }); +} - if (!this->read_data(measurements, read_words)) { - this->status_set_warning(); - ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); - return; - } - int8_t voc_index = -1; - int8_t nox_index = -1; - int8_t hcho_index = -1; - int8_t co2_index = -1; - bool co2_uint16 = false; - switch (this->sen6x_type_) { - case SEN62: - break; - case SEN63C: - co2_index = 6; - break; - case SEN65: - voc_index = 6; - nox_index = 7; - break; - case SEN66: - voc_index = 6; - nox_index = 7; - co2_index = 8; - co2_uint16 = true; - break; - case SEN68: - voc_index = 6; - nox_index = 7; - hcho_index = 8; - break; - case SEN69C: - voc_index = 6; - nox_index = 7; - hcho_index = 8; - co2_index = 9; - break; - default: - break; - } +void SEN6XComponent::parse_and_publish_measurements_() { + uint16_t measurements[10]; - float pm_1_0 = measurements[0] / 10.0f; - if (measurements[0] == 0xFFFF) - pm_1_0 = NAN; - float pm_2_5 = measurements[1] / 10.0f; - if (measurements[1] == 0xFFFF) - pm_2_5 = NAN; - float pm_4_0 = measurements[2] / 10.0f; - if (measurements[2] == 0xFFFF) - pm_4_0 = NAN; - float pm_10_0 = measurements[3] / 10.0f; - if (measurements[3] == 0xFFFF) - pm_10_0 = NAN; - float humidity = static_cast(measurements[4]) / 100.0f; - if (measurements[4] == 0x7FFF) - humidity = NAN; - float temperature = static_cast(measurements[5]) / 200.0f; - if (measurements[5] == 0x7FFF) - temperature = NAN; + if (!this->read_data(measurements, this->read_words_)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); + return; + } + int8_t voc_index = -1; + int8_t nox_index = -1; + int8_t hcho_index = -1; + int8_t co2_index = -1; + bool co2_uint16 = false; + switch (this->sen6x_type_) { + case SEN62: + break; + case SEN63C: + co2_index = 6; + break; + case SEN65: + voc_index = 6; + nox_index = 7; + break; + case SEN66: + voc_index = 6; + nox_index = 7; + co2_index = 8; + co2_uint16 = true; + break; + case SEN68: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + break; + case SEN69C: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + co2_index = 9; + break; + default: + break; + } - float voc = NAN; - float nox = NAN; - float hcho = NAN; - float co2 = NAN; + float pm_1_0 = measurements[0] / 10.0f; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0f; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0f; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0f; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = static_cast(measurements[4]) / 100.0f; + if (measurements[4] == 0x7FFF) + humidity = NAN; + float temperature = static_cast(measurements[5]) / 200.0f; + if (measurements[5] == 0x7FFF) + temperature = NAN; - if (voc_index >= 0) { - voc = static_cast(measurements[voc_index]) / 10.0f; - if (measurements[voc_index] == 0x7FFF) - voc = NAN; - } - if (nox_index >= 0) { - nox = static_cast(measurements[nox_index]) / 10.0f; - if (measurements[nox_index] == 0x7FFF) - nox = NAN; - } + float voc = NAN; + float nox = NAN; + float hcho = NAN; + float co2 = NAN; - if (hcho_index >= 0) { - const uint16_t hcho_raw = measurements[hcho_index]; - hcho = hcho_raw / 10.0f; - if (hcho_raw == 0xFFFF) - hcho = NAN; - } + if (voc_index >= 0) { + voc = static_cast(measurements[voc_index]) / 10.0f; + if (measurements[voc_index] == 0x7FFF) + voc = NAN; + } + if (nox_index >= 0) { + nox = static_cast(measurements[nox_index]) / 10.0f; + if (measurements[nox_index] == 0x7FFF) + nox = NAN; + } - if (co2_index >= 0) { - if (co2_uint16) { - const uint16_t co2_raw = measurements[co2_index]; - co2 = static_cast(co2_raw); - if (co2_raw == 0xFFFF) - co2 = NAN; - } else { - const int16_t co2_raw = static_cast(measurements[co2_index]); - co2 = static_cast(co2_raw); - if (co2_raw == 0x7FFF) - co2 = NAN; - } - } + if (hcho_index >= 0) { + const uint16_t hcho_raw = measurements[hcho_index]; + hcho = hcho_raw / 10.0f; + if (hcho_raw == 0xFFFF) + hcho = NAN; + } - if (!this->startup_complete_) { - ESP_LOGD(TAG, "Startup delay, ignoring values"); - this->status_clear_warning(); - return; - } + if (co2_index >= 0) { + if (co2_uint16) { + const uint16_t co2_raw = measurements[co2_index]; + co2 = static_cast(co2_raw); + if (co2_raw == 0xFFFF) + co2 = NAN; + } else { + const int16_t co2_raw = static_cast(measurements[co2_index]); + co2 = static_cast(co2_raw); + if (co2_raw == 0x7FFF) + co2 = NAN; + } + } - if (this->pm_1_0_sensor_ != nullptr) - this->pm_1_0_sensor_->publish_state(pm_1_0); - if (this->pm_2_5_sensor_ != nullptr) - this->pm_2_5_sensor_->publish_state(pm_2_5); - if (this->pm_4_0_sensor_ != nullptr) - this->pm_4_0_sensor_->publish_state(pm_4_0); - if (this->pm_10_0_sensor_ != nullptr) - this->pm_10_0_sensor_->publish_state(pm_10_0); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - if (this->voc_sensor_ != nullptr) - this->voc_sensor_->publish_state(voc); - if (this->nox_sensor_ != nullptr) - this->nox_sensor_->publish_state(nox); - if (this->hcho_sensor_ != nullptr) - this->hcho_sensor_->publish_state(hcho); - if (this->co2_sensor_ != nullptr) - this->co2_sensor_->publish_state(co2); + if (!this->startup_complete_) { + ESP_LOGD(TAG, "Startup delay, ignoring values"); + this->status_clear_warning(); + return; + } - this->status_clear_warning(); - }); - }); - }; + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + if (this->hcho_sensor_ != nullptr) + this->hcho_sensor_->publish_state(hcho); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); - (*poll_ready)(poll_retries); + this->status_clear_warning(); } SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) { diff --git a/esphome/components/sen6x/sen6x.h b/esphome/components/sen6x/sen6x.h index 01e89dce1b..bc44611882 100644 --- a/esphome/components/sen6x/sen6x.h +++ b/esphome/components/sen6x/sen6x.h @@ -30,13 +30,19 @@ class SEN6XComponent : public PollingComponent, public sensirion_common::Sensiri protected: Sen6xType infer_type_from_product_name_(const std::string &product_name); + void poll_data_ready_(); + void read_measurements_(); + void parse_and_publish_measurements_(); bool initialized_{false}; std::string product_name_; Sen6xType sen6x_type_{UNKNOWN}; std::string serial_number_; + uint16_t read_cmd_{0}; uint8_t firmware_version_major_{0}; uint8_t firmware_version_minor_{0}; + uint8_t poll_retries_remaining_{0}; + uint8_t read_words_{0}; bool startup_complete_{false}; }; From e82f0f443223a4985bf2014fbf9b5f4130b2f4b8 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 10 Mar 2026 03:41:02 +0100 Subject: [PATCH 101/101] [cpptests] support testing platform components (#13075) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/core/__init__.py | 15 + esphome/loader.py | 5 + script/cpp_unit_test.py | 153 +++++++---- script/helpers.py | 15 +- tests/components/README.md | 13 + .../binary_sensor/binary_sensor_test.cpp | 77 ++++++ .../components/packet_transport/cpp_test.yaml | 11 - .../packet_transport_test.cpp | 259 ------------------ .../packet_transport/sensor/sensor_test.cpp | 170 ++++++++++++ tests/script/test_helpers.py | 67 +++++ tests/unit_tests/test_core.py | 12 + 11 files changed, 467 insertions(+), 330 deletions(-) create mode 100644 tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp delete mode 100644 tests/components/packet_transport/cpp_test.yaml create mode 100644 tests/components/packet_transport/sensor/sensor_test.cpp diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 484f679369..a86478aca1 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -615,6 +615,10 @@ class EsphomeCore: self.address_cache: AddressCache | None = None # Cached config hash (computed lazily) self._config_hash: int | None = None + # True if compiling for C++ unit tests + self.cpp_testing = False + # Allowlist of components whose to_code should run during C++ testing + self.cpp_testing_codegen: set[str] = set() def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -644,6 +648,8 @@ class EsphomeCore: self.current_component = None self.address_cache = None self._config_hash = None + self.cpp_testing = False + self.cpp_testing_codegen = set() PIN_SCHEMA_REGISTRY.reset() @contextmanager @@ -987,6 +993,15 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def testing_ensure_platform_registered(self, platform_name: str) -> None: + """Ensure a platform has at least one entity registered for testing. + + Used during C++ test builds to guarantee USE_* defines are emitted + without needing a real component variable. + """ + if not self.platform_counts[platform_name]: + self.platform_counts[platform_name] = 1 + def register_controller(self) -> None: """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) diff --git a/esphome/loader.py b/esphome/loader.py index 968c8cf3e0..5771e07473 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -71,6 +71,11 @@ class ComponentManifest: @property def to_code(self) -> Callable[[Any], None] | None: + if CORE.cpp_testing: + # During C++ testing, only run to_code for allowlisted components + name = self.module.__package__.rsplit(".", 1)[-1] + if name not in CORE.cpp_testing_codegen: + return None return getattr(self.module, "to_code", None) @property diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index b87261ab33..e11687dc16 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -10,9 +10,10 @@ from helpers import get_all_components, get_all_dependencies, root_path from esphome.__main__ import command_compile, parse_args from esphome.config import validate_config +from esphome.const import CONF_PLATFORM from esphome.core import CORE +from esphome.loader import get_component from esphome.platformio_api import get_idedata -from esphome.yaml_util import load_yaml # This must coincide with the version in /platformio.ini PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" @@ -20,6 +21,13 @@ PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" # Path to /tests/components COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" +# Components whose to_code should run during C++ test builds. +# Most components don't need code generation for tests; only these +# essential ones (platform setup, logging, core config) are needed. +# Note: "core" is the esphome core config module (esphome/core/config.py), +# which registers under package name "core" not "esphome". +CPP_TESTING_CODEGEN_COMPONENTS = {"core", "host", "logger"} + def hash_components(components: list[str]) -> str: key = ",".join(components) @@ -30,12 +38,14 @@ def filter_components_without_tests(components: list[str]) -> list[str]: """Filter out components that do not have a corresponding test file. This is done by checking if the component's directory contains at - least a .cpp file. + least a .cpp or .h file. """ filtered_components: list[str] = [] for component in components: test_dir = COMPONENTS_TESTS_DIR / component - if test_dir.is_dir() and any(test_dir.glob("*.cpp")): + if test_dir.is_dir() and ( + any(test_dir.glob("*.cpp")) or any(test_dir.glob("*.h")) + ): filtered_components.append(component) else: print( @@ -45,38 +55,6 @@ def filter_components_without_tests(components: list[str]) -> list[str]: return filtered_components -# Name of optional per-component YAML config merged into the test build -# before validation so that platform defines (USE_SENSOR, etc.) are generated. -CPP_TEST_CONFIG_FILE = "cpp_test.yaml" - - -def load_component_test_configs(components: list[str]) -> dict: - """Load cpp_test.yaml files from test component directories. - - These configs are merged into the base test config *before* validation - so that entity registration runs during code generation, which causes - the corresponding USE_* defines to be emitted. - """ - merged: dict = {} - for component in components: - config_file = COMPONENTS_TESTS_DIR / component / CPP_TEST_CONFIG_FILE - if not config_file.exists(): - continue - component_config = load_yaml(config_file) - if not component_config: - continue - for key, value in component_config.items(): - if ( - key in merged - and isinstance(merged[key], list) - and isinstance(value, list) - ): - merged[key].extend(value) - else: - merged[key] = value - return merged - - def create_test_config(config_name: str, includes: list[str]) -> dict: """Create ESPHome test configuration for C++ unit tests. @@ -113,11 +91,52 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: } +def get_platform_components(components: list[str]) -> list[str]: + """Discover platform sub-components referenced by test directory structure. + + For each component being tested, any sub-directory named after a platform + domain (e.g. ``sensor``, ``binary_sensor``) is treated as a request to + include that ``.`` platform in the build. The sub- + directory must name a valid platform domain; anything else raises an error + so that typos are caught early. + + Returns: + List of ``"domain.component"`` strings, one per discovered sub-directory. + """ + platform_components: list[str] = [] + for component in components: + test_dir = COMPONENTS_TESTS_DIR / component + if not test_dir.is_dir(): + continue + # Each sub-directory name is expected to be a platform domain + # (e.g. tests/components/bthome/sensor/ → sensor.bthome). + for domain_dir in test_dir.iterdir(): + if not domain_dir.is_dir(): + continue + domain = domain_dir.name + domain_module = get_component(domain) + if domain_module is None or not domain_module.is_platform_component: + raise ValueError( + f"Component tests for '{component}' reference non-existing or invalid domain '{domain}'" + f" in its directory structure. See ({COMPONENTS_TESTS_DIR / component / domain})." + ) + platform_components.append(f"{domain}.{component}") + return platform_components + + +# Exit codes for run_tests +EXIT_OK = 0 +EXIT_SKIPPED = 1 +EXIT_COMPILE_ERROR = 2 +EXIT_CONFIG_ERROR = 3 +EXIT_NO_EXECUTABLE = 4 + + def run_tests(selected_components: list[str]) -> int: # Skip tests on Windows if os.name == "nt": print("Skipping esphome tests on Windows", file=sys.stderr) - return 1 + return EXIT_SKIPPED # Remove components that do not have tests components = filter_components_without_tests(selected_components) @@ -127,45 +146,63 @@ def run_tests(selected_components: list[str]) -> int: "No components specified or no tests found for the specified components.", file=sys.stderr, ) - return 0 + return EXIT_OK components = sorted(components) - # Obtain possible dependencies for the requested components. - # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, - # which causes core/time.h to include components/time/posix_tz.h. - components_with_dependencies = sorted( - get_all_dependencies(set(components) | {"time"}) - ) - - # Build a list of include folders, one folder per component containing tests. - # A special replacement main.cpp is located in /tests/components/main.cpp + # Build a list of include folders relative to COMPONENTS_TESTS_DIR. These folders will + # be added along with their subfolders. + # "main.cpp" is a special entry that points to /tests/components/main.cpp, + # which provides a custom test runner entry-point replacing the default one. + # Each remaining entry is a component folder whose *.cpp files are compiled. includes: list[str] = ["main.cpp"] + components + # Obtain a list of platform components to be tested: + try: + platform_components = get_platform_components(components) + except ValueError as e: + print(f"Error obtaining platform components: {e}") + return EXIT_CONFIG_ERROR + + components = sorted(components + platform_components) + # Create a unique name for this config based on the actual components being tested # to maximize cache during testing config_name: str = "cpptests-" + hash_components(components) - config = create_test_config(config_name, includes) + # Obtain possible dependencies for the requested components. + # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, + # which causes core/time.h to include components/time/posix_tz.h. + components_with_dependencies: list[str] = sorted( + get_all_dependencies(set(components) | {"time"}, cpp_testing=True) + ) - # Merge component-specific test configs (e.g. sensor instances) before - # validation so that entity registration and USE_* defines work. - extra_config = load_component_test_configs(components) - config.update(extra_config) + config = create_test_config(config_name, includes) CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" CORE.dashboard = None + CORE.cpp_testing = True + CORE.cpp_testing_codegen = CPP_TESTING_CODEGEN_COMPONENTS # Validate config will expand the above with defaults: config = validate_config(config, {}) # Add all components and dependencies to the base configuration after validation, so their files - # are added to the build. Use setdefault to avoid overwriting entries that were - # already validated (e.g. sensor instances from cpp_test.yaml). - for key in components_with_dependencies: - config.setdefault(key, {}) + # are added to the build. + for component_name in components_with_dependencies: + if "." in component_name: + # Format is always "domain.component" (exactly one dot), + # as produced by get_platform_components(). + domain, component = component_name.split(".", maxsplit=1) + domain_list = config.setdefault(domain, []) + CORE.testing_ensure_platform_registered(domain) + domain_list.append({CONF_PLATFORM: component}) + else: + config.setdefault(component_name, []) - print(f"Testing components: {', '.join(components)}") + dependencies = set(components_with_dependencies) - set(components) + deps_str = ", ".join(dependencies) if dependencies else "None" + print(f"Testing components: {', '.join(components)}. Dependencies: {deps_str}") CORE.config = config args = parse_args(["program", "compile", str(CORE.config_path)]) try: @@ -178,13 +215,13 @@ def run_tests(selected_components: list[str]) -> int: print( f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}" ) - return 2 + return EXIT_COMPILE_ERROR # After a successful compilation, locate the executable and run it: idedata = get_idedata(config) if idedata is None: print("Cannot find executable") - return 1 + return EXIT_NO_EXECUTABLE program_path: str = idedata.raw["prog_path"] run_cmd: list[str] = [program_path] diff --git a/script/helpers.py b/script/helpers.py index d372d2a7ec..6ee286a657 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -15,6 +15,8 @@ from typing import Any import colorama +from esphome.loader import get_platform + root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") @@ -624,11 +626,15 @@ def get_usable_cpu_count() -> int: ) -def get_all_dependencies(component_names: set[str]) -> set[str]: +def get_all_dependencies( + component_names: set[str], cpp_testing: bool = False +) -> set[str]: """Get all dependencies for a set of components. Args: component_names: Set of component names to get dependencies for + cpp_testing: If True, set CORE.cpp_testing so AUTO_LOAD callables that + conditionally include testing-only dependencies work correctly Returns: Set of all components including dependencies and auto-loaded components @@ -646,6 +652,7 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: # Reset CORE to ensure clean state CORE.reset() + CORE.cpp_testing = cpp_testing # Set up fake config path for component loading root = Path(__file__).parent.parent @@ -660,7 +667,11 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: new_components: set[str] = set() for comp_name in all_components: - comp = get_component(comp_name) + if "." in comp_name: + domain, platform = comp_name.split(".", maxsplit=1) + comp = get_platform(domain, platform) + else: + comp = get_component(comp_name) if not comp: continue diff --git a/tests/components/README.md b/tests/components/README.md index 0901f2ef17..6da0dadd25 100644 --- a/tests/components/README.md +++ b/tests/components/README.md @@ -7,10 +7,23 @@ testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing` (where "component" is the component under test), for example: `esphome::uart::testing`. +### Platform components + +For components that expose to a platform component, create a folder under your component test folder with the platform component name, e.g. `binary_sensor` and +include the relevant `.cpp` and `.h` test files there. + +### Override component code generation for testing + +When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not +need to generate configuration code for testing. + +If you do need to generate code to for example configure compilation flags or add libraries, +add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`. ## Running component unit tests (from the repository root) + ```bash ./script/cpp_unit_test.py component1 component2 ... ``` diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp new file mode 100644 index 0000000000..36af087d2c --- /dev/null +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -0,0 +1,77 @@ +#include "../common.h" + +namespace esphome::packet_transport::testing { + +TEST(PacketTransportBinarySensorTest, AddBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_binary_sensor("motion", &bs); + ASSERT_EQ(transport.binary_sensors_.size(), 1u); + EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); + EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); +} + +TEST(PacketTransportBinarySensorTest, AddRemoteBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_remote_binary_sensor("host1", "remote_motion", &bs); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); +} + +TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + binary_sensor::BinarySensor local_bs; + local_bs.state = true; + encoder.add_binary_sensor("motion", &local_bs); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + binary_sensor::BinarySensor remote_bs; + decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_TRUE(remote_bs.state); +} + +TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 10.0f; + s2.state = 20.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + binary_sensor::BinarySensor bs1; + bs1.state = true; + encoder.add_binary_sensor("bs1", &bs1); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + binary_sensor::BinarySensor rbs1; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, 10.0f); + EXPECT_FLOAT_EQ(rs2.state, 20.0f); + EXPECT_TRUE(rbs1.state); +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/components/packet_transport/cpp_test.yaml b/tests/components/packet_transport/cpp_test.yaml deleted file mode 100644 index fa39df3c0a..0000000000 --- a/tests/components/packet_transport/cpp_test.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Extra component configuration required by C++ unit tests. -# Loaded by cpp_unit_test.py and merged into the test build config -# before validation, so that platform defines (USE_SENSOR, etc.) are generated. - -sensor: - - platform: template - id: test_cpp_sensor - -binary_sensor: - - platform: template - id: test_cpp_binary_sensor diff --git a/tests/components/packet_transport/packet_transport_test.cpp b/tests/components/packet_transport/packet_transport_test.cpp index d8f11ca607..59c0a88ed7 100644 --- a/tests/components/packet_transport/packet_transport_test.cpp +++ b/tests/components/packet_transport/packet_transport_test.cpp @@ -65,198 +65,6 @@ TEST(PacketTransportTest, SetProviderEncryption) { EXPECT_EQ(transport.providers_["host1"].encryption_key, key); } -// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, AddSensor) { - TestablePacketTransport transport; - sensor::Sensor s; - transport.add_sensor("temp", &s); - ASSERT_EQ(transport.sensors_.size(), 1u); - EXPECT_STREQ(transport.sensors_[0].id, "temp"); - EXPECT_EQ(transport.sensors_[0].sensor, &s); - EXPECT_TRUE(transport.sensors_[0].updated); -} - -TEST(PacketTransportTest, AddRemoteSensor) { - TestablePacketTransport transport; - sensor::Sensor s; - transport.add_remote_sensor("host1", "remote_temp", &s); - EXPECT_TRUE(transport.providers_.contains("host1")); - EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); -} -#endif - -#ifdef USE_BINARY_SENSOR -TEST(PacketTransportTest, AddBinarySensor) { - TestablePacketTransport transport; - binary_sensor::BinarySensor bs; - transport.add_binary_sensor("motion", &bs); - ASSERT_EQ(transport.binary_sensors_.size(), 1u); - EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); - EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); -} - -TEST(PacketTransportTest, AddRemoteBinarySensor) { - TestablePacketTransport transport; - binary_sensor::BinarySensor bs; - transport.add_remote_binary_sensor("host1", "remote_motion", &bs); - EXPECT_TRUE(transport.providers_.contains("host1")); - EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); -} -#endif - -// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, UnencryptedSensorRoundTrip) { - // Encoder - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - sensor::Sensor local_sensor; - local_sensor.state = 42.5f; - encoder.add_sensor("temp", &local_sensor); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - // Decoder - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; // sentinel - decoder.add_remote_sensor("sender", "temp", &remote_sensor); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); -} -#endif - -#ifdef USE_BINARY_SENSOR -TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - binary_sensor::BinarySensor local_bs; - local_bs.state = true; - encoder.add_binary_sensor("motion", &local_bs); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - binary_sensor::BinarySensor remote_bs; - decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_TRUE(remote_bs.state); -} -#endif - -#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR) -TEST(PacketTransportTest, MultipleSensorsRoundTrip) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - - sensor::Sensor s1, s2; - s1.state = 10.0f; - s2.state = 20.0f; - encoder.add_sensor("s1", &s1); - encoder.add_sensor("s2", &s2); - - binary_sensor::BinarySensor bs1; - bs1.state = true; - encoder.add_binary_sensor("bs1", &bs1); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor rs1, rs2; - binary_sensor::BinarySensor rbs1; - rs1.state = -999.0f; - rs2.state = -999.0f; - decoder.add_remote_sensor("sender", "s1", &rs1); - decoder.add_remote_sensor("sender", "s2", &rs2); - decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - - EXPECT_FLOAT_EQ(rs1.state, 10.0f); - EXPECT_FLOAT_EQ(rs2.state, 20.0f); - EXPECT_TRUE(rbs1.state); -} -#endif - -// --- Encrypted round-trip --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, EncryptedSensorRoundTrip) { - std::vector key(32); - for (int i = 0; i < 32; i++) - key[i] = i; - - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - encoder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 99.9f; - encoder.add_sensor("temp", &local_sensor); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - decoder.add_remote_sensor("sender", "temp", &remote_sensor); - decoder.set_provider_encryption("sender", key); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); -} - -// --- Selective send --- - -TEST(PacketTransportTest, SendDataOnlyUpdated) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - - sensor::Sensor s1, s2; - s1.state = 1.0f; - s2.state = 2.0f; - encoder.add_sensor("s1", &s1); - encoder.add_sensor("s2", &s2); - - // Mark s1 as not updated, only s2 as updated - encoder.sensors_[0].updated = false; - encoder.sensors_[1].updated = true; - - encoder.send_data_(false); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor rs1, rs2; - rs1.state = -999.0f; - rs2.state = -999.0f; - decoder.add_remote_sensor("sender", "s1", &rs1); - decoder.add_remote_sensor("sender", "s2", &rs2); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - - EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent - EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent -} -#endif - // --- Ping key tests --- TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) { @@ -319,73 +127,6 @@ TEST(PacketTransportTest, PingKeyMaxLimit) { EXPECT_FALSE(transport.ping_keys_.contains("host4")); } -#ifdef USE_SENSOR -TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) { - std::vector key(32, 0xBB); - - // Responder: encrypted, owns a sensor - TestablePacketTransport responder; - responder.init_for_test("responder"); - responder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 77.7f; - responder.add_sensor("temp", &local_sensor); - - // Requester sends a MAGIC_PING that the responder processes - auto ping = build_ping_packet("requester", 0xDEADBEEF); - responder.process_({ping.data(), ping.size()}); - ASSERT_EQ(responder.ping_keys_.size(), 1u); - - // Responder sends sensor data — ping key should be embedded - responder.send_data_(true); - ASSERT_EQ(responder.sent_packets.size(), 1u); - - // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF - TestablePacketTransport requester; - requester.init_for_test("requester"); - requester.set_ping_pong_enable(true); - requester.ping_key_ = 0xDEADBEEF; - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - requester.add_remote_sensor("responder", "temp", &remote_sensor); - requester.set_provider_encryption("responder", key); - - // The requester decrypts the packet and finds its ping key echoed back, - // which gates the sensor data — if the key is missing, data is blocked. - auto &packet = responder.sent_packets[0]; - requester.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); -} - -TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) { - std::vector key(32, 0xBB); - - // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys - TestablePacketTransport responder; - responder.init_for_test("responder"); - responder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 77.7f; - responder.add_sensor("temp", &local_sensor); - responder.send_data_(true); - ASSERT_EQ(responder.sent_packets.size(), 1u); - - // Requester with ping-pong enabled expects a key that isn't in the packet - TestablePacketTransport requester; - requester.init_for_test("requester"); - requester.set_ping_pong_enable(true); - requester.ping_key_ = 0xDEADBEEF; - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - requester.add_remote_sensor("responder", "temp", &remote_sensor); - requester.set_provider_encryption("responder", key); - - auto &packet = responder.sent_packets[0]; - requester.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found -} -#endif - // --- Process error handling --- TEST(PacketTransportTest, ProcessShortBuffer) { diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp new file mode 100644 index 0000000000..2f681aee58 --- /dev/null +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -0,0 +1,170 @@ +#include "../common.h" + +namespace esphome::packet_transport::testing { + +TEST(PacketTransportSensorTest, AddSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_sensor("temp", &s); + ASSERT_EQ(transport.sensors_.size(), 1u); + EXPECT_STREQ(transport.sensors_[0].id, "temp"); + EXPECT_EQ(transport.sensors_[0].sensor, &s); + EXPECT_TRUE(transport.sensors_[0].updated); +} + +TEST(PacketTransportSensorTest, AddRemoteSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_remote_sensor("host1", "remote_temp", &s); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); +} + +TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { + // Encoder + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + sensor::Sensor local_sensor; + local_sensor.state = 42.5f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + // Decoder + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; // sentinel + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); +} + +TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { + std::vector key(32); + for (int i = 0; i < 32; i++) + key[i] = i; + + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + encoder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 99.9f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + decoder.set_provider_encryption("sender", key); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); +} + +TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 1.0f; + s2.state = 2.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + // Mark s1 as not updated, only s2 as updated + encoder.sensors_[0].updated = false; + encoder.sensors_[1].updated = true; + + encoder.send_data_(false); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent + EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent +} + +TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { + std::vector key(32, 0xBB); + + // Responder: encrypted, owns a sensor + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + + // Requester sends a MAGIC_PING that the responder processes + auto ping = build_ping_packet("requester", 0xDEADBEEF); + responder.process_({ping.data(), ping.size()}); + ASSERT_EQ(responder.ping_keys_.size(), 1u); + + // Responder sends sensor data — ping key should be embedded + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + // The requester decrypts the packet and finds its ping key echoed back, + // which gates the sensor data — if the key is missing, data is blocked. + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); +} + +TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { + std::vector key(32, 0xBB); + + // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester with ping-pong enabled expects a key that isn't in the packet + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 2953a9fd42..781054eb3b 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1027,6 +1027,73 @@ def test_get_all_dependencies_empty_set() -> None: assert result == set() +def test_get_all_dependencies_platform_component() -> None: + """Platform components (domain.component) are looked up via get_platform, + not get_component.""" + platform_comp = Mock() + platform_comp.dependencies = [] + platform_comp.auto_load = [] + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("helpers.get_platform") as mock_get_platform, + ): + mock_get_platform.return_value = platform_comp + mock_get_component.return_value = None + + result = helpers.get_all_dependencies({"sensor.bthome"}) + + mock_get_platform.assert_called_once_with("sensor", "bthome") + mock_get_component.assert_not_called() + assert result == {"sensor.bthome"} + + +def test_get_all_dependencies_platform_component_with_dependencies() -> None: + """Dependencies of a platform component are resolved transitively.""" + platform_comp = Mock() + platform_comp.dependencies = ["sensor"] + platform_comp.auto_load = [] + + sensor_comp = Mock() + sensor_comp.dependencies = [] + sensor_comp.auto_load = [] + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("helpers.get_platform") as mock_get_platform, + ): + mock_get_platform.return_value = platform_comp + mock_get_component.side_effect = lambda name: ( + sensor_comp if name == "sensor" else None + ) + + result = helpers.get_all_dependencies({"sensor.bthome"}) + + assert result == {"sensor.bthome", "sensor"} + + +def test_get_all_dependencies_cpp_testing_flag() -> None: + """cpp_testing=True propagates to CORE.cpp_testing during resolution.""" + from esphome.core import CORE + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("esphome.loader.get_platform"), + ): + observed: list[bool] = [] + + def capturing_get_component(name: str): + observed.append(CORE.cpp_testing) + + mock_get_component.side_effect = capturing_get_component + + helpers.get_all_dependencies({"some_comp"}, cpp_testing=True) + + assert observed and all(observed), ( + "CORE.cpp_testing should be True during resolution" + ) + + def test_get_components_from_integration_fixtures() -> None: """Test extraction of components from fixture YAML files.""" yaml_content = { diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 174b3fec85..22be59653a 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -841,6 +841,18 @@ class TestEsphomeCore: assert "WiFi" in target.platformio_libraries + def test_testing_ensure_platform_registered__sets_count(self, target): + """Test testing_ensure_platform_registered sets count to 1 for new platform.""" + assert target.platform_counts["sensor"] == 0 + target.testing_ensure_platform_registered("sensor") + assert target.platform_counts["sensor"] == 1 + + def test_testing_ensure_platform_registered__does_not_overwrite(self, target): + """Test testing_ensure_platform_registered preserves existing count.""" + target.platform_counts["sensor"] = 3 + target.testing_ensure_platform_registered("sensor") + assert target.platform_counts["sensor"] == 3 + def test_add_library__extracts_short_name_from_path(self, target): """Test add_library extracts short name from library paths like owner/lib.""" target.data[const.KEY_CORE] = {