Merge branch 'dev' into runtime_stats

This commit is contained in:
J. Nick Koston 2025-07-12 07:21:44 -10:00 committed by GitHub
commit f1b888b309
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 5866 additions and 1926 deletions

1
.clang-tidy.hash Normal file
View File

@ -0,0 +1 @@
a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a

View File

@ -41,7 +41,7 @@ runs:
shell: bash shell: bash
run: | run: |
python -m venv venv python -m venv venv
./venv/Scripts/activate source ./venv/Scripts/activate
python --version python --version
pip install -r requirements.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .

View File

@ -0,0 +1,76 @@
name: Clang-tidy Hash CI
on:
pull_request:
paths:
- ".clang-tidy"
- "platformio.ini"
- "requirements_dev.txt"
- ".clang-tidy.hash"
- "script/clang_tidy_hash.py"
- ".github/workflows/ci-clang-tidy-hash.yml"
permissions:
contents: read
pull-requests: write
jobs:
verify-hash:
name: Verify clang-tidy hash
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.11"
- name: Verify hash
run: |
python script/clang_tidy_hash.py --verify
- if: failure()
name: Show hash details
run: |
python script/clang_tidy_hash.py
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
- if: failure()
name: Request changes
uses: actions/github-script@v7.0.1
with:
script: |
await github.rest.pulls.createReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
event: 'REQUEST_CHANGES',
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
})
- if: success()
name: Dismiss review
uses: actions/github-script@v7.0.1
with:
script: |
let reviews = await github.rest.pulls.listReviews({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
for (let review of reviews.data) {
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
await github.rest.pulls.dismissReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
review_id: review.id,
message: 'Clang-tidy hash now matches configuration.'
});
}
}

View File

@ -66,6 +66,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -87,6 +89,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -108,6 +112,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -129,6 +135,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -204,6 +212,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -213,7 +222,7 @@ jobs:
- name: Run pytest - name: Run pytest
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
./venv/Scripts/activate . ./venv/Scripts/activate.ps1
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest - name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
@ -224,12 +233,61 @@ jobs:
uses: codecov/codecov-action@v5.4.3 uses: codecov/codecov-action@v5.4.3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@v4.2.3
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
determine-jobs:
name: Determine which jobs to run
runs-on: ubuntu-24.04
needs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-format: ${{ steps.determine.outputs.clang-format }}
python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Determine which tests to run
id: determine
env:
GH_TOKEN: ${{ github.token }}
run: |
. venv/bin/activate
output=$(python script/determine-jobs.py)
echo "Test determination output:"
echo "$output" | jq
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-format=$(echo "$output" | jq -r '.clang_format')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
integration-tests: integration-tests:
name: Run integration tests name: Run integration tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -264,6 +322,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.clang-format == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -297,6 +357,10 @@ jobs:
- pylint - pylint
- pytest - pytest
- pyupgrade - pyupgrade
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2
@ -335,6 +399,10 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -346,14 +414,14 @@ jobs:
uses: actions/cache@v4.2.3 uses: actions/cache@v4.2.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.3 uses: actions/cache/restore@v4.2.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers - name: Register problem matchers
run: | run: |
@ -367,10 +435,28 @@ jobs:
mkdir -p .temp mkdir -p .temp
pio run --list-targets -e esp32-idf-tidy pio run --list-targets -e esp32-idf-tidy
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy - name: Run clang-tidy
run: | run: |
. venv/bin/activate . venv/bin/activate
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
fi
env: env:
# Also cache libdeps, store them in a ~/.platformio subfolder # Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
@ -380,59 +466,18 @@ jobs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()
list-components:
runs-on: ubuntu-24.04
needs:
- common
if: github.event_name == 'pull_request'
outputs:
components: ${{ steps.list-components.outputs.components }}
count: ${{ steps.list-components.outputs.count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
- name: Get target branch
id: target-branch
run: |
echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT
- name: Fetch ${{ steps.target-branch.outputs.branch }} branch
run: |
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }}
git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Find changed components
id: list-components
run: |
. venv/bin/activate
components=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }})
output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))')
count=$(echo "$output_components" | jq length)
echo "components=$output_components" >> $GITHUB_OUTPUT
echo "count=$count" >> $GITHUB_OUTPUT
echo "$count Components:"
echo "$output_components" | jq
test-build-components: test-build-components:
name: Component test ${{ matrix.file }} name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- list-components - determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2
matrix: matrix:
file: ${{ fromJson(needs.list-components.outputs.components) }} file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
steps: steps:
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -460,8 +505,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- list-components - determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
outputs: outputs:
matrix: ${{ steps.split.outputs.components }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
@ -470,7 +515,7 @@ jobs:
- name: Split components into 20 groups - name: Split components into 20 groups
id: split id: split
run: | run: |
components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]') components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
echo "components=$components" >> $GITHUB_OUTPUT echo "components=$components" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
@ -478,9 +523,9 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- list-components - determine-jobs
- test-build-components-splitter - test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 4 max-parallel: 4
@ -531,7 +576,7 @@ jobs:
- integration-tests - integration-tests
- pyupgrade - pyupgrade
- clang-tidy - clang-tidy
- list-components - determine-jobs
- test-build-components - test-build-components
- test-build-components-splitter - test-build-components-splitter
- test-build-components-split - test-build-components-split

View File

@ -4,7 +4,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.12.2 rev: v0.12.3
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -48,3 +48,10 @@ repos:
entry: python3 script/run-in-env.py pylint entry: python3 script/run-in-env.py pylint
language: system language: system
types: [python] types: [python]
- id: clang-tidy-hash
name: Update clang-tidy hash
entry: python script/clang_tidy_hash.py --update-if-changed
language: python
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
pass_filenames: false
additional_dependencies: []

View File

@ -28,7 +28,7 @@ esphome/components/aic3204/* @kbx81
esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier esphome/components/alpha3/* @jan-hofmeier
esphome/components/am2315c/* @swoboda1337 esphome/components/am2315c/* @swoboda1337
@ -170,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz
@ -254,6 +255,7 @@ esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita

View File

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

View File

@ -1 +1 @@
CODEOWNERS = ["@jeromelaban"] CODEOWNERS = ["@jeromelaban", "@precurse"]

View File

@ -73,11 +73,29 @@ void AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
} }
AirthingsWavePlus::AirthingsWavePlus() { void AirthingsWavePlus::setup() {
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); const char *service_uuid;
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); const char *characteristic_uuid;
const char *access_control_point_characteristic_uuid;
// Change UUIDs for Wave Radon Gen2
switch (this->wave_device_type_) {
case WaveDeviceType::WAVE_GEN2:
service_uuid = SERVICE_UUID_WAVE_RADON_GEN2;
characteristic_uuid = CHARACTERISTIC_UUID_WAVE_RADON_GEN2;
access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2;
break;
default:
// Wave Plus
service_uuid = SERVICE_UUID;
characteristic_uuid = CHARACTERISTIC_UUID;
access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID;
}
this->service_uuid_ = espbt::ESPBTUUID::from_raw(service_uuid);
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(characteristic_uuid);
this->access_control_point_characteristic_uuid_ = this->access_control_point_characteristic_uuid_ =
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); espbt::ESPBTUUID::from_raw(access_control_point_characteristic_uuid);
} }
} // namespace airthings_wave_plus } // namespace airthings_wave_plus

View File

@ -9,13 +9,20 @@ namespace airthings_wave_plus {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
enum WaveDeviceType : uint8_t { WAVE_PLUS = 0, WAVE_GEN2 = 1 };
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba"; static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba";
static const char *const SERVICE_UUID_WAVE_RADON_GEN2 = "b42e4a8e-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = "b42e4dcc-ade7-11e4-89d3-123b93f75cba";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2 =
"b42e50d8-ade7-11e4-89d3-123b93f75cba";
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
public: public:
AirthingsWavePlus(); void setup() override;
void dump_config() override; void dump_config() override;
@ -23,12 +30,14 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
void set_device_type(WaveDeviceType wave_device_type) { wave_device_type_ = wave_device_type; }
protected: protected:
bool is_valid_radon_value_(uint16_t radon); bool is_valid_radon_value_(uint16_t radon);
bool is_valid_co2_value_(uint16_t co2); bool is_valid_co2_value_(uint16_t co2);
void read_sensors(uint8_t *raw_value, uint16_t value_len) override; void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
WaveDeviceType wave_device_type_{WaveDeviceType::WAVE_PLUS};
sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr};

View File

@ -7,6 +7,7 @@ from esphome.const import (
CONF_ILLUMINANCE, CONF_ILLUMINANCE,
CONF_RADON, CONF_RADON,
CONF_RADON_LONG_TERM, CONF_RADON_LONG_TERM,
CONF_TVOC,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
ICON_RADIOACTIVE, ICON_RADIOACTIVE,
@ -15,6 +16,7 @@ from esphome.const import (
UNIT_LUX, UNIT_LUX,
UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_MILLION,
) )
from esphome.types import ConfigType
DEPENDENCIES = airthings_wave_base.DEPENDENCIES DEPENDENCIES = airthings_wave_base.DEPENDENCIES
@ -25,35 +27,59 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_(
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
) )
CONF_DEVICE_TYPE = "device_type"
WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType")
DEVICE_TYPES = {
"WAVE_PLUS": WaveDeviceType.WAVE_PLUS,
"WAVE_GEN2": WaveDeviceType.WAVE_GEN2,
}
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
{ def validate_wave_gen2_config(config: ConfigType) -> ConfigType:
cv.GenerateID(): cv.declare_id(AirthingsWavePlus), """Validate that Wave Gen2 devices don't have CO2 or TVOC sensors."""
cv.Optional(CONF_RADON): sensor.sensor_schema( if config[CONF_DEVICE_TYPE] == "WAVE_GEN2":
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, if CONF_CO2 in config:
icon=ICON_RADIOACTIVE, raise cv.Invalid("Wave Gen2 devices do not support CO2 sensor")
accuracy_decimals=0, # Check for TVOC in the base schema config
state_class=STATE_CLASS_MEASUREMENT, if CONF_TVOC in config:
), raise cv.Invalid("Wave Gen2 devices do not support TVOC sensor")
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( return config
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0, CONFIG_SCHEMA = cv.All(
state_class=STATE_CLASS_MEASUREMENT, airthings_wave_base.BASE_SCHEMA.extend(
), {
cv.Optional(CONF_CO2): sensor.sensor_schema( cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
unit_of_measurement=UNIT_PARTS_PER_MILLION, cv.Optional(CONF_RADON): sensor.sensor_schema(
accuracy_decimals=0, unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
device_class=DEVICE_CLASS_CARBON_DIOXIDE, icon=ICON_RADIOACTIVE,
state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=0,
), state_class=STATE_CLASS_MEASUREMENT,
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( ),
unit_of_measurement=UNIT_LUX, cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
accuracy_decimals=0, unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
device_class=DEVICE_CLASS_ILLUMINANCE, icon=ICON_RADIOACTIVE,
state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=0,
), state_class=STATE_CLASS_MEASUREMENT,
} ),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_LUX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_DEVICE_TYPE, default="WAVE_PLUS"): cv.enum(
DEVICE_TYPES, upper=True
),
}
),
validate_wave_gen2_config,
) )
@ -73,3 +99,4 @@ async def to_code(config):
if config_illuminance := config.get(CONF_ILLUMINANCE): if config_illuminance := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(config_illuminance) sens = await sensor.new_sensor(config_illuminance)
cg.add(var.set_illuminance(sens)) cg.add(var.set_illuminance(sens))
cg.add(var.set_device_type(config[CONF_DEVICE_TYPE]))

View File

@ -23,7 +23,7 @@ void APDS9960::setup() {
return; return;
} }
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->error_code_ = WRONG_ID; this->error_code_ = WRONG_ID;
this->mark_failed(); this->mark_failed();
return; return;

View File

@ -24,8 +24,9 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VARIABLES, CONF_VARIABLES,
) )
from esphome.core import coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
DOMAIN = "api"
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"] AUTO_LOAD = ["socket"]
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@OttoWinter"]
@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = {
} }
CONF_ENCRYPTION = "encryption" CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay" CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
def validate_encryption_key(value): def validate_encryption_key(value):
@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All(
cv.positive_time_period_milliseconds, cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)), cv.Range(max=cv.TimePeriod(milliseconds=65535)),
), ),
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True single=True
), ),
@ -139,8 +142,11 @@ async def to_code(config):
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
# Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_SERVICES")
if actions := config.get(CONF_ACTIONS, []): if actions := config.get(CONF_ACTIONS, []):
cg.add_define("USE_API_YAML_SERVICES")
for conf in actions: for conf in actions:
template_args = [] template_args = []
func_args = [] func_args = []
@ -317,7 +323,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args):
def FILTER_SOURCE_FILES() -> list[str]: def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled.""" """Filter out api_pb2_dump.cpp when proto message dumping is not enabled
and user_services.cpp when no services are defined."""
files_to_filter = []
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read # This is a particularly large file that still needs to be opened and read
# all the way to the end even when ifdef'd out # all the way to the end even when ifdef'd out
@ -325,6 +334,11 @@ def FILTER_SOURCE_FILES() -> list[str]:
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
# which happens when the logger level is VERY_VERBOSE # which happens when the logger level is VERY_VERBOSE
if get_logger_level() != "VERY_VERBOSE": if get_logger_level() != "VERY_VERBOSE":
return ["api_pb2_dump.cpp"] files_to_filter.append("api_pb2_dump.cpp")
return [] # user_services.cpp is only needed when services are defined
config = CORE.config.get(DOMAIN, {})
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
files_to_filter.append("user_services.cpp")
return files_to_filter

View File

@ -374,6 +374,7 @@ message CoverCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_COVER"; option (ifdef) = "USE_COVER";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
@ -387,6 +388,7 @@ message CoverCommandRequest {
bool has_tilt = 6; bool has_tilt = 6;
float tilt = 7; float tilt = 7;
bool stop = 8; bool stop = 8;
uint32 device_id = 9;
} }
// ==================== FAN ==================== // ==================== FAN ====================
@ -441,6 +443,7 @@ message FanCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_FAN"; option (ifdef) = "USE_FAN";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
bool has_state = 2; bool has_state = 2;
@ -455,6 +458,7 @@ message FanCommandRequest {
int32 speed_level = 11; int32 speed_level = 11;
bool has_preset_mode = 12; bool has_preset_mode = 12;
string preset_mode = 13; string preset_mode = 13;
uint32 device_id = 14;
} }
// ==================== LIGHT ==================== // ==================== LIGHT ====================
@ -523,6 +527,7 @@ message LightCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LIGHT"; option (ifdef) = "USE_LIGHT";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
bool has_state = 2; bool has_state = 2;
@ -551,6 +556,7 @@ message LightCommandRequest {
uint32 flash_length = 17; uint32 flash_length = 17;
bool has_effect = 18; bool has_effect = 18;
string effect = 19; string effect = 19;
uint32 device_id = 28;
} }
// ==================== SENSOR ==================== // ==================== SENSOR ====================
@ -640,9 +646,11 @@ message SwitchCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SWITCH"; option (ifdef) = "USE_SWITCH";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
bool state = 2; bool state = 2;
uint32 device_id = 3;
} }
// ==================== TEXT SENSOR ==================== // ==================== TEXT SENSOR ====================
@ -799,18 +807,21 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_STRING_ARRAY = 7; SERVICE_ARG_TYPE_STRING_ARRAY = 7;
} }
message ListEntitiesServicesArgument { message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_SERVICES";
string name = 1; string name = 1;
ServiceArgType type = 2; ServiceArgType type = 2;
} }
message ListEntitiesServicesResponse { message ListEntitiesServicesResponse {
option (id) = 41; option (id) = 41;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_SERVICES";
string name = 1; string name = 1;
fixed32 key = 2; fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3; repeated ListEntitiesServicesArgument args = 3;
} }
message ExecuteServiceArgument { message ExecuteServiceArgument {
option (ifdef) = "USE_API_SERVICES";
bool bool_ = 1; bool bool_ = 1;
int32 legacy_int = 2; int32 legacy_int = 2;
float float_ = 3; float float_ = 3;
@ -826,6 +837,7 @@ message ExecuteServiceRequest {
option (id) = 42; option (id) = 42;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (no_delay) = true; option (no_delay) = true;
option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1; fixed32 key = 1;
repeated ExecuteServiceArgument args = 2; repeated ExecuteServiceArgument args = 2;
@ -850,12 +862,14 @@ message ListEntitiesCameraResponse {
message CameraImageResponse { message CameraImageResponse {
option (id) = 44; option (id) = 44;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CAMERA"; option (ifdef) = "USE_CAMERA";
fixed32 key = 1; fixed32 key = 1;
bytes data = 2; bytes data = 2;
bool done = 3; bool done = 3;
uint32 device_id = 4;
} }
message CameraImageRequest { message CameraImageRequest {
option (id) = 45; option (id) = 45;
@ -980,6 +994,7 @@ message ClimateCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_CLIMATE"; option (ifdef) = "USE_CLIMATE";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
bool has_mode = 2; bool has_mode = 2;
@ -1005,6 +1020,7 @@ message ClimateCommandRequest {
string custom_preset = 21; string custom_preset = 21;
bool has_target_humidity = 22; bool has_target_humidity = 22;
float target_humidity = 23; float target_humidity = 23;
uint32 device_id = 24;
} }
// ==================== NUMBER ==================== // ==================== NUMBER ====================
@ -1054,9 +1070,11 @@ message NumberCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_NUMBER"; option (ifdef) = "USE_NUMBER";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
float state = 2; float state = 2;
uint32 device_id = 3;
} }
// ==================== SELECT ==================== // ==================== SELECT ====================
@ -1096,9 +1114,11 @@ message SelectCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SELECT"; option (ifdef) = "USE_SELECT";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
string state = 2; string state = 2;
uint32 device_id = 3;
} }
// ==================== SIREN ==================== // ==================== SIREN ====================
@ -1137,6 +1157,7 @@ message SirenCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SIREN"; option (ifdef) = "USE_SIREN";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
bool has_state = 2; bool has_state = 2;
@ -1147,6 +1168,7 @@ message SirenCommandRequest {
uint32 duration = 7; uint32 duration = 7;
bool has_volume = 8; bool has_volume = 8;
float volume = 9; float volume = 9;
uint32 device_id = 10;
} }
// ==================== LOCK ==================== // ==================== LOCK ====================
@ -1201,12 +1223,14 @@ message LockCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LOCK"; option (ifdef) = "USE_LOCK";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
LockCommand command = 2; LockCommand command = 2;
// Not yet implemented: // Not yet implemented:
bool has_code = 3; bool has_code = 3;
string code = 4; string code = 4;
uint32 device_id = 5;
} }
// ==================== BUTTON ==================== // ==================== BUTTON ====================
@ -1232,8 +1256,10 @@ message ButtonCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BUTTON"; option (ifdef) = "USE_BUTTON";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
uint32 device_id = 2;
} }
// ==================== MEDIA PLAYER ==================== // ==================== MEDIA PLAYER ====================
@ -1301,6 +1327,7 @@ message MediaPlayerCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_MEDIA_PLAYER"; option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
@ -1315,6 +1342,7 @@ message MediaPlayerCommandRequest {
bool has_announcement = 8; bool has_announcement = 8;
bool announcement = 9; bool announcement = 9;
uint32 device_id = 10;
} }
// ==================== BLUETOOTH ==================== // ==================== BLUETOOTH ====================
@ -1843,9 +1871,11 @@ message AlarmControlPanelCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
AlarmControlPanelStateCommand command = 2; AlarmControlPanelStateCommand command = 2;
string code = 3; string code = 3;
uint32 device_id = 4;
} }
// ===================== TEXT ===================== // ===================== TEXT =====================
@ -1892,9 +1922,11 @@ message TextCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_TEXT"; option (ifdef) = "USE_TEXT";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
string state = 2; string state = 2;
uint32 device_id = 3;
} }
@ -1936,11 +1968,13 @@ message DateCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATE"; option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
uint32 year = 2; uint32 year = 2;
uint32 month = 3; uint32 month = 3;
uint32 day = 4; uint32 day = 4;
uint32 device_id = 5;
} }
// ==================== DATETIME TIME ==================== // ==================== DATETIME TIME ====================
@ -1981,11 +2015,13 @@ message TimeCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_TIME"; option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
uint32 hour = 2; uint32 hour = 2;
uint32 minute = 3; uint32 minute = 3;
uint32 second = 4; uint32 second = 4;
uint32 device_id = 5;
} }
// ==================== EVENT ==================== // ==================== EVENT ====================
@ -2065,11 +2101,13 @@ message ValveCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VALVE"; option (ifdef) = "USE_VALVE";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
bool has_position = 2; bool has_position = 2;
float position = 3; float position = 3;
bool stop = 4; bool stop = 4;
uint32 device_id = 5;
} }
// ==================== DATETIME DATETIME ==================== // ==================== DATETIME DATETIME ====================
@ -2108,9 +2146,11 @@ message DateTimeCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATETIME"; option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
fixed32 epoch_seconds = 2; fixed32 epoch_seconds = 2;
uint32 device_id = 3;
} }
// ==================== UPDATE ==================== // ==================== UPDATE ====================
@ -2160,7 +2200,9 @@ message UpdateCommandRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_UPDATE"; option (ifdef) = "USE_UPDATE";
option (no_delay) = true; option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
UpdateCommand command = 2; UpdateCommand command = 2;
uint32 device_id = 3;
} }

View File

@ -193,14 +193,15 @@ void APIConnection::loop() {
// If we can't send the ping request directly (tx_buffer full), // 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 // schedule it at the front of the batch so it will be sent with priority
ESP_LOGW(TAG, "Buffer full, ping queued"); ESP_LOGW(TAG, "Buffer full, ping queued");
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE,
PingRequest::ESTIMATED_SIZE);
this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
} }
} }
#ifdef USE_CAMERA #ifdef USE_CAMERA
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) MAX_PACKET_SIZE, this->image_reader_->available()); uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send; bool done = this->image_reader_->available() == to_send;
uint32_t msg_size = 0; uint32_t msg_size = 0;
ProtoSize::add_fixed_field<4>(msg_size, 1, true); ProtoSize::add_fixed_field<4>(msg_size, 1, true);
@ -265,7 +266,7 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
// Encodes a message to the buffer and returns the total number of bytes used, // 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. // including header and footer overhead. Returns 0 if the message doesn't fit.
uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
// If in log-only mode, just log and return // If in log-only mode, just log and return
@ -316,7 +317,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state, return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
BinarySensorStateResponse::MESSAGE_TYPE); BinarySensorStateResponse::MESSAGE_TYPE, BinarySensorStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -343,7 +344,8 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
#ifdef USE_COVER #ifdef USE_COVER
bool APIConnection::send_cover_state(cover::Cover *cover) { bool APIConnection::send_cover_state(cover::Cover *cover) {
return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE,
CoverStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -400,7 +402,8 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#ifdef USE_FAN #ifdef USE_FAN
bool APIConnection::send_fan_state(fan::Fan *fan) { bool APIConnection::send_fan_state(fan::Fan *fan) {
return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE,
FanStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -455,7 +458,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool APIConnection::send_light_state(light::LightState *light) { bool APIConnection::send_light_state(light::LightState *light) {
return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE,
LightStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -543,7 +547,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE,
SensorStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -575,7 +580,8 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool APIConnection::send_switch_state(switch_::Switch *a_switch) { bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE,
SwitchStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -611,7 +617,7 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) { bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state, return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state,
TextSensorStateResponse::MESSAGE_TYPE); TextSensorStateResponse::MESSAGE_TYPE, TextSensorStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -638,7 +644,8 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool APIConnection::send_climate_state(climate::Climate *climate) { bool APIConnection::send_climate_state(climate::Climate *climate) {
return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE); return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE,
ClimateStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -734,7 +741,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool APIConnection::send_number_state(number::Number *number) { bool APIConnection::send_number_state(number::Number *number) {
return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE,
NumberStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -770,7 +778,8 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool APIConnection::send_date_state(datetime::DateEntity *date) { bool APIConnection::send_date_state(datetime::DateEntity *date) {
return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE); return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE,
DateStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -800,7 +809,8 @@ void APIConnection::date_command(const DateCommandRequest &msg) {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool APIConnection::send_time_state(datetime::TimeEntity *time) { bool APIConnection::send_time_state(datetime::TimeEntity *time) {
return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE); return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE,
TimeStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -831,7 +841,7 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state, return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state,
DateTimeStateResponse::MESSAGE_TYPE); DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -862,7 +872,8 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
#ifdef USE_TEXT #ifdef USE_TEXT
bool APIConnection::send_text_state(text::Text *text) { bool APIConnection::send_text_state(text::Text *text) {
return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE,
TextStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -896,7 +907,8 @@ void APIConnection::text_command(const TextCommandRequest &msg) {
#ifdef USE_SELECT #ifdef USE_SELECT
bool APIConnection::send_select_state(select::Select *select) { bool APIConnection::send_select_state(select::Select *select) {
return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE,
SelectStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -944,7 +956,8 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg
#ifdef USE_LOCK #ifdef USE_LOCK
bool APIConnection::send_lock_state(lock::Lock *a_lock) { bool APIConnection::send_lock_state(lock::Lock *a_lock) {
return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE,
LockStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -986,7 +999,8 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
#ifdef USE_VALVE #ifdef USE_VALVE
bool APIConnection::send_valve_state(valve::Valve *valve) { bool APIConnection::send_valve_state(valve::Valve *valve) {
return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE); return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE,
ValveStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1023,7 +1037,7 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) {
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state, return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state,
MediaPlayerStateResponse::MESSAGE_TYPE); MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1262,7 +1276,8 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state, return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
AlarmControlPanelStateResponse::MESSAGE_TYPE); AlarmControlPanelStateResponse::MESSAGE_TYPE,
AlarmControlPanelStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
@ -1316,7 +1331,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#ifdef USE_EVENT #ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const std::string &event_type) { void APIConnection::send_event(event::Event *event, const std::string &event_type) {
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
@ -1341,7 +1357,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) { bool APIConnection::send_update_state(update::UpdateEntity *update) {
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE); return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE,
UpdateStateResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1534,6 +1551,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
} }
} }
} }
#ifdef USE_API_SERVICES
void APIConnection::execute_service(const ExecuteServiceRequest &msg) { void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
bool found = false; bool found = false;
for (auto *service : this->parent_->get_user_services()) { for (auto *service : this->parent_->get_user_services()) {
@ -1545,6 +1563,7 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
ESP_LOGV(TAG, "Could not find service"); ESP_LOGV(TAG, "Could not find service");
} }
} }
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
psk_t psk{}; psk_t psk{};
@ -1588,7 +1607,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
} }
return false; return false;
} }
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
return false; return false;
} }
@ -1622,7 +1641,8 @@ void APIConnection::on_fatal_error() {
this->flags_.remove = true; this->flags_.remove = true;
} }
void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) { void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type,
uint8_t estimated_size) {
// Check if we already have a message of this type for this entity // Check if we already have a message of this type for this entity
// This provides deduplication per entity/message_type combination // This provides deduplication per entity/message_type combination
// O(n) but optimized for RAM and not performance. // O(n) but optimized for RAM and not performance.
@ -1637,12 +1657,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
} }
// No existing item found, add new one // No existing item found, add new one
items.emplace_back(entity, std::move(creator), message_type); items.emplace_back(entity, std::move(creator), message_type, estimated_size);
} }
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) { void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
uint8_t estimated_size) {
// Insert at front for high priority messages (no deduplication check) // Insert at front for high priority messages (no deduplication check)
items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type)); items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type, estimated_size));
} }
bool APIConnection::schedule_batch_() { bool APIConnection::schedule_batch_() {
@ -1714,7 +1735,7 @@ void APIConnection::process_batch_() {
uint32_t total_estimated_size = 0; uint32_t total_estimated_size = 0;
for (size_t i = 0; i < this->deferred_batch_.size(); i++) { for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
const auto &item = this->deferred_batch_[i]; const auto &item = this->deferred_batch_[i];
total_estimated_size += get_estimated_message_size(item.message_type); total_estimated_size += item.estimated_size;
} }
// Calculate total overhead for all messages // Calculate total overhead for all messages
@ -1752,9 +1773,9 @@ void APIConnection::process_batch_() {
// Update tracking variables // Update tracking variables
items_processed++; items_processed++;
// After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
if (items_processed == 1) { if (items_processed == 1) {
remaining_size = MAX_PACKET_SIZE; remaining_size = MAX_BATCH_PACKET_SIZE;
} }
remaining_size -= payload_size; remaining_size -= payload_size;
// Calculate where the next message's header padding will start // Calculate where the next message's header padding will start
@ -1808,7 +1829,7 @@ void APIConnection::process_batch_() {
} }
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single, uint16_t message_type) const { bool is_single, uint8_t message_type) const {
#ifdef USE_EVENT #ifdef USE_EVENT
// Special case: EventResponse uses string pointer // Special case: EventResponse uses string pointer
if (message_type == EventResponse::MESSAGE_TYPE) { if (message_type == EventResponse::MESSAGE_TYPE) {
@ -1839,149 +1860,6 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) {
// Use generated ESTIMATED_SIZE constants from each message type
switch (message_type) {
#ifdef USE_BINARY_SENSOR
case BinarySensorStateResponse::MESSAGE_TYPE:
return BinarySensorStateResponse::ESTIMATED_SIZE;
case ListEntitiesBinarySensorResponse::MESSAGE_TYPE:
return ListEntitiesBinarySensorResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_SENSOR
case SensorStateResponse::MESSAGE_TYPE:
return SensorStateResponse::ESTIMATED_SIZE;
case ListEntitiesSensorResponse::MESSAGE_TYPE:
return ListEntitiesSensorResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_SWITCH
case SwitchStateResponse::MESSAGE_TYPE:
return SwitchStateResponse::ESTIMATED_SIZE;
case ListEntitiesSwitchResponse::MESSAGE_TYPE:
return ListEntitiesSwitchResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_TEXT_SENSOR
case TextSensorStateResponse::MESSAGE_TYPE:
return TextSensorStateResponse::ESTIMATED_SIZE;
case ListEntitiesTextSensorResponse::MESSAGE_TYPE:
return ListEntitiesTextSensorResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_NUMBER
case NumberStateResponse::MESSAGE_TYPE:
return NumberStateResponse::ESTIMATED_SIZE;
case ListEntitiesNumberResponse::MESSAGE_TYPE:
return ListEntitiesNumberResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_TEXT
case TextStateResponse::MESSAGE_TYPE:
return TextStateResponse::ESTIMATED_SIZE;
case ListEntitiesTextResponse::MESSAGE_TYPE:
return ListEntitiesTextResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_SELECT
case SelectStateResponse::MESSAGE_TYPE:
return SelectStateResponse::ESTIMATED_SIZE;
case ListEntitiesSelectResponse::MESSAGE_TYPE:
return ListEntitiesSelectResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_LOCK
case LockStateResponse::MESSAGE_TYPE:
return LockStateResponse::ESTIMATED_SIZE;
case ListEntitiesLockResponse::MESSAGE_TYPE:
return ListEntitiesLockResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_EVENT
case EventResponse::MESSAGE_TYPE:
return EventResponse::ESTIMATED_SIZE;
case ListEntitiesEventResponse::MESSAGE_TYPE:
return ListEntitiesEventResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_COVER
case CoverStateResponse::MESSAGE_TYPE:
return CoverStateResponse::ESTIMATED_SIZE;
case ListEntitiesCoverResponse::MESSAGE_TYPE:
return ListEntitiesCoverResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_FAN
case FanStateResponse::MESSAGE_TYPE:
return FanStateResponse::ESTIMATED_SIZE;
case ListEntitiesFanResponse::MESSAGE_TYPE:
return ListEntitiesFanResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_LIGHT
case LightStateResponse::MESSAGE_TYPE:
return LightStateResponse::ESTIMATED_SIZE;
case ListEntitiesLightResponse::MESSAGE_TYPE:
return ListEntitiesLightResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_CLIMATE
case ClimateStateResponse::MESSAGE_TYPE:
return ClimateStateResponse::ESTIMATED_SIZE;
case ListEntitiesClimateResponse::MESSAGE_TYPE:
return ListEntitiesClimateResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_ESP32_CAMERA
case ListEntitiesCameraResponse::MESSAGE_TYPE:
return ListEntitiesCameraResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_BUTTON
case ListEntitiesButtonResponse::MESSAGE_TYPE:
return ListEntitiesButtonResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_MEDIA_PLAYER
case MediaPlayerStateResponse::MESSAGE_TYPE:
return MediaPlayerStateResponse::ESTIMATED_SIZE;
case ListEntitiesMediaPlayerResponse::MESSAGE_TYPE:
return ListEntitiesMediaPlayerResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
case AlarmControlPanelStateResponse::MESSAGE_TYPE:
return AlarmControlPanelStateResponse::ESTIMATED_SIZE;
case ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE:
return ListEntitiesAlarmControlPanelResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_DATETIME_DATE
case DateStateResponse::MESSAGE_TYPE:
return DateStateResponse::ESTIMATED_SIZE;
case ListEntitiesDateResponse::MESSAGE_TYPE:
return ListEntitiesDateResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_DATETIME_TIME
case TimeStateResponse::MESSAGE_TYPE:
return TimeStateResponse::ESTIMATED_SIZE;
case ListEntitiesTimeResponse::MESSAGE_TYPE:
return ListEntitiesTimeResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_DATETIME_DATETIME
case DateTimeStateResponse::MESSAGE_TYPE:
return DateTimeStateResponse::ESTIMATED_SIZE;
case ListEntitiesDateTimeResponse::MESSAGE_TYPE:
return ListEntitiesDateTimeResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_VALVE
case ValveStateResponse::MESSAGE_TYPE:
return ValveStateResponse::ESTIMATED_SIZE;
case ListEntitiesValveResponse::MESSAGE_TYPE:
return ListEntitiesValveResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_UPDATE
case UpdateStateResponse::MESSAGE_TYPE:
return UpdateStateResponse::ESTIMATED_SIZE;
case ListEntitiesUpdateResponse::MESSAGE_TYPE:
return ListEntitiesUpdateResponse::ESTIMATED_SIZE;
#endif
case ListEntitiesServicesResponse::MESSAGE_TYPE:
return ListEntitiesServicesResponse::ESTIMATED_SIZE;
case ListEntitiesDoneResponse::MESSAGE_TYPE:
return ListEntitiesDoneResponse::ESTIMATED_SIZE;
case DisconnectRequest::MESSAGE_TYPE:
return DisconnectRequest::ESTIMATED_SIZE;
default:
// Fallback for unknown message types
return 24;
}
}
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome
#endif #endif

View File

@ -33,7 +33,7 @@ class APIConnection : public APIServerConnection {
bool send_list_info_done() { bool send_list_info_done() {
return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done, return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done,
ListEntitiesDoneResponse::MESSAGE_TYPE); ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE);
} }
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
@ -195,7 +195,9 @@ class APIConnection : public APIServerConnection {
// TODO // TODO
return {}; return {};
} }
#ifdef USE_API_SERVICES
void execute_service(const ExecuteServiceRequest &msg) override; void execute_service(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
@ -256,7 +258,7 @@ class APIConnection : public APIServerConnection {
} }
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
std::string get_client_combined_info() const { std::string get_client_combined_info() const {
if (this->client_info_ == this->client_peername_) { if (this->client_info_ == this->client_peername_) {
@ -298,7 +300,7 @@ class APIConnection : public APIServerConnection {
} }
// Non-template helper to encode any ProtoMessage // Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@ -443,9 +445,6 @@ class APIConnection : public APIServerConnection {
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
// Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type);
// Batch message method for ping requests // Batch message method for ping requests
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
@ -505,10 +504,10 @@ class APIConnection : public APIServerConnection {
// Call operator - uses message_type to determine union type // Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint16_t message_type) const; uint8_t message_type) const;
// Manual cleanup method - must be called before destruction for string types // Manual cleanup method - must be called before destruction for string types
void cleanup(uint16_t message_type) { void cleanup(uint8_t message_type) {
#ifdef USE_EVENT #ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr; delete data_.string_ptr;
@ -529,11 +528,12 @@ class APIConnection : public APIServerConnection {
struct BatchItem { struct BatchItem {
EntityBase *entity; // Entity pointer EntityBase *entity; // Entity pointer
MessageCreator creator; // Function that creates the message when needed MessageCreator creator; // Function that creates the message when needed
uint16_t message_type; // Message type for overhead calculation uint8_t message_type; // Message type for overhead calculation (max 255)
uint8_t estimated_size; // Estimated message size (max 255 bytes)
// Constructor for creating BatchItem // Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint16_t message_type) BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(std::move(creator)), message_type(message_type) {} : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
}; };
std::vector<BatchItem> items; std::vector<BatchItem> items;
@ -559,9 +559,9 @@ class APIConnection : public APIServerConnection {
} }
// Add item to the batch // Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Add item to the front of the batch (for high priority messages like ping) // Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Clear all items with proper cleanup // Clear all items with proper cleanup
void clear() { void clear() {
@ -630,7 +630,7 @@ class APIConnection : public APIServerConnection {
// to send in one go. This is the maximum size of a single packet // to send in one go. This is the maximum size of a single packet
// that can be sent over the network. // that can be sent over the network.
// This is to avoid fragmentation of the packet. // This is to avoid fragmentation of the packet.
static constexpr size_t MAX_PACKET_SIZE = 1390; // MTU static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU
bool schedule_batch_(); bool schedule_batch_();
void process_batch_(); void process_batch_();
@ -641,9 +641,9 @@ class APIConnection : public APIServerConnection {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
// Helper to log a proto message from a MessageCreator object // Helper to log a proto message from a MessageCreator object
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) { void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) {
this->flags_.log_only_mode = true; this->flags_.log_only_mode = true;
creator(entity, this, MAX_PACKET_SIZE, true, message_type); creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type);
this->flags_.log_only_mode = false; this->flags_.log_only_mode = false;
} }
@ -654,7 +654,8 @@ class APIConnection : public APIServerConnection {
#endif #endif
// Helper method to send a message either immediately or via batching // Helper method to send a message either immediately or via batching
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) { bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
// Try to send immediately if: // Try to send immediately if:
// 1. We should try to send immediately (should_try_send_immediately = true) // 1. We should try to send immediately (should_try_send_immediately = true)
// 2. Batch delay is 0 (user has opted in to immediate sending) // 2. Batch delay is 0 (user has opted in to immediate sending)
@ -662,7 +663,7 @@ class APIConnection : public APIServerConnection {
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
this->helper_->can_write_without_blocking()) { this->helper_->can_write_without_blocking()) {
// Now actually encode and send // Now actually encode and send
if (creator(entity, this, MAX_PACKET_SIZE, true) && if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode // Log the message in verbose mode
@ -675,23 +676,25 @@ class APIConnection : public APIServerConnection {
} }
// Fall back to scheduled batching // Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type); return this->schedule_message_(entity, creator, message_type, estimated_size);
} }
// Helper function to schedule a deferred message with known message type // Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type); this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
return this->schedule_batch_(); return this->schedule_batch_();
} }
// Overload for function pointers (for info messages and current state reads) // Overload for function pointers (for info messages and current state reads)
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
return schedule_message_(entity, MessageCreator(function_ptr), message_type); uint8_t estimated_size) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size);
} }
// Helper function to schedule a high priority message at the front of the batch // Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); uint8_t estimated_size) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
return this->schedule_batch_(); return this->schedule_batch_();
} }
}; };

View File

@ -613,7 +613,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type; buffer->type = type;
return APIError::OK; return APIError::OK;
} }
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption) // Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
PacketInfo packet{type, 0, PacketInfo packet{type, 0,
@ -1002,7 +1002,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = rx_header_parsed_type_; buffer->type = rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)}; PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1)); return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
} }

View File

@ -30,13 +30,11 @@ struct ReadPacketBuffer {
// Packed packet info structure to minimize memory usage // Packed packet info structure to minimize memory usage
struct PacketInfo { struct PacketInfo {
uint16_t message_type; // 2 bytes uint16_t offset; // Offset in buffer where message starts
uint16_t offset; // 2 bytes (sufficient for packet size ~1460 bytes) uint16_t payload_size; // Size of the message payload
uint16_t payload_size; // 2 bytes (up to 65535 bytes) uint8_t message_type; // Message type (0-255)
uint16_t padding; // 2 byte (for alignment)
PacketInfo(uint16_t type, uint16_t off, uint16_t size) PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
: message_type(type), offset(off), payload_size(size), padding(0) {}
}; };
enum class APIError : uint16_t { enum class APIError : uint16_t {
@ -98,7 +96,7 @@ class APIFrameHelper {
} }
// Give this helper a name for logging // Give this helper a name for logging
void set_log_info(std::string info) { info_ = std::move(info); } void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0; virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf packets in a single operation // Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer // packets contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each // The buffer contains all messages with appropriate padding before each
@ -197,7 +195,7 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
// Get the frame header padding required by this protocol // Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; } uint8_t frame_header_padding() override { return frame_header_padding_; }
@ -251,7 +249,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
uint8_t frame_header_padding() override { return frame_header_padding_; } uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol // Get the frame footer size required by this protocol

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -162,6 +162,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
return "UNKNOWN"; return "UNKNOWN";
} }
} }
#ifdef USE_API_SERVICES
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) { template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
switch (value) { switch (value) {
case enums::SERVICE_ARG_TYPE_BOOL: case enums::SERVICE_ARG_TYPE_BOOL:
@ -184,6 +185,7 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
return "UNKNOWN"; return "UNKNOWN";
} }
} }
#endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
switch (value) { switch (value) {
@ -986,6 +988,11 @@ void CoverCommandRequest::dump_to(std::string &out) const {
out.append(" stop: "); out.append(" stop: ");
out.append(YESNO(this->stop)); out.append(YESNO(this->stop));
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -1146,6 +1153,11 @@ void FanCommandRequest::dump_to(std::string &out) const {
out.append(" preset_mode: "); out.append(" preset_mode: ");
out.append("'").append(this->preset_mode).append("'"); out.append("'").append(this->preset_mode).append("'");
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -1419,6 +1431,11 @@ void LightCommandRequest::dump_to(std::string &out) const {
out.append(" effect: "); out.append(" effect: ");
out.append("'").append(this->effect).append("'"); out.append("'").append(this->effect).append("'");
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -1586,6 +1603,11 @@ void SwitchCommandRequest::dump_to(std::string &out) const {
out.append(" state: "); out.append(" state: ");
out.append(YESNO(this->state)); out.append(YESNO(this->state));
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -1791,6 +1813,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
out.append("\n"); out.append("\n");
out.append("}"); out.append("}");
} }
#ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::dump_to(std::string &out) const { void ListEntitiesServicesArgument::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64]; __attribute__((unused)) char buffer[64];
out.append("ListEntitiesServicesArgument {\n"); out.append("ListEntitiesServicesArgument {\n");
@ -1890,6 +1913,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
} }
out.append("}"); out.append("}");
} }
#endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
void ListEntitiesCameraResponse::dump_to(std::string &out) const { void ListEntitiesCameraResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64]; __attribute__((unused)) char buffer[64];
@ -1944,6 +1968,11 @@ void CameraImageResponse::dump_to(std::string &out) const {
out.append(" done: "); out.append(" done: ");
out.append(YESNO(this->done)); out.append(YESNO(this->done));
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
void CameraImageRequest::dump_to(std::string &out) const { void CameraImageRequest::dump_to(std::string &out) const {
@ -2263,6 +2292,11 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); snprintf(buffer, sizeof(buffer), "%g", this->target_humidity);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -2367,6 +2401,11 @@ void NumberCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%g", this->state); snprintf(buffer, sizeof(buffer), "%g", this->state);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -2448,6 +2487,11 @@ void SelectCommandRequest::dump_to(std::string &out) const {
out.append(" state: "); out.append(" state: ");
out.append("'").append(this->state).append("'"); out.append("'").append(this->state).append("'");
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -2563,6 +2607,11 @@ void SirenCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%g", this->volume); snprintf(buffer, sizeof(buffer), "%g", this->volume);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -2658,6 +2707,11 @@ void LockCommandRequest::dump_to(std::string &out) const {
out.append(" code: "); out.append(" code: ");
out.append("'").append(this->code).append("'"); out.append("'").append(this->code).append("'");
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -2711,6 +2765,11 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -2857,6 +2916,11 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
out.append(" announcement: "); out.append(" announcement: ");
out.append(YESNO(this->announcement)); out.append(YESNO(this->announcement));
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -3682,6 +3746,11 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
out.append(" code: "); out.append(" code: ");
out.append("'").append(this->code).append("'"); out.append("'").append(this->code).append("'");
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -3775,6 +3844,11 @@ void TextCommandRequest::dump_to(std::string &out) const {
out.append(" state: "); out.append(" state: ");
out.append("'").append(this->state).append("'"); out.append("'").append(this->state).append("'");
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -3872,6 +3946,11 @@ void DateCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -3969,6 +4048,11 @@ void TimeCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -4138,6 +4222,11 @@ void ValveCommandRequest::dump_to(std::string &out) const {
out.append(" stop: "); out.append(" stop: ");
out.append(YESNO(this->stop)); out.append(YESNO(this->stop));
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -4215,6 +4304,11 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -4323,6 +4417,11 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
out.append(" command: "); out.append(" command: ");
out.append(proto_enum_to_string<enums::UpdateCommand>(this->command)); out.append(proto_enum_to_string<enums::UpdateCommand>(this->command));
out.append("\n"); out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif

View File

@ -195,6 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_home_assistant_state_response(msg); this->on_home_assistant_state_response(msg);
break; break;
} }
#ifdef USE_API_SERVICES
case 42: { case 42: {
ExecuteServiceRequest msg; ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
@ -204,6 +205,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_execute_service_request(msg); this->on_execute_service_request(msg);
break; break;
} }
#endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
case 45: { case 45: {
CameraImageRequest msg; CameraImageRequest msg;
@ -660,11 +662,13 @@ void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
} }
} }
} }
#ifdef USE_API_SERVICES
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_()) {
this->execute_service(msg); this->execute_service(msg);
} }
} }
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_()) {

View File

@ -69,7 +69,9 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_get_time_request(const GetTimeRequest &value){}; virtual void on_get_time_request(const GetTimeRequest &value){};
virtual void on_get_time_response(const GetTimeResponse &value){}; virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_SERVICES
virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
virtual void on_camera_image_request(const CameraImageRequest &value){}; virtual void on_camera_image_request(const CameraImageRequest &value){};
@ -216,7 +218,9 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
#ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif #endif
@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase {
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
void on_get_time_request(const GetTimeRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override;
#ifdef USE_API_SERVICES
void on_execute_service_request(const ExecuteServiceRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif

View File

@ -141,9 +141,9 @@ class ProtoSize {
/** /**
* @brief Calculates and adds the size of an int32 field to the total message size * @brief Calculates and adds the size of an int32 field to the total message size
*/ */
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero and not forced // Skip calculation if value is zero
if (value == 0 && !force) { if (value == 0) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -157,13 +157,26 @@ class ProtoSize {
} }
} }
/**
* @brief Calculates and adds the size of an int32 field to the total message size (repeated field version)
*/
static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/** /**
* @brief Calculates and adds the size of a uint32 field to the total message size * @brief Calculates and adds the size of a uint32 field to the total message size
*/ */
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
bool force = false) { // Skip calculation if value is zero
// Skip calculation if value is zero and not forced if (value == 0) {
if (value == 0 && !force) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -171,12 +184,20 @@ class ProtoSize {
total_size += field_id_size + varint(value); total_size += field_id_size + varint(value);
} }
/**
* @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version)
*/
static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/** /**
* @brief Calculates and adds the size of a boolean field to the total message size * @brief Calculates and adds the size of a boolean field to the total message size
*/ */
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) { static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) {
// Skip calculation if value is false and not forced // Skip calculation if value is false
if (!value && !force) { if (!value) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -184,6 +205,15 @@ class ProtoSize {
total_size += field_id_size + 1; total_size += field_id_size + 1;
} }
/**
* @brief Calculates and adds the size of a boolean field to the total message size (repeated field version)
*/
static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) {
// Always calculate size for repeated fields
// Boolean fields always use 1 byte
total_size += field_id_size + 1;
}
/** /**
* @brief Calculates and adds the size of a fixed field to the total message size * @brief Calculates and adds the size of a fixed field to the total message size
* *
@ -193,10 +223,9 @@ class ProtoSize {
* @param is_nonzero Whether the value is non-zero * @param is_nonzero Whether the value is non-zero
*/ */
template<uint32_t NumBytes> template<uint32_t NumBytes>
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero, static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) {
bool force = false) { // Skip calculation if value is zero
// Skip calculation if value is zero and not forced if (!is_nonzero) {
if (!is_nonzero && !force) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -209,9 +238,9 @@ class ProtoSize {
* *
* Enum fields are encoded as uint32 varints. * Enum fields are encoded as uint32 varints.
*/ */
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) { static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero and not forced // Skip calculation if value is zero
if (value == 0 && !force) { if (value == 0) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -219,14 +248,25 @@ class ProtoSize {
total_size += field_id_size + varint(value); total_size += field_id_size + varint(value);
} }
/**
* @brief Calculates and adds the size of an enum field to the total message size (repeated field version)
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/** /**
* @brief Calculates and adds the size of a sint32 field to the total message size * @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. * Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/ */
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero and not forced // Skip calculation if value is zero
if (value == 0 && !force) { if (value == 0) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -235,12 +275,24 @@ class ProtoSize {
total_size += field_id_size + varint(zigzag); total_size += field_id_size + varint(zigzag);
} }
/**
* @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version)
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/** /**
* @brief Calculates and adds the size of an int64 field to the total message size * @brief Calculates and adds the size of an int64 field to the total message size
*/ */
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero and not forced // Skip calculation if value is zero
if (value == 0 && !force) { if (value == 0) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -248,13 +300,20 @@ class ProtoSize {
total_size += field_id_size + varint(value); total_size += field_id_size + varint(value);
} }
/**
* @brief Calculates and adds the size of an int64 field to the total message size (repeated field version)
*/
static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/** /**
* @brief Calculates and adds the size of a uint64 field to the total message size * @brief Calculates and adds the size of a uint64 field to the total message size
*/ */
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value, static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
bool force = false) { // Skip calculation if value is zero
// Skip calculation if value is zero and not forced if (value == 0) {
if (value == 0 && !force) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -262,14 +321,22 @@ class ProtoSize {
total_size += field_id_size + varint(value); total_size += field_id_size + varint(value);
} }
/**
* @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version)
*/
static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/** /**
* @brief Calculates and adds the size of a sint64 field to the total message size * @brief Calculates and adds the size of a sint64 field to the total message size
* *
* Sint64 fields use ZigZag encoding, which is more efficient for negative values. * Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/ */
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero and not forced // Skip calculation if value is zero
if (value == 0 && !force) { if (value == 0) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -278,13 +345,24 @@ class ProtoSize {
total_size += field_id_size + varint(zigzag); total_size += field_id_size + varint(zigzag);
} }
/**
* @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version)
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
total_size += field_id_size + varint(zigzag);
}
/** /**
* @brief Calculates and adds the size of a string/bytes field to the total message size * @brief Calculates and adds the size of a string/bytes field to the total message size
*/ */
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str, static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
bool force = false) { // Skip calculation if string is empty
// Skip calculation if string is empty and not forced if (str.empty()) {
if (str.empty() && !force) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -293,18 +371,26 @@ class ProtoSize {
total_size += field_id_size + varint(str_size) + str_size; total_size += field_id_size + varint(str_size) + str_size;
} }
/**
* @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version)
*/
static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
// Always calculate size for repeated fields
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/** /**
* @brief Calculates and adds the size of a nested message field to the total message 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 * This helper function directly updates the total_size reference if the nested size
* is greater than zero or force is true. * is greater than zero.
* *
* @param nested_size The pre-calculated size of the nested message * @param nested_size The pre-calculated size of the nested message
*/ */
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size, static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
bool force = false) { // Skip calculation if nested message is empty
// Skip calculation if nested message is empty and not forced if (nested_size == 0) {
if (nested_size == 0 && !force) {
return; // No need to update total_size return; // No need to update total_size
} }
@ -313,6 +399,17 @@ class ProtoSize {
total_size += field_id_size + varint(nested_size) + nested_size; total_size += field_id_size + varint(nested_size) + nested_size;
} }
/**
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
*
* @param nested_size The pre-calculated size of the nested message
*/
static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size for repeated fields
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/** /**
* @brief Calculates and adds the size of a nested message field to the total message size * @brief Calculates and adds the size of a nested message field to the total message size
* *
@ -322,13 +419,26 @@ class ProtoSize {
* *
* @param message The nested message object * @param message The nested message object
*/ */
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message, static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) {
bool force = false) {
uint32_t nested_size = 0; uint32_t nested_size = 0;
message.calculate_size(nested_size); message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size // Use the base implementation with the calculated nested_size
add_message_field(total_size, field_id_size, nested_size, force); add_message_field(total_size, field_id_size, nested_size);
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
*
* @param message The nested message object
*/
static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size,
const ProtoMessage &message) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field_repeated(total_size, field_id_size, nested_size);
} }
/** /**
@ -348,9 +458,9 @@ class ProtoSize {
return; return;
} }
// For repeated fields, always use force=true // Use the repeated field version for all messages
for (const auto &message : messages) { for (const auto &message : messages) {
add_message_object(total_size, field_id_size, message, true); add_message_object_repeated(total_size, field_id_size, message);
} }
} }
}; };

View File

@ -24,14 +24,6 @@ static const char *const TAG = "api";
// APIServer // APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifndef USE_API_YAML_SERVICES
// Global empty vector to avoid guard variables (saves 8 bytes)
// This is initialized at program startup before any threads
static const std::vector<UserServiceDescriptor *> empty_user_services{};
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance() { return empty_user_services; }
#endif
APIServer::APIServer() { APIServer::APIServer() {
global_api_server = this; global_api_server = this;
// Pre-allocate shared write buffer // Pre-allocate shared write buffer
@ -475,7 +467,8 @@ void APIServer::on_shutdown() {
if (!c->send_message(DisconnectRequest())) { if (!c->send_message(DisconnectRequest())) {
// If we can't send the disconnect request directly (tx_buffer full), // 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 // schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE,
DisconnectRequest::ESTIMATED_SIZE);
} }
} }
} }

View File

@ -12,7 +12,9 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "list_entities.h" #include "list_entities.h"
#include "subscribe_state.h" #include "subscribe_state.h"
#ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
#endif
#include <vector> #include <vector>
@ -25,11 +27,6 @@ struct SavedNoisePsk {
} PACKED; // NOLINT } PACKED; // NOLINT
#endif #endif
#ifndef USE_API_YAML_SERVICES
// Forward declaration of helper function
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance();
#endif
class APIServer : public Component, public Controller { class APIServer : public Component, public Controller {
public: public:
APIServer(); APIServer();
@ -112,18 +109,9 @@ class APIServer : public Component, public Controller {
void on_media_player_update(media_player::MediaPlayer *obj) override; void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif #endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { #ifdef USE_API_SERVICES
#ifdef USE_API_YAML_SERVICES void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
// Vector is pre-allocated when services are defined in YAML
this->user_services_.push_back(descriptor);
#else
// Lazy allocate vector on first use for CustomAPIDevice
if (!this->user_services_) {
this->user_services_ = std::make_unique<std::vector<UserServiceDescriptor *>>();
}
this->user_services_->push_back(descriptor);
#endif #endif
}
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void request_time(); void request_time();
#endif #endif
@ -152,17 +140,9 @@ class APIServer : public Component, public Controller {
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f); std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
const std::vector<UserServiceDescriptor *> &get_user_services() const { #ifdef USE_API_SERVICES
#ifdef USE_API_YAML_SERVICES const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
return this->user_services_;
#else
if (this->user_services_) {
return *this->user_services_;
}
// Return reference to global empty instance (no guard needed)
return get_empty_user_services_instance();
#endif #endif
}
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; } Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
@ -194,14 +174,8 @@ class APIServer : public Component, public Controller {
#endif #endif
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
std::vector<HomeAssistantStateSubscription> state_subs_; std::vector<HomeAssistantStateSubscription> state_subs_;
#ifdef USE_API_YAML_SERVICES #ifdef USE_API_SERVICES
// When services are defined in YAML, we know at compile time that services will be registered
std::vector<UserServiceDescriptor *> user_services_; std::vector<UserServiceDescriptor *> user_services_;
#else
// Services can still be registered at runtime by CustomAPIDevice components even when not
// defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common
// case where no services (YAML or custom) are used.
std::unique_ptr<std::vector<UserServiceDescriptor *>> user_services_;
#endif #endif
// Group smaller types together // Group smaller types together

View File

@ -3,10 +3,13 @@
#include <map> #include <map>
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
#endif
namespace esphome { namespace esphome {
namespace api { namespace api {
#ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
public: public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
@ -19,6 +22,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
T *obj_; T *obj_;
void (T::*callback_)(Ts...); void (T::*callback_)(Ts...);
}; };
#endif // USE_API_SERVICES
class CustomAPIDevice { class CustomAPIDevice {
public: public:
@ -46,12 +50,14 @@ class CustomAPIDevice {
* @param name The name of the service to register. * @param name The name of the service to register.
* @param arg_names The name of the arguments for the service, must match the arguments of the function. * @param arg_names The name of the arguments for the service, must match the arguments of the function.
*/ */
#ifdef USE_API_SERVICES
template<typename T, typename... Ts> template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name, void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) { const std::array<std::string, sizeof...(Ts)> &arg_names) {
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#endif
/** Register a custom native API service that will show up in Home Assistant. /** Register a custom native API service that will show up in Home Assistant.
* *
@ -71,10 +77,12 @@ class CustomAPIDevice {
* @param callback The member function to call when the service is triggered. * @param callback The member function to call when the service is triggered.
* @param name The name of the arguments for the service, must match the arguments of the function. * @param name The name of the arguments for the service, must match the arguments of the function.
*/ */
#ifdef USE_API_SERVICES
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#endif
/** Subscribe to the state (or attribute state) of an entity from Home Assistant. /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
* *

View File

@ -83,10 +83,12 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_SERVICES
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response(); auto resp = service->encode_list_service_response();
return this->client_->send_message(resp); return this->client_->send_message(resp);
} }
#endif
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View File

@ -14,7 +14,7 @@ class APIConnection;
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ #define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
ResponseType::MESSAGE_TYPE); \ ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
} }
class ListEntitiesIterator : public ComponentIterator { class ListEntitiesIterator : public ComponentIterator {
@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *entity) override; bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif #endif
#ifdef USE_API_SERVICES
bool on_service(UserServiceDescriptor *service) override; bool on_service(UserServiceDescriptor *service) override;
#endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
bool on_camera(camera::Camera *entity) override; bool on_camera(camera::Camera *entity) override;
#endif #endif

View File

@ -59,7 +59,6 @@ class ProtoVarInt {
uint32_t as_uint32() const { return this->value_; } uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; } uint64_t as_uint64() const { return this->value_; }
bool as_bool() const { return this->value_; } bool as_bool() const { return this->value_; }
template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); }
int32_t as_int32() const { int32_t as_int32() const {
// Not ZigZag encoded // Not ZigZag encoded
return static_cast<int32_t>(this->as_int64()); return static_cast<int32_t>(this->as_int64());
@ -133,15 +132,24 @@ class ProtoVarInt {
uint64_t value_; uint64_t value_;
}; };
// Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage;
class ProtoLengthDelimited { class ProtoLengthDelimited {
public: public:
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); } std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
template<class C> C as_message() const {
auto msg = C(); /**
msg.decode(this->value_, this->length_); * Decode the length-delimited data into an existing ProtoMessage instance.
return msg; *
} * This method allows decoding without templates, enabling use in contexts
* where the message type is not known at compile time. The ProtoMessage's
* decode() method will be called with the raw data and length.
*
* @param msg The ProtoMessage instance to decode into
*/
void decode_to_message(ProtoMessage &msg) const;
protected: protected:
const uint8_t *const value_; const uint8_t *const value_;
@ -263,9 +271,6 @@ class ProtoWriteBuffer {
this->write((value >> 48) & 0xFF); this->write((value >> 48) & 0xFF);
this->write((value >> 56) & 0xFF); this->write((value >> 56) & 0xFF);
} }
template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
}
void encode_float(uint32_t field_id, float value, bool force = false) { void encode_float(uint32_t field_id, float value, bool force = false) {
if (value == 0.0f && !force) if (value == 0.0f && !force)
return; return;
@ -306,18 +311,7 @@ class ProtoWriteBuffer {
} }
this->encode_uint64(field_id, uvalue, force); this->encode_uint64(field_id, uvalue, force);
} }
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
size_t begin = this->buffer_->size();
value.encode(*this);
const uint32_t nested_length = this->buffer_->size() - begin;
// add size varint
std::vector<uint8_t> var;
ProtoVarInt(nested_length).encode(var);
this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end());
}
std::vector<uint8_t> *get_buffer() const { return buffer_; } std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected: protected:
@ -345,6 +339,25 @@ class ProtoMessage {
virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; }
}; };
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
size_t begin = this->buffer_->size();
value.encode(*this);
const uint32_t nested_length = this->buffer_->size() - begin;
// add size varint
std::vector<uint8_t> var;
ProtoVarInt(nested_length).encode(var);
this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end());
}
// Implementation of decode_to_message - must be after ProtoMessage is defined
inline void ProtoLengthDelimited::decode_to_message(ProtoMessage &msg) const {
msg.decode(this->value_, this->length_);
}
template<typename T> const char *proto_enum_to_string(T value); template<typename T> const char *proto_enum_to_string(T value);
class ProtoService { class ProtoService {
@ -363,11 +376,11 @@ class ProtoService {
* @return A ProtoWriteBuffer object with the reserved size. * @return A ProtoWriteBuffer object with the reserved size.
*/ */
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size // Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint16_t message_type) { bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
uint32_t msg_size = 0; uint32_t msg_size = 0;
msg.calculate_size(msg_size); msg.calculate_size(msg_size);

View File

@ -7,6 +7,7 @@
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "api_pb2.h" #include "api_pb2.h"
#ifdef USE_API_SERVICES
namespace esphome { namespace esphome {
namespace api { namespace api {
@ -73,3 +74,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome
#endif // USE_API_SERVICES

View File

@ -53,6 +53,7 @@ void DebugComponent::on_shutdown() {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
if (component != nullptr) { if (component != nullptr) {
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1); strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
buffer[REBOOT_MAX_LEN - 1] = '\0';
} }
ESP_LOGD(TAG, "Storing reboot source: %s", buffer); ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
pref.save(&buffer); pref.save(&buffer);
@ -68,6 +69,7 @@ std::string DebugComponent::get_reset_reason_() {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
char buffer[REBOOT_MAX_LEN]{}; char buffer[REBOOT_MAX_LEN]{};
if (pref.load(&buffer)) { if (pref.load(&buffer)) {
buffer[REBOOT_MAX_LEN - 1] = '\0';
reset_reason = "Reboot request from " + std::string(buffer); reset_reason = "Reboot request from " + std::string(buffer);
} }
} }

View File

@ -1,6 +1,6 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import time from esphome.components import esp32, time
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
@ -116,12 +116,20 @@ def validate_pin_number(value):
return value return value
def validate_config(config): def _validate_ex1_wakeup_mode(value):
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: if value == "ALL_LOW":
raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: if value == "ANY_LOW":
raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") esp32.only_on_variant(
return config supported=[
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
],
msg_prefix="ANY_LOW",
)(value)
return value
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
@ -148,6 +156,7 @@ WAKEUP_PIN_MODES = {
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
EXT1_WAKEUP_MODES = { EXT1_WAKEUP_MODES = {
"ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW,
"ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
"ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
} }
@ -187,16 +196,28 @@ CONFIG_SCHEMA = cv.All(
), ),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
),
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_PINS): cv.ensure_list( cv.Required(CONF_PINS): cv.ensure_list(
pins.internal_gpio_input_pin_schema, validate_pin_number pins.internal_gpio_input_pin_schema, validate_pin_number
), ),
cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True),
_validate_ex1_wakeup_mode,
),
} }
), ),
), ),
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
),
cv.boolean,
),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),

View File

@ -189,7 +189,7 @@ def get_download_types(storage_json):
] ]
def only_on_variant(*, supported=None, unsupported=None): def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
"""Config validator for features only available on some ESP32 variants.""" """Config validator for features only available on some ESP32 variants."""
if supported is not None and not isinstance(supported, list): if supported is not None and not isinstance(supported, list):
supported = [supported] supported = [supported]
@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None):
variant = get_esp32_variant() variant = get_esp32_variant()
if supported is not None and variant not in supported: if supported is not None and variant not in supported:
raise cv.Invalid( raise cv.Invalid(
f"This feature is only available on {', '.join(supported)}" f"{msg_prefix} is only available on {', '.join(supported)}"
) )
if unsupported is not None and variant in unsupported: if unsupported is not None and variant in unsupported:
raise cv.Invalid( raise cv.Invalid(
f"This feature is not available on {', '.join(unsupported)}" f"{msg_prefix} is not available on {', '.join(unsupported)}"
) )
return obj return obj
@ -707,6 +707,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]

View File

@ -114,7 +114,6 @@ void ESP32InternalGPIOPin::setup() {
if (flags_ & gpio::FLAG_OUTPUT) { if (flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(pin_, drive_strength_); gpio_set_drive_capability(pin_, drive_strength_);
} }
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
} }
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {

View File

@ -308,7 +308,7 @@ async def to_code(config):
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
cg.add_define("USE_ESP32_CAMERA") cg.add_define("USE_CAMERA")
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.0.15") add_idf_component(name="espressif/esp32-camera", ref="2.0.15")

View File

@ -109,6 +109,7 @@ void ESP32TouchComponent::loop() {
// Only publish if state changed - this filters out repeated events // Only publish if state changed - this filters out repeated events
if (new_state != child->last_state_) { if (new_state != child->last_state_) {
child->initial_state_published_ = true;
child->last_state_ = new_state; child->last_state_ = new_state;
child->publish_state(new_state); child->publish_state(new_state);
// Original ESP32: ISR only fires when touched, release is detected by timeout // Original ESP32: ISR only fires when touched, release is detected by timeout
@ -175,6 +176,9 @@ void ESP32TouchComponent::on_shutdown() {
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg); ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
uint32_t mask = 0;
touch_ll_read_trigger_status_mask(&mask);
touch_ll_clear_trigger_status_mask();
touch_pad_clear_status(); touch_pad_clear_status();
// INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured
@ -184,6 +188,11 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
// as any pad remains touched. This allows us to detect both new touches and // as any pad remains touched. This allows us to detect both new touches and
// continued touches, but releases must be detected by timeout in the main loop. // continued touches, but releases must be detected by timeout in the main loop.
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
// Process all configured pads to check their current state // Process all configured pads to check their current state
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
// so we must scan all configured pads to find which ones were touched // so we must scan all configured pads to find which ones were touched
@ -201,19 +210,12 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
value = touch_ll_read_raw_data(pad); value = touch_ll_read_raw_data(pad);
} }
// Skip pads with 0 value - they haven't been measured in this cycle // Skip pads that arent in the trigger mask
// This is important: not all pads are measured every interrupt cycle, bool is_touched = (mask >> pad) & 1;
// only those that the hardware has updated if (!is_touched) {
if (value == 0) {
continue; continue;
} }
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
bool is_touched = value < child->get_threshold();
// Always send the current state - the main loop will filter for changes // Always send the current state - the main loop will filter for changes
// We send both touched and untouched states because the ISR doesn't // We send both touched and untouched states because the ISR doesn't
// track previous state (to keep ISR fast and simple) // track previous state (to keep ISR fast and simple)

View File

@ -180,6 +180,7 @@ async def to_code(config):
cg.add(esp8266_ns.setup_preferences()) cg.add(esp8266_ns.setup_preferences())
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_ESP8266") cg.add_build_flag("-DUSE_ESP8266")

View File

@ -342,5 +342,11 @@ async def to_code(config):
cg.add_define("USE_ETHERNET") cg.add_define("USE_ETHERNET")
# Disable WiFi when using Ethernet to save memory
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
# Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
if CORE.using_arduino: if CORE.using_arduino:
cg.add_library("WiFi", None) cg.add_library("WiFi", None)

View File

@ -0,0 +1,68 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "gl_r01_i2c.h"
namespace esphome {
namespace gl_r01_i2c {
static const char *const TAG = "gl_r01_i2c";
// Register definitions from datasheet
static const uint8_t REG_VERSION = 0x00;
static const uint8_t REG_DISTANCE = 0x02;
static const uint8_t REG_TRIGGER = 0x10;
static const uint8_t CMD_TRIGGER = 0xB0;
static const uint8_t RESTART_CMD1 = 0x5A;
static const uint8_t RESTART_CMD2 = 0xA5;
static const uint8_t READ_DELAY = 40; // minimum milliseconds from datasheet to safely read measurement result
void GLR01I2CComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C...");
// Verify sensor presence
if (!this->read_byte_16(REG_VERSION, &this->version_)) {
ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!");
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_);
}
void GLR01I2CComponent::dump_config() {
ESP_LOGCONFIG(TAG, "GL-R01 I2C:");
ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_);
LOG_I2C_DEVICE(this);
LOG_SENSOR(" ", "Distance", this);
}
void GLR01I2CComponent::update() {
// Trigger a new measurement
if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) {
ESP_LOGE(TAG, "Failed to trigger measurement!");
this->status_set_warning();
return;
}
// Schedule reading the result after the read delay
this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); });
}
void GLR01I2CComponent::read_distance_() {
uint16_t distance = 0;
if (!this->read_byte_16(REG_DISTANCE, &distance)) {
ESP_LOGE(TAG, "Failed to read distance value!");
this->status_set_warning();
return;
}
if (distance == 0xFFFF) {
ESP_LOGW(TAG, "Invalid measurement received!");
this->status_set_warning();
} else {
ESP_LOGV(TAG, "Distance: %umm", distance);
this->publish_state(distance);
this->status_clear_warning();
}
}
} // namespace gl_r01_i2c
} // namespace esphome

View File

@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace gl_r01_i2c {
class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent {
public:
void setup() override;
void dump_config() override;
void update() override;
protected:
void read_distance_();
uint16_t version_{0};
};
} // namespace gl_r01_i2c
} // namespace esphome

View File

@ -0,0 +1,36 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER,
)
CODEOWNERS = ["@pkejval"]
DEPENDENCIES = ["i2c"]
gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
GLR01I2CComponent = gl_r01_i2c_ns.class_(
"GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
GLR01I2CComponent,
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x74))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await i2c.register_i2c_device(var, config)

View File

@ -45,3 +45,4 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", "host") cg.add_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")

View File

@ -178,13 +178,8 @@ static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
if (header_footer[i] != buffer[i]) {
return false; // Mismatch in header/footer
}
}
return true; // Valid header/footer
} }
void LD2410Component::dump_config() { void LD2410Component::dump_config() {
@ -300,14 +295,12 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu
if (command_value != nullptr) { if (command_value != nullptr) {
len += command_value_len; len += command_value_len;
} }
uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; // 2 length bytes (low, high) + 2 command bytes (low, high)
uint8_t len_cmd[] = {len, 0x00, command, 0x00};
this->write_array(len_cmd, sizeof(len_cmd)); this->write_array(len_cmd, sizeof(len_cmd));
// command value bytes // command value bytes
if (command_value != nullptr) { if (command_value != nullptr) {
for (uint8_t i = 0; i < command_value_len; i++) { this->write_array(command_value, command_value_len);
this->write_byte(command_value[i]);
}
} }
// frame footer bytes // frame footer bytes
this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
@ -401,7 +394,7 @@ void LD2410Component::handle_periodic_data_() {
/* /*
Moving distance range: 18th byte Moving distance range: 18th byte
Still distance range: 19th byte Still distance range: 19th byte
Moving enery: 20~28th bytes Moving energy: 20~28th bytes
*/ */
for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_move_sensors_.size(); i++) {
sensor::Sensor *s = this->gate_move_sensors_[i]; sensor::Sensor *s = this->gate_move_sensors_[i];
@ -480,7 +473,7 @@ bool LD2410Component::handle_ack_data_() {
ESP_LOGE(TAG, "Invalid status"); ESP_LOGE(TAG, "Invalid status");
return true; return true;
} }
if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) { if (this->buffer_data_[8] || this->buffer_data_[9]) {
ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
return true; return true;
} }
@ -534,8 +527,8 @@ bool LD2410Component::handle_ack_data_() {
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
ESP_LOGV(TAG, ESP_LOGV(TAG,
"Light function is: %s\n" "Light function: %s\n"
"Light threshold is: %u\n" "Light threshold: %u\n"
"Out pin level: %s", "Out pin level: %s",
light_function_str, this->light_threshold_, out_pin_level_str); light_function_str, this->light_threshold_, out_pin_level_str);
#ifdef USE_SELECT #ifdef USE_SELECT
@ -600,7 +593,7 @@ bool LD2410Component::handle_ack_data_() {
break; break;
case CMD_QUERY: { // Query parameters response case CMD_QUERY: { // Query parameters response
if (this->buffer_data_[10] != 0xAA) if (this->buffer_data_[10] != HEADER)
return true; // value head=0xAA return true; // value head=0xAA
#ifdef USE_NUMBER #ifdef USE_NUMBER
/* /*
@ -656,17 +649,11 @@ void LD2410Component::readline_(int readch) {
if (this->buffer_pos_ < 4) { if (this->buffer_pos_ < 4) {
return; // Not enough data to process yet return; // Not enough data to process yet
} }
if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] && if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
this->buffer_data_[this->buffer_pos_ - 3] == DATA_FRAME_FOOTER[1] &&
this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[2] &&
this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[3]) {
ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
this->handle_periodic_data_(); this->handle_periodic_data_();
this->buffer_pos_ = 0; // Reset position index for next message this->buffer_pos_ = 0; // Reset position index for next message
} else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] && } else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
this->buffer_data_[this->buffer_pos_ - 3] == CMD_FRAME_FOOTER[1] &&
this->buffer_data_[this->buffer_pos_ - 2] == CMD_FRAME_FOOTER[2] &&
this->buffer_data_[this->buffer_pos_ - 1] == CMD_FRAME_FOOTER[3]) {
ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
if (this->handle_ack_data_()) { if (this->handle_ack_data_()) {
this->buffer_pos_ = 0; // Reset position index for next message this->buffer_pos_ = 0; // Reset position index for next message
@ -772,7 +759,6 @@ void LD2410Component::set_max_distances_timeout() {
0x00}; 0x00};
this->set_config_mode_(true); this->set_config_mode_(true);
this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value)); this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value));
delay(50); // NOLINT
this->query_parameters_(); this->query_parameters_();
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
this->set_config_mode_(false); this->set_config_mode_(false);
@ -802,7 +788,6 @@ void LD2410Component::set_gate_threshold(uint8_t gate) {
0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, 0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00,
0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00}; 0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00};
this->send_command_(CMD_GATE_SENS, value, sizeof(value)); this->send_command_(CMD_GATE_SENS, value, sizeof(value));
delay(50); // NOLINT
this->query_parameters_(); this->query_parameters_();
this->set_config_mode_(false); this->set_config_mode_(false);
} }
@ -833,7 +818,6 @@ void LD2410Component::set_light_out_control() {
this->set_config_mode_(true); this->set_config_mode_(true);
uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00}; uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00};
this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value));
delay(50); // NOLINT
this->query_light_control_(); this->query_light_control_();
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
this->set_config_mode_(false); this->set_config_mode_(false);

View File

@ -5,10 +5,10 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.binary_sensor"; static const char *const TAG = "ld2420.binary_sensor";
void LD2420BinarySensor::dump_config() { void LD2420BinarySensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); ESP_LOGCONFIG(TAG, "Binary Sensor:");
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
} }

View File

@ -2,7 +2,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
static const char *const TAG = "LD2420.button"; static const char *const TAG = "ld2420.button";
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {

View File

@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple";
// Memory-efficient lookup tables // Memory-efficient lookup tables
struct StringToUint8 { struct StringToUint8 {
const char *str; const char *str;
uint8_t value; const uint8_t value;
}; };
static constexpr StringToUint8 OP_MODE_BY_STR[] = { static constexpr StringToUint8 OP_MODE_BY_STR[] = {
@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = {
// Helper function for lookups // Helper function for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) { for (const auto &entry : arr) {
if (str == entry.str) if (str == entry.str) {
return entry.value; return entry.value;
}
} }
return 0xFF; // Not found return 0xFF; // Not found
} }
@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() {
void LD2420Component::loop() { void LD2420Component::loop() {
// If there is a active send command do not process it here, the send command call will handle it. // If there is a active send command do not process it here, the send command call will handle it.
if (!this->get_cmd_active_()) { while (!this->cmd_active_ && this->available()) {
if (!this->available()) this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
return;
static uint8_t buffer[2048];
static uint8_t rx_data;
while (this->available()) {
rx_data = this->read();
this->readline_(rx_data, buffer, sizeof(buffer));
}
} }
} }
@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() {
// Store average and peak values // Store average and peak values
this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
if (this->gate_peak[gate] < peak) if (this->gate_peak[gate] < peak) {
this->gate_peak[gate] = peak; this->gate_peak[gate] = peak;
}
uint32_t calculated_value = uint32_t calculated_value =
(static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate]))); (static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate])));
@ -403,8 +398,9 @@ void LD2420Component::set_operating_mode(const std::string &state) {
} }
} else { } else {
// Set the current data back so we don't have new data that can be applied in error. // Set the current data back so we don't have new data that can be applied in error.
if (this->get_calibration_()) if (this->get_calibration_()) {
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
}
this->set_calibration_(false); this->set_calibration_(false);
} }
} else { } else {
@ -414,30 +410,32 @@ void LD2420Component::set_operating_mode(const std::string &state) {
} }
void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) {
static int pos = 0; if (rx_data < 0) {
return; // No data available
if (rx_data >= 0) { }
if (pos < len - 1) { if (this->buffer_pos_ < len - 1) {
buffer[pos++] = rx_data; buffer[this->buffer_pos_++] = rx_data;
buffer[pos] = 0; buffer[this->buffer_pos_] = 0;
} else { } else {
pos = 0; // We should never get here, but just in case...
} ESP_LOGW(TAG, "Max command length exceeded; ignoring");
if (pos >= 4) { this->buffer_pos_ = 0;
if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { }
this->set_cmd_active_(false); // Set command state to inactive after responce. if (this->buffer_pos_ < 4) {
this->handle_ack_data_(buffer, pos); return; // Not enough data to process yet
pos = 0; }
} else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { this->cmd_active_ = false; // Set command state to inactive after response
this->handle_simple_mode_(buffer, pos); this->handle_ack_data_(buffer, this->buffer_pos_);
pos = 0; this->buffer_pos_ = 0;
} else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && } else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) &&
(this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
this->handle_energy_mode_(buffer, pos); this->handle_simple_mode_(buffer, this->buffer_pos_);
pos = 0; this->buffer_pos_ = 0;
} } else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
} (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
this->handle_energy_mode_(buffer, this->buffer_pos_);
this->buffer_pos_ = 0;
} }
} }
@ -462,8 +460,9 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) {
// Resonable refresh rate for home assistant database size health // Resonable refresh rate for home assistant database size health
const int32_t current_millis = App.get_loop_component_start_time(); const int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) {
return; return;
}
this->last_periodic_millis = current_millis; this->last_periodic_millis = current_millis;
for (auto &listener : this->listeners_) { for (auto &listener : this->listeners_) {
listener->on_distance(this->get_distance_()); listener->on_distance(this->get_distance_());
@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
} }
} }
outbuf[index] = '\0'; outbuf[index] = '\0';
if (index > 1) if (index > 1) {
this->set_distance_(strtol(outbuf, &endptr, 10)); this->set_distance_(strtol(outbuf, &endptr, 10));
}
if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
// Resonable refresh rate for home assistant database size health // Resonable refresh rate for home assistant database size health
const int32_t current_millis = App.get_loop_component_start_time(); const int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) {
return; return;
}
this->last_normal_periodic_millis = current_millis; this->last_normal_periodic_millis = current_millis;
for (auto &listener : this->listeners_) for (auto &listener : this->listeners_)
listener->on_distance(this->get_distance_()); listener->on_distance(this->get_distance_());
@ -593,11 +594,12 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
int LD2420Component::send_cmd_from_array(CmdFrameT frame) { int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
uint32_t start_millis = millis(); uint32_t start_millis = millis();
uint8_t error = 0; uint8_t error = 0;
uint8_t ack_buffer[64]; uint8_t ack_buffer[MAX_LINE_LENGTH];
uint8_t cmd_buffer[64]; uint8_t cmd_buffer[MAX_LINE_LENGTH];
this->cmd_reply_.ack = false; this->cmd_reply_.ack = false;
if (frame.command != CMD_RESTART) if (frame.command != CMD_RESTART) {
this->set_cmd_active_(true); // Restart does not reply, thus no ack state required. this->cmd_active_ = true;
} // Restart does not reply, thus no ack state required
uint8_t retry = 3; uint8_t retry = 3;
while (retry) { while (retry) {
frame.length = 0; frame.length = 0;
@ -619,9 +621,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
frame.length += sizeof(frame.footer); frame.length += sizeof(frame.footer);
for (uint16_t index = 0; index < frame.length; index++) { this->write_array(cmd_buffer, frame.length);
this->write_byte(cmd_buffer[index]);
}
error = 0; error = 0;
if (frame.command == CMD_RESTART) { if (frame.command == CMD_RESTART) {
@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
while (!this->cmd_reply_.ack) { while (!this->cmd_reply_.ack) {
while (this->available()) { while (this->available()) {
this->readline_(read(), ack_buffer, sizeof(ack_buffer)); this->readline_(this->read(), ack_buffer, sizeof(ack_buffer));
} }
delay_microseconds_safe(1450); delay_microseconds_safe(1450);
// Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT. // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
@ -641,10 +641,12 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
break; break;
} }
} }
if (this->cmd_reply_.ack) if (this->cmd_reply_.ack) {
retry = 0; retry = 0;
if (this->cmd_reply_.error > 0) }
if (this->cmd_reply_.error > 0) {
this->handle_cmd_error(error); this->handle_cmd_error(error);
}
} }
return error; return error;
} }
@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) {
cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.data_length += sizeof(unknown_parm);
cmd_frame.footer = CMD_FRAME_FOOTER; cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
if (this->send_cmd_from_array(cmd_frame) == 0) if (this->send_cmd_from_array(cmd_frame) == 0) {
this->set_mode_(mode); this->set_mode_(mode);
}
} }
void LD2420Component::get_firmware_version_() { void LD2420Component::get_firmware_version_() {
@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) {
#ifdef USE_NUMBER #ifdef USE_NUMBER
void LD2420Component::init_gate_config_numbers() { void LD2420Component::init_gate_config_numbers() {
if (this->gate_timeout_number_ != nullptr) if (this->gate_timeout_number_ != nullptr) {
this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout)); this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout));
if (this->gate_select_number_ != nullptr) }
if (this->gate_select_number_ != nullptr) {
this->gate_select_number_->publish_state(0); this->gate_select_number_->publish_state(0);
if (this->min_gate_distance_number_ != nullptr) }
if (this->min_gate_distance_number_ != nullptr) {
this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate)); this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate));
if (this->max_gate_distance_number_ != nullptr) }
if (this->max_gate_distance_number_ != nullptr) {
this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate)); this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate));
if (this->gate_move_sensitivity_factor_number_ != nullptr) }
if (this->gate_move_sensitivity_factor_number_ != nullptr) {
this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor);
if (this->gate_still_sensitivity_factor_number_ != nullptr) }
if (this->gate_still_sensitivity_factor_number_ != nullptr) {
this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor);
}
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
if (this->gate_still_threshold_numbers_[gate] != nullptr) { if (this->gate_still_threshold_numbers_[gate] != nullptr) {
this->gate_still_threshold_numbers_[gate]->publish_state( this->gate_still_threshold_numbers_[gate]->publish_state(

View File

@ -20,8 +20,9 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const uint8_t TOTAL_GATES = 16;
static const uint8_t CALIBRATE_SAMPLES = 64; static const uint8_t CALIBRATE_SAMPLES = 64;
static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
static const uint8_t TOTAL_GATES = 16;
enum OpMode : uint8_t { enum OpMode : uint8_t {
OP_NORMAL_MODE = 1, OP_NORMAL_MODE = 1,
@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice {
float gate_move_sensitivity_factor{0.5}; float gate_move_sensitivity_factor{0.5};
float gate_still_sensitivity_factor{0.5}; float gate_still_sensitivity_factor{0.5};
int32_t last_periodic_millis = millis(); int32_t last_periodic_millis{0};
int32_t report_periodic_millis = millis(); int32_t report_periodic_millis{0};
int32_t monitor_periodic_millis = millis(); int32_t monitor_periodic_millis{0};
int32_t last_normal_periodic_millis = millis(); int32_t last_normal_periodic_millis{0};
uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES];
uint16_t gate_avg[TOTAL_GATES]; uint16_t gate_avg[TOTAL_GATES];
uint16_t gate_peak[TOTAL_GATES]; uint16_t gate_peak[TOTAL_GATES];
@ -161,8 +162,6 @@ class LD2420Component : public Component, public uart::UARTDevice {
void set_presence_(bool presence) { this->presence_ = presence; }; void set_presence_(bool presence) { this->presence_ = presence; };
uint16_t get_distance_() { return this->distance_; }; uint16_t get_distance_() { return this->distance_; };
void set_distance_(uint16_t distance) { this->distance_ = distance; }; void set_distance_(uint16_t distance) { this->distance_ = distance; };
bool get_cmd_active_() { return this->cmd_active_; };
void set_cmd_active_(bool active) { this->cmd_active_ = active; };
void handle_simple_mode_(const uint8_t *inbuf, int len); void handle_simple_mode_(const uint8_t *inbuf, int len);
void handle_energy_mode_(uint8_t *buffer, int len); void handle_energy_mode_(uint8_t *buffer, int len);
void handle_ack_data_(uint8_t *buffer, int len); void handle_ack_data_(uint8_t *buffer, int len);
@ -181,12 +180,11 @@ class LD2420Component : public Component, public uart::UARTDevice {
std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16); std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16);
#endif #endif
uint32_t max_distance_gate_; uint16_t distance_{0};
uint32_t min_distance_gate_;
uint16_t system_mode_; uint16_t system_mode_;
uint16_t gate_energy_[TOTAL_GATES]; uint16_t gate_energy_[TOTAL_GATES];
uint16_t distance_{0}; uint8_t buffer_pos_{0}; // where to resume processing/populating buffer
uint8_t config_checksum_{0}; uint8_t buffer_data_[MAX_LINE_LENGTH];
char firmware_ver_[8]{"v0.0.0"}; char firmware_ver_[8]{"v0.0.0"};
bool cmd_active_{false}; bool cmd_active_{false};
bool presence_{false}; bool presence_{false};

View File

@ -2,7 +2,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
static const char *const TAG = "LD2420.number"; static const char *const TAG = "ld2420.number";
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {

View File

@ -5,7 +5,7 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.select"; static const char *const TAG = "ld2420.select";
void LD2420Select::control(const std::string &value) { void LD2420Select::control(const std::string &value) {
this->publish_state(value); this->publish_state(value);

View File

@ -5,10 +5,10 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.sensor"; static const char *const TAG = "ld2420.sensor";
void LD2420Sensor::dump_config() { void LD2420Sensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); ESP_LOGCONFIG(TAG, "Sensor:");
LOG_SENSOR(" ", "Distance", this->distance_sensor_); LOG_SENSOR(" ", "Distance", this->distance_sensor_);
} }

View File

@ -5,10 +5,10 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.text_sensor"; static const char *const TAG = "ld2420.text_sensor";
void LD2420TextSensor::dump_config() { void LD2420TextSensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); ESP_LOGCONFIG(TAG, "Text Sensor:");
LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_);
} }

View File

@ -268,6 +268,7 @@ async def component_to_code(config):
# disable library compatibility checks # disable library compatibility checks
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
# include <Arduino.h> in every file # include <Arduino.h> in every file
cg.add_platformio_option("build_src_flags", "-include Arduino.h") cg.add_platformio_option("build_src_flags", "-include Arduino.h")
# dummy version code # dummy version code

View File

View File

@ -0,0 +1,75 @@
#include "lps22.h"
namespace esphome {
namespace lps22 {
static constexpr const char *const TAG = "lps22";
static constexpr uint8_t WHO_AM_I = 0x0F;
static constexpr uint8_t LPS22HB_ID = 0xB1;
static constexpr uint8_t LPS22HH_ID = 0xB3;
static constexpr uint8_t CTRL_REG2 = 0x11;
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
static constexpr uint8_t STATUS = 0x27;
static constexpr uint8_t STATUS_T_DA_MASK = 0b10;
static constexpr uint8_t STATUS_P_DA_MASK = 0b01;
static constexpr uint8_t TEMP_L = 0x2b;
static constexpr uint8_t PRES_OUT_XL = 0x28;
static constexpr uint8_t REF_P_XL = 0x28;
static constexpr uint8_t READ_ATTEMPTS = 10;
static constexpr uint8_t READ_INTERVAL = 5;
static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f;
static constexpr float TEMPERATURE_SCALE = 0.01f;
void LPS22Component::setup() {
uint8_t value = 0x00;
this->read_register(WHO_AM_I, &value, 1);
if (value != LPS22HB_ID && value != LPS22HH_ID) {
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
this->mark_failed();
}
}
void LPS22Component::dump_config() {
ESP_LOGCONFIG(TAG, "LPS22:");
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
void LPS22Component::update() {
uint8_t value = 0x00;
this->read_register(CTRL_REG2, &value, 1);
value |= CTRL_REG2_ONE_SHOT_MASK;
this->write_register(CTRL_REG2, &value, 1);
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
}
RetryResult LPS22Component::try_read_() {
uint8_t value = 0x00;
this->read_register(STATUS, &value, 1);
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
if ((value & expected_status_mask) != expected_status_mask) {
ESP_LOGD(TAG, "STATUS not ready: %x", value);
return RetryResult::RETRY;
}
if (this->temperature_sensor_ != nullptr) {
uint8_t t_buf[2]{0};
this->read_register(TEMP_L, t_buf, 2);
int16_t encoded = static_cast<int16_t>(encode_uint16(t_buf[1], t_buf[0]));
float temp = TEMPERATURE_SCALE * static_cast<float>(encoded);
this->temperature_sensor_->publish_state(temp);
}
if (this->pressure_sensor_ != nullptr) {
uint8_t p_buf[3]{0};
this->read_register(PRES_OUT_XL, p_buf, 3);
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
}
return RetryResult::DONE;
}
} // namespace lps22
} // namespace esphome

View File

@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace lps22 {
class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
void setup() override;
void update() override;
void dump_config() override;
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
RetryResult try_read_();
};
} // namespace lps22
} // namespace esphome

View File

@ -0,0 +1,58 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
)
CODEOWNERS = ["@nagisa"]
DEPENDENCIES = ["i2c"]
lps22 = cg.esphome_ns.namespace("lps22")
LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LPS22Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x5D)) # can also be 0x5C
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))

View File

@ -153,11 +153,15 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
case MQTT_EVENT_DATA: { case MQTT_EVENT_DATA: {
static std::string topic; static std::string topic;
if (!event.topic.empty()) { if (!event.topic.empty()) {
// When a single message arrives as multiple chunks, the topic will be empty
// on any but the first message, leading to event.topic being an empty string.
// To ensure handlers get the correct topic, cache the last seen topic to
// simulate always receiving the topic from underlying library
topic = event.topic; topic = event.topic;
} }
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
this->on_message_.call(!event.topic.empty() ? topic.c_str() : nullptr, event.data.data(), event.data.size(), this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset,
event.current_data_offset, event.total_data_len); event.total_data_len);
} break; } break;
case MQTT_EVENT_ERROR: case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); ESP_LOGE(TAG, "MQTT_EVENT_ERROR");

View File

@ -1,5 +1,6 @@
#include "nfc.h" #include "nfc.h"
#include <cstdio> #include <cstdio>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@ -7,29 +8,9 @@ namespace nfc {
static const char *const TAG = "nfc"; static const char *const TAG = "nfc";
std::string format_uid(std::vector<uint8_t> &uid) { std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); }
char buf[(uid.size() * 2) + uid.size() - 1];
int offset = 0;
for (size_t i = 0; i < uid.size(); i++) {
const char *format = "%02X";
if (i + 1 < uid.size())
format = "%02X-";
offset += sprintf(buf + offset, format, uid[i]);
}
return std::string(buf);
}
std::string format_bytes(std::vector<uint8_t> &bytes) { std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); }
char buf[(bytes.size() * 2) + bytes.size() - 1];
int offset = 0;
for (size_t i = 0; i < bytes.size(); i++) {
const char *format = "%02X";
if (i + 1 < bytes.size())
format = "%02X ";
offset += sprintf(buf + offset, format, bytes[i]);
}
return std::string(buf);
}
uint8_t guess_tag_type(uint8_t uid_length) { uint8_t guess_tag_type(uint8_t uid_length) {
if (uid_length == 4) { if (uid_length == 4) {

View File

@ -2,8 +2,8 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "ndef_record.h"
#include "ndef_message.h" #include "ndef_message.h"
#include "ndef_record.h"
#include "nfc_tag.h" #include "nfc_tag.h"
#include <vector> #include <vector>
@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
std::string format_uid(std::vector<uint8_t> &uid); std::string format_uid(const std::vector<uint8_t> &uid);
std::string format_bytes(std::vector<uint8_t> &bytes); std::string format_bytes(const std::vector<uint8_t> &bytes);
uint8_t guess_tag_type(uint8_t uid_length); uint8_t guess_tag_type(uint8_t uid_length);
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);

View File

@ -1,11 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
from esphome.const import ( import esphome.config_validation as cv
DEVICE_CLASS_ILLUMINANCE, from esphome.const import DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, UNIT_LUX
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@ccutrer"] CODEOWNERS = ["@ccutrer"]

View File

@ -314,6 +314,9 @@ void PacketTransport::send_data_(bool all) {
} }
void PacketTransport::update() { void PacketTransport::update() {
if (!this->ping_pong_enable_) {
return;
}
auto now = millis() / 1000; auto now = millis() / 1000;
if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) {
this->resend_ping_key_ = this->ping_pong_enable_; this->resend_ping_key_ = this->ping_pong_enable_;

View File

@ -165,6 +165,7 @@ async def to_code(config):
# Allow LDF to properly discover dependency including those in preprocessor # Allow LDF to properly discover dependency including those in preprocessor
# conditionals # conditionals
cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("lib_ldf_mode", "chain+")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040") cg.add_build_flag("-DUSE_RP2040")
cg.set_cpp_standard("gnu++20") cg.set_cpp_standard("gnu++20")

View File

@ -5,13 +5,8 @@ from esphome.config_helpers import Extend, Remove, merge_config
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
from esphome.yaml_util import ESPHomeDataBase, make_data_base from esphome.yaml_util import ESPHomeDataBase, make_data_base
from .jinja import (
Jinja, from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja
JinjaStr,
has_jinja,
TemplateError,
TemplateRuntimeError,
)
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -1,6 +1,7 @@
import logging import logging
import math import math
import re import re
import jinja2 as jinja import jinja2 as jinja
from jinja2.nativetypes import NativeEnvironment from jinja2.nativetypes import NativeEnvironment

View File

@ -167,8 +167,8 @@ def validate_config(config):
if config[CONF_MODULATION] == "LORA": if config[CONF_MODULATION] == "LORA":
if config[CONF_BANDWIDTH] not in lora_bws: if config[CONF_BANDWIDTH] not in lora_bws:
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
if config[CONF_PREAMBLE_SIZE] > 0 and config[CONF_PREAMBLE_SIZE] < 6: if config[CONF_PREAMBLE_SIZE] < 6:
raise cv.Invalid("Minimum preamble size is 6 with LORA") raise cv.Invalid("Minimum 'preamble_size' is 6 with LORA")
if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0: if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0:
raise cv.Invalid("Payload length must be set when spreading factor is 6") raise cv.Invalid("Payload length must be set when spreading factor is 6")
else: else:
@ -200,7 +200,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP),
cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4),
cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535), cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535),
cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_RX_START, default=True): cv.boolean, cv.Optional(CONF_RX_START, default=True): cv.boolean,
cv.Required(CONF_RF_SWITCH): cv.boolean, cv.Required(CONF_RF_SWITCH): cv.boolean,

View File

@ -164,8 +164,8 @@ def validate_config(config):
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
if CONF_DIO0_PIN not in config: if CONF_DIO0_PIN not in config:
raise cv.Invalid("Cannot use LoRa without dio0_pin") raise cv.Invalid("Cannot use LoRa without dio0_pin")
if 0 < config[CONF_PREAMBLE_SIZE] < 6: if config[CONF_PREAMBLE_SIZE] < 6:
raise cv.Invalid("Minimum preamble size is 6 with LORA") raise cv.Invalid("Minimum 'preamble_size' is 6 with LORA")
if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0: if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0:
raise cv.Invalid("Payload length must be set when spreading factor is 6") raise cv.Invalid("Payload length must be set when spreading factor is 6")
else: else:

View File

@ -78,7 +78,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred
update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with
multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's
implemented in a more straightforward way for ESP-IDF. Arudino platform will eventually go away and this workaround implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround
can be forgotten. can be forgotten.
*/ */
#ifdef USE_ARDUINO #ifdef USE_ARDUINO

View File

@ -1055,6 +1055,7 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False):
return validator return validator
bps = float_with_unit("bits per second", "(bps|bits/s|bit/s)?")
frequency = float_with_unit("frequency", "(Hz|HZ|hz)?") frequency = float_with_unit("frequency", "(Hz|HZ|hz)?")
resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?") resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?")
current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")

View File

@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2025.7.0-dev" __version__ = "2025.8.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@ -4,6 +4,8 @@
#ifdef USE_API #ifdef USE_API
#include "esphome/components/api/api_server.h" #include "esphome/components/api/api_server.h"
#endif
#ifdef USE_API_SERVICES
#include "esphome/components/api/user_services.h" #include "esphome/components/api/user_services.h"
#endif #endif
@ -148,7 +150,7 @@ void ComponentIterator::advance() {
} }
break; break;
#endif #endif
#ifdef USE_API #ifdef USE_API_SERVICES
case IteratorState ::SERVICE: case IteratorState ::SERVICE:
if (this->at_ >= api::global_api_server->get_user_services().size()) { if (this->at_ >= api::global_api_server->get_user_services().size()) {
advance_platform = true; advance_platform = true;
@ -383,7 +385,7 @@ void ComponentIterator::advance() {
} }
bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_end() { return true; }
bool ComponentIterator::on_begin() { return true; } bool ComponentIterator::on_begin() { return true; }
#ifdef USE_API #ifdef USE_API_SERVICES
bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; }
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA

View File

@ -10,7 +10,7 @@
namespace esphome { namespace esphome {
#ifdef USE_API #ifdef USE_API_SERVICES
namespace api { namespace api {
class UserServiceDescriptor; class UserServiceDescriptor;
} // namespace api } // namespace api
@ -45,7 +45,7 @@ class ComponentIterator {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
#endif #endif
#ifdef USE_API #ifdef USE_API_SERVICES
virtual bool on_service(api::UserServiceDescriptor *service); virtual bool on_service(api::UserServiceDescriptor *service);
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
@ -122,7 +122,7 @@ class ComponentIterator {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
TEXT_SENSOR, TEXT_SENSOR,
#endif #endif
#ifdef USE_API #ifdef USE_API_SERVICES
SERVICE, SERVICE,
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA

View File

@ -108,7 +108,7 @@
#define USE_API_CLIENT_DISCONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER
#define USE_API_NOISE #define USE_API_NOISE
#define USE_API_PLAINTEXT #define USE_API_PLAINTEXT
#define USE_API_YAML_SERVICES #define USE_API_SERVICES
#define USE_MD5 #define USE_MD5
#define USE_MQTT #define USE_MQTT
#define USE_NETWORK #define USE_NETWORK

View File

@ -187,6 +187,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
# No name to validate # No name to validate
return config return config
# Skip validation for internal entities
# Internal entities are not exposed to Home Assistant and don't use the hash-based
# entity state tracking system, so name collisions don't matter for them
if config.get(CONF_INTERNAL, False):
return config
# Get the entity name # Get the entity name
entity_name = config[CONF_NAME] entity_name = config[CONF_NAME]

View File

@ -263,7 +263,7 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
return ""; return "";
std::string ret; std::string ret;
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
ret.resize(multiple * length - 1); ret.resize(multiple * length - (separator ? 1 : 0));
for (size_t i = 0; i < length; i++) { for (size_t i = 0; i < length; i++) {
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
@ -283,7 +283,7 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato
return ""; return "";
std::string ret; std::string ret;
uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise
ret.resize(multiple * length - 1); ret.resize(multiple * length - (separator ? 1 : 0));
for (size_t i = 0; i < length; i++) { for (size_t i = 0; i < length; i++) {
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
@ -304,7 +304,7 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show
return ""; return "";
std::string ret; std::string ret;
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
ret.resize(multiple * data.length() - 1); ret.resize(multiple * data.length() - (separator ? 1 : 0));
for (size_t i = 0; i < data.length(); i++) { for (size_t i = 0; i < data.length(); i++) {
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);

View File

@ -66,10 +66,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
if (delay == SCHEDULER_DONT_RUN) { if (delay == SCHEDULER_DONT_RUN) {
// Still need to cancel existing timer if name is not empty // Still need to cancel existing timer if name is not empty
if (this->is_name_valid_(name_cstr)) { LockGuard guard{this->lock_};
LockGuard guard{this->lock_}; this->cancel_item_locked_(component, name_cstr, type);
this->cancel_item_locked_(component, name_cstr, type);
}
return; return;
} }
@ -125,10 +123,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
LockGuard guard{this->lock_}; LockGuard guard{this->lock_};
// If name is provided, do atomic cancel-and-add // If name is provided, do atomic cancel-and-add
if (this->is_name_valid_(name_cstr)) { // Cancel existing items
// Cancel existing items this->cancel_item_locked_(component, name_cstr, type);
this->cancel_item_locked_(component, name_cstr, type);
}
// Add new item directly to to_add_ // Add new item directly to to_add_
// since we have the lock held // since we have the lock held
this->to_add_.push_back(std::move(item)); this->to_add_.push_back(std::move(item));
@ -442,10 +438,6 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
// Get the name as const char* // Get the name as const char*
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
// Handle null or empty names
if (!this->is_name_valid_(name_cstr))
return false;
// obtain lock because this function iterates and can be called from non-loop task context // obtain lock because this function iterates and can be called from non-loop task context
LockGuard guard{this->lock_}; LockGuard guard{this->lock_};
return this->cancel_item_locked_(component, name_cstr, type); return this->cancel_item_locked_(component, name_cstr, type);
@ -453,6 +445,11 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
// Helper to cancel items by name - must be called with lock held // Helper to cancel items by name - must be called with lock held
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) { bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
// Early return if name is invalid - no items to cancel
if (name_cstr == nullptr || name_cstr[0] == '\0') {
return false;
}
size_t total_cancelled = 0; size_t total_cancelled = 0;
// Check all containers for matching items // Check all containers for matching items

View File

@ -150,9 +150,6 @@ class Scheduler {
return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str(); return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
} }
// Helper to check if a name is valid (not null and not empty)
inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; }
// Common implementation for cancel operations // Common implementation for cancel operations
bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);

View File

@ -411,7 +411,7 @@ def wizard(path):
safe_print("Options:") safe_print("Options:")
for board_id, board_data in boards_list: for board_id, board_data in boards_list:
safe_print(f" - {board_id} - {board_data['name']}") safe_print(f" - {board_id} - {board_data['name']}")
boards.append(board_id) boards.append(board_id.lower())
while True: while True:
board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): ")) board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): "))

View File

@ -61,6 +61,7 @@ src_filter =
+<../tests/dummy_main.cpp> +<../tests/dummy_main.cpp>
+<../.temp/all-include.cpp> +<../.temp/all-include.cpp>
lib_ldf_mode = off lib_ldf_mode = off
lib_compat_mode = strict
; This are common settings for all Arduino-framework based environments. ; This are common settings for all Arduino-framework based environments.
[common:arduino] [common:arduino]

View File

@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.9.0 esptool==4.9.0
click==8.1.7 click==8.1.7
esphome-dashboard==20250514.0 esphome-dashboard==20250514.0
aioesphomeapi==34.1.0 aioesphomeapi==34.2.0
zeroconf==0.147.0 zeroconf==0.147.0
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import ruamel.yaml==0.18.14 # dashboard_import

View File

@ -1,6 +1,6 @@
pylint==3.3.7 pylint==3.3.7
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.12.2 # also change in .pre-commit-config.yaml when updating ruff==0.12.3 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit

View File

@ -249,6 +249,42 @@ class TypeInfo(ABC):
return 4 # 28 bits return 4 # 28 bits
return 5 # 32 bits (maximum for uint32_t) return 5 # 32 bits (maximum for uint32_t)
def _get_simple_size_calculation(
self, name: str, force: bool, base_method: str, value_expr: str = None
) -> str:
"""Helper for simple size calculations.
Args:
name: Field name
force: Whether this is for a repeated field
base_method: Base method name (e.g., "add_int32_field")
value_expr: Optional value expression (defaults to name)
"""
field_id_size = self.calculate_field_id_size()
method = f"{base_method}_repeated" if force else base_method
value = value_expr if value_expr else name
return f"ProtoSize::{method}(total_size, {field_id_size}, {value});"
def _get_fixed_size_calculation(
self, name: str, force: bool, num_bytes: int, zero_check: str
) -> str:
"""Helper for fixed-size field calculations.
Args:
name: Field name
force: Whether this is for a repeated field
num_bytes: Number of bytes (4 or 8)
zero_check: Expression to check for zero value (e.g., "!= 0.0f")
"""
field_id_size = self.calculate_field_id_size()
# Fixed-size repeated fields are handled differently in RepeatedTypeInfo
# so we should never get force=True here
assert not force, (
"Fixed-size repeated fields should be handled by RepeatedTypeInfo"
)
method = f"add_fixed_field<{num_bytes}>"
return f"ProtoSize::{method}(total_size, {field_id_size}, {name} {zero_check});"
@abstractmethod @abstractmethod
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
"""Calculate the size needed for encoding this field. """Calculate the size needed for encoding this field.
@ -258,6 +294,14 @@ class TypeInfo(ABC):
force: Whether to force encoding the field even if it has a default value force: Whether to force encoding the field even if it has a default value
""" """
def get_fixed_size_bytes(self) -> int | None:
"""Get the number of bytes for fixed-size fields (float, double, fixed32, etc).
Returns:
The number of bytes (4 or 8) for fixed-size fields, None for variable-size fields.
"""
return None
@abstractmethod @abstractmethod
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
"""Get estimated size in bytes for this field with typical values. """Get estimated size in bytes for this field with typical values.
@ -295,9 +339,10 @@ class DoubleType(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_fixed_size_calculation(name, force, 8, "!= 0.0")
o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0.0, {force_str(force)});"
return o def get_fixed_size_bytes(self) -> int:
return 8
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes for double return self.calculate_field_id_size() + 8 # field ID + 8 bytes for double
@ -317,9 +362,10 @@ class FloatType(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_fixed_size_calculation(name, force, 4, "!= 0.0f")
o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0.0f, {force_str(force)});"
return o def get_fixed_size_bytes(self) -> int:
return 4
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 4 # field ID + 4 bytes for float return self.calculate_field_id_size() + 4 # field ID + 4 bytes for float
@ -339,9 +385,7 @@ class Int64Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_int64_field")
o = f"ProtoSize::add_int64_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@ -361,9 +405,7 @@ class UInt64Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_uint64_field")
o = f"ProtoSize::add_uint64_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@ -383,9 +425,7 @@ class Int32Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_int32_field")
o = f"ProtoSize::add_int32_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@ -405,9 +445,10 @@ class Fixed64Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_fixed_size_calculation(name, force, 8, "!= 0")
o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0, {force_str(force)});"
return o def get_fixed_size_bytes(self) -> int:
return 8
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes fixed return self.calculate_field_id_size() + 8 # field ID + 8 bytes fixed
@ -427,9 +468,10 @@ class Fixed32Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_fixed_size_calculation(name, force, 4, "!= 0")
o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0, {force_str(force)});"
return o def get_fixed_size_bytes(self) -> int:
return 4
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 4 # field ID + 4 bytes fixed return self.calculate_field_id_size() + 4 # field ID + 4 bytes fixed
@ -448,9 +490,7 @@ class BoolType(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_bool_field")
o = f"ProtoSize::add_bool_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 1 # field ID + 1 byte return self.calculate_field_id_size() + 1 # field ID + 1 byte
@ -471,9 +511,7 @@ class StringType(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_string_field")
o = f"ProtoSize::add_string_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
@ -498,20 +536,33 @@ class MessageType(TypeInfo):
@property @property
def encode_func(self) -> str: def encode_func(self) -> str:
return f"encode_message<{self.cpp_type}>" return "encode_message"
@property @property
def decode_length(self) -> str: def decode_length(self) -> str:
return f"value.as_message<{self.cpp_type}>()" # Override to return None for message types because we can't use template-based
# decoding when the specific message type isn't known at compile time.
# Instead, we use the non-template decode_to_message() method which allows
# runtime polymorphism through virtual function calls.
return None
@property
def decode_length_content(self) -> str:
# Custom decode that doesn't use templates
return dedent(
f"""\
case {self.number}: {{
value.decode_to_message(this->{self.field_name});
return true;
}}"""
)
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
o = f"{name}.dump_to(out);" o = f"{name}.dump_to(out);"
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_message_object")
o = f"ProtoSize::add_message_object(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return ( return (
@ -538,9 +589,7 @@ class BytesType(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_string_field")
o = f"ProtoSize::add_string_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
@ -560,9 +609,7 @@ class UInt32Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_uint32_field")
o = f"ProtoSize::add_uint32_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@ -576,23 +623,27 @@ class EnumType(TypeInfo):
@property @property
def decode_varint(self) -> str: def decode_varint(self) -> str:
return f"value.as_enum<{self.cpp_type}>()" return f"static_cast<{self.cpp_type}>(value.as_uint32())"
default_value = "" default_value = ""
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@property @property
def encode_func(self) -> str: def encode_func(self) -> str:
return f"encode_enum<{self.cpp_type}>" return "encode_uint32"
@property
def encode_content(self) -> str:
return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));"
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));"
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(
o = f"ProtoSize::add_enum_field(total_size, {field_id_size}, static_cast<uint32_t>({name}), {force_str(force)});" name, force, "add_enum_field", f"static_cast<uint32_t>({name})"
return o )
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 1 # field ID + 1 byte typical enum return self.calculate_field_id_size() + 1 # field ID + 1 byte typical enum
@ -612,9 +663,10 @@ class SFixed32Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_fixed_size_calculation(name, force, 4, "!= 0")
o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0, {force_str(force)});"
return o def get_fixed_size_bytes(self) -> int:
return 4
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 4 # field ID + 4 bytes fixed return self.calculate_field_id_size() + 4 # field ID + 4 bytes fixed
@ -634,9 +686,10 @@ class SFixed64Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_fixed_size_calculation(name, force, 8, "!= 0")
o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0, {force_str(force)});"
return o def get_fixed_size_bytes(self) -> int:
return 8
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes fixed return self.calculate_field_id_size() + 8 # field ID + 8 bytes fixed
@ -656,9 +709,7 @@ class SInt32Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_sint32_field")
o = f"ProtoSize::add_sint32_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@ -678,9 +729,7 @@ class SInt64Type(TypeInfo):
return o return o
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() return self._get_simple_size_calculation(name, force, "add_sint64_field")
o = f"ProtoSize::add_sint64_field(total_size, {field_id_size}, {name}, {force_str(force)});"
return o
def get_estimated_size(self) -> int: def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@ -727,6 +776,16 @@ class RepeatedTypeInfo(TypeInfo):
@property @property
def decode_length_content(self) -> str: def decode_length_content(self) -> str:
content = self._ti.decode_length content = self._ti.decode_length
if content is None and isinstance(self._ti, MessageType):
# Special handling for non-template message decoding
return dedent(
f"""\
case {self.number}: {{
this->{self.field_name}.emplace_back();
value.decode_to_message(this->{self.field_name}.back());
return true;
}}"""
)
if content is None: if content is None:
return None return None
return dedent( return dedent(
@ -771,7 +830,10 @@ class RepeatedTypeInfo(TypeInfo):
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n"
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" if isinstance(self._ti, EnumType):
o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n"
else:
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
o += "}" o += "}"
return o return o
@ -795,11 +857,23 @@ class RepeatedTypeInfo(TypeInfo):
field_id_size = self._ti.calculate_field_id_size() field_id_size = self._ti.calculate_field_id_size()
o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});" o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});"
return o return o
# For other repeated types, use the underlying type's size calculation with force=True # For other repeated types, use the underlying type's size calculation with force=True
o = f"if (!{name}.empty()) {{\n" o = f"if (!{name}.empty()) {{\n"
o += f" for (const auto {'' if self._ti_is_bool else '&'}it : {name}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n" # Check if this is a fixed-size type by seeing if it has a fixed byte count
o += " }\n" num_bytes = self._ti.get_fixed_size_bytes()
if num_bytes is not None:
# Fixed types have constant size per element, so we can multiply
field_id_size = self._ti.calculate_field_id_size()
# Pre-calculate the total bytes per element
bytes_per_element = field_id_size + num_bytes
o += f" total_size += {name}.size() * {bytes_per_element};\n"
else:
# Other types need the actual value
o += f" for (const auto {'' if self._ti_is_bool else '&'}it : {name}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n"
o += " }\n"
o += "}" o += "}"
return o return o
@ -987,13 +1061,24 @@ def build_message_type(
# Add MESSAGE_TYPE method if this is a service message # Add MESSAGE_TYPE method if this is a service message
if message_id is not None: if message_id is not None:
# Validate that message_id fits in uint8_t
if message_id > 255:
raise ValueError(
f"Message ID {message_id} for {desc.name} exceeds uint8_t maximum (255)"
)
# Add static constexpr for message type # Add static constexpr for message type
public_content.append(f"static constexpr uint16_t MESSAGE_TYPE = {message_id};") public_content.append(f"static constexpr uint8_t MESSAGE_TYPE = {message_id};")
# Add estimated size constant # Add estimated size constant
estimated_size = calculate_message_estimated_size(desc) estimated_size = calculate_message_estimated_size(desc)
# Validate that estimated_size fits in uint8_t
if estimated_size > 255:
raise ValueError(
f"Estimated size {estimated_size} for {desc.name} exceeds uint8_t maximum (255)"
)
public_content.append( public_content.append(
f"static constexpr uint16_t ESTIMATED_SIZE = {estimated_size};" f"static constexpr uint8_t ESTIMATED_SIZE = {estimated_size};"
) )
# Add message_name method inline in header # Add message_name method inline in header
@ -1701,7 +1786,6 @@ static const char *const TAG = "api.service";
exec_clang_format(root / "api_pb2_service.cpp") exec_clang_format(root / "api_pb2_service.cpp")
exec_clang_format(root / "api_pb2.h") exec_clang_format(root / "api_pb2.h")
exec_clang_format(root / "api_pb2.cpp") exec_clang_format(root / "api_pb2.cpp")
exec_clang_format(root / "api_pb2_dump.h")
exec_clang_format(root / "api_pb2_dump.cpp") exec_clang_format(root / "api_pb2_dump.cpp")
except ImportError: except ImportError:
pass pass

View File

@ -270,7 +270,7 @@ def lint_newline(fname):
return "File contains Windows newline. Please set your editor to Unix newline mode." return "File contains Windows newline. Please set your editor to Unix newline mode."
@lint_content_check(exclude=["*.svg"]) @lint_content_check(exclude=["*.svg", ".clang-tidy.hash"])
def lint_end_newline(fname, content): def lint_end_newline(fname, content):
if content and not content.endswith("\n"): if content and not content.endswith("\n"):
return "File does not end with a newline, please add an empty line at the end of the file." return "File does not end with a newline, please add an empty line at the end of the file."

View File

@ -22,6 +22,7 @@ from helpers import (
git_ls_files, git_ls_files,
load_idedata, load_idedata,
print_error_for_file, print_error_for_file,
print_file_list,
root_path, root_path,
temp_header_file, temp_header_file,
) )
@ -218,13 +219,14 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
idedata = load_idedata(args.environment)
options = clang_options(idedata)
files = [] files = []
for path in git_ls_files(["*.cpp"]): for path in git_ls_files(["*.cpp"]):
files.append(os.path.relpath(path, os.getcwd())) files.append(os.path.relpath(path, os.getcwd()))
# Print initial file count if it's large
if len(files) > 50:
print(f"Found {len(files)} total files to process")
if args.files: if args.files:
# Match against files specified on command-line # Match against files specified on command-line
file_name_re = re.compile("|".join(args.files)) file_name_re = re.compile("|".join(args.files))
@ -240,10 +242,28 @@ def main():
if args.split_num: if args.split_num:
files = split_list(files, args.split_num)[args.split_at - 1] files = split_list(files, args.split_num)[args.split_at - 1]
print(f"Split {args.split_at}/{args.split_num}: checking {len(files)} files")
# Print file count before adding header file
print(f"\nTotal files to check: {len(files)}")
# Early exit if no files to check
if not files:
print("No files to check - exiting early")
return 0
# Only build header file if we have actual files to check
if args.all_headers and args.split_at in (None, 1): if args.all_headers and args.split_at in (None, 1):
build_all_include() build_all_include()
files.insert(0, temp_header_file) files.insert(0, temp_header_file)
print(f"Added all-include header file, new total: {len(files)}")
# Print final file list before loading idedata
print_file_list(files, "Final files to process:")
# Load idedata and options only if we have files to check
idedata = load_idedata(args.environment)
options = clang_options(idedata)
tmpdir = None tmpdir = None
if args.fix: if args.fix:

188
script/clang_tidy_hash.py Executable file
View File

@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Calculate and manage hash for clang-tidy configuration."""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path
import re
import sys
# Add the script directory to path to import helpers
script_dir = Path(__file__).parent
sys.path.insert(0, str(script_dir))
def read_file_lines(path: Path) -> list[str]:
"""Read lines from a file."""
with open(path) as f:
return f.readlines()
def parse_requirement_line(line: str) -> tuple[str, str] | None:
"""Parse a requirement line and return (package, original_line) or None.
Handles formats like:
- package==1.2.3
- package==1.2.3 # comment
- package>=1.2.3,<2.0.0
"""
original_line = line.strip()
# Extract the part before any comment for parsing
parse_line = line
if "#" in parse_line:
parse_line = parse_line[: parse_line.index("#")]
parse_line = parse_line.strip()
if not parse_line:
return None
# Use regex to extract package name
# This matches package names followed by version operators
match = re.match(r"^([a-zA-Z0-9_-]+)(==|>=|<=|>|<|!=|~=)(.+)$", parse_line)
if match:
return (match.group(1), original_line) # Return package name and original line
return None
def get_clang_tidy_version_from_requirements() -> str:
"""Get clang-tidy version from requirements_dev.txt"""
requirements_path = Path(__file__).parent.parent / "requirements_dev.txt"
lines = read_file_lines(requirements_path)
for line in lines:
parsed = parse_requirement_line(line)
if parsed and parsed[0] == "clang-tidy":
# Return the original line (preserves comments)
return parsed[1]
return "clang-tidy version not found"
def extract_platformio_flags() -> str:
"""Extract clang-tidy related flags from platformio.ini"""
flags: list[str] = []
in_clangtidy_section = False
platformio_path = Path(__file__).parent.parent / "platformio.ini"
lines = read_file_lines(platformio_path)
for line in lines:
line = line.strip()
if line.startswith("[flags:clangtidy]"):
in_clangtidy_section = True
continue
elif line.startswith("[") and in_clangtidy_section:
break
elif in_clangtidy_section and line and not line.startswith("#"):
flags.append(line)
return "\n".join(sorted(flags))
def read_file_bytes(path: Path) -> bytes:
"""Read bytes from a file."""
with open(path, "rb") as f:
return f.read()
def calculate_clang_tidy_hash() -> str:
"""Calculate hash of clang-tidy configuration and version"""
hasher = hashlib.sha256()
# Hash .clang-tidy file
clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy"
content = read_file_bytes(clang_tidy_path)
hasher.update(content)
# Hash clang-tidy version from requirements_dev.txt
version = get_clang_tidy_version_from_requirements()
hasher.update(version.encode())
# Hash relevant platformio.ini sections
pio_flags = extract_platformio_flags()
hasher.update(pio_flags.encode())
return hasher.hexdigest()
def read_stored_hash() -> str | None:
"""Read the stored hash from file"""
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
if hash_file.exists():
lines = read_file_lines(hash_file)
return lines[0].strip() if lines else None
return None
def write_file_content(path: Path, content: str) -> None:
"""Write content to a file."""
with open(path, "w") as f:
f.write(content)
def write_hash(hash_value: str) -> None:
"""Write hash to file"""
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
write_file_content(hash_file, hash_value)
def main() -> None:
parser = argparse.ArgumentParser(description="Manage clang-tidy configuration hash")
parser.add_argument(
"--check",
action="store_true",
help="Check if full scan needed (exit 0 if needed)",
)
parser.add_argument("--update", action="store_true", help="Update the hash file")
parser.add_argument(
"--update-if-changed",
action="store_true",
help="Update hash only if configuration changed (for pre-commit)",
)
parser.add_argument(
"--verify", action="store_true", help="Verify hash matches (for CI)"
)
args = parser.parse_args()
current_hash = calculate_clang_tidy_hash()
stored_hash = read_stored_hash()
if args.check:
# Exit 0 if full scan needed (hash changed or no hash file)
sys.exit(0 if current_hash != stored_hash else 1)
elif args.update:
write_hash(current_hash)
print(f"Hash updated: {current_hash}")
elif args.update_if_changed:
if current_hash != stored_hash:
write_hash(current_hash)
print(f"Clang-tidy hash updated: {current_hash}")
# Exit 0 so pre-commit can stage the file
sys.exit(0)
else:
print("Clang-tidy hash unchanged")
sys.exit(0)
elif args.verify:
if current_hash != stored_hash:
print("ERROR: Clang-tidy configuration has changed but hash not updated!")
print(f"Expected: {current_hash}")
print(f"Found: {stored_hash}")
print("\nPlease run: script/clang_tidy_hash.py --update")
sys.exit(1)
print("Hash verification passed")
else:
print(f"Current hash: {current_hash}")
print(f"Stored hash: {stored_hash}")
print(f"Match: {current_hash == stored_hash}")
if __name__ == "__main__":
main()

245
script/determine-jobs.py Executable file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Determine which CI jobs should run based on changed files.
This script is a centralized way to determine which CI jobs need to run based on
what files have changed. It outputs JSON with the following structure:
{
"integration_tests": true/false,
"clang_tidy": true/false,
"clang_format": true/false,
"python_linters": true/false,
"changed_components": ["component1", "component2", ...],
"component_test_count": 5
}
The CI workflow uses this information to:
- Skip or run integration tests
- Skip or run clang-tidy (and whether to do a full scan)
- Skip or run clang-format
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
- Determine which components to test individually
- Decide how to split component tests (if there are many)
Usage:
python script/determine-jobs.py [-b BRANCH]
Options:
-b, --branch BRANCH Branch to compare against (default: dev)
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
from helpers import (
CPP_FILE_EXTENSIONS,
ESPHOME_COMPONENTS_PATH,
PYTHON_FILE_EXTENSIONS,
changed_files,
get_all_dependencies,
get_components_from_integration_fixtures,
parse_list_components_output,
root_path,
)
def should_run_integration_tests(branch: str | None = None) -> bool:
"""Determine if integration tests should run based on changed files.
This function is used by the CI workflow to intelligently skip integration tests when they're
not needed, saving significant CI time and resources.
Integration tests will run when ANY of the following conditions are met:
1. Core C++ files changed (esphome/core/*)
- Any .cpp, .h, .tcc files in the core directory
- These files contain fundamental functionality used throughout ESPHome
- Examples: esphome/core/component.cpp, esphome/core/application.h
2. Core Python files changed (esphome/core/*.py)
- Only .py files in the esphome/core/ directory
- These are core Python files that affect the entire system
- Examples: esphome/core/config.py, esphome/core/__init__.py
- NOT included: esphome/*.py, esphome/dashboard/*.py, esphome/components/*/*.py
3. Integration test files changed
- Any file in tests/integration/ directory
- This includes test files themselves and fixture YAML files
- Examples: tests/integration/test_api.py, tests/integration/fixtures/api.yaml
4. Components used by integration tests (or their dependencies) changed
- The function parses all YAML files in tests/integration/fixtures/
- Extracts which components are used in integration tests
- Recursively finds all dependencies of those components
- If any of these components have changes, tests must run
- Example: If api.yaml uses 'sensor' and 'api' components, and 'api' depends on 'socket',
then changes to sensor/, api/, or socket/ components trigger tests
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if integration tests should run, False otherwise.
"""
files = changed_files(branch)
# Check if any core files changed (esphome/core/*)
for file in files:
if file.startswith("esphome/core/"):
return True
# Check if any integration test files changed
if any("tests/integration" in file for file in files):
return True
# Get all components used in integration tests and their dependencies
fixture_components = get_components_from_integration_fixtures()
all_required_components = get_all_dependencies(fixture_components)
# Check if any required components changed
for file in files:
if file.startswith(ESPHOME_COMPONENTS_PATH):
parts = file.split("/")
if len(parts) >= 3:
component = parts[2]
if component in all_required_components:
return True
return False
def should_run_clang_tidy(branch: str | None = None) -> bool:
"""Determine if clang-tidy should run based on changed files.
This function is used by the CI workflow to intelligently skip clang-tidy checks when they're
not needed, saving significant CI time and resources.
Clang-tidy will run when ANY of the following conditions are met:
1. Clang-tidy configuration changed
- The hash of .clang-tidy configuration file has changed
- The hash includes the .clang-tidy file, clang-tidy version from requirements_dev.txt,
and relevant platformio.ini sections
- When configuration changes, a full scan is needed to ensure all code complies
with the new rules
- Detected by script/clang_tidy_hash.py --check returning exit code 0
2. Any C++ source files changed
- Any file with C++ extensions: .cpp, .h, .hpp, .cc, .cxx, .c, .tcc
- Includes files anywhere in the repository, not just in esphome/
- This ensures all C++ code is checked, including tests, examples, etc.
- Examples: esphome/core/component.cpp, tests/custom/my_component.h
If the hash check fails for any reason, clang-tidy runs as a safety measure to ensure
code quality is maintained.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if clang-tidy should run, False otherwise.
"""
# First check if clang-tidy configuration changed (full scan needed)
try:
result = subprocess.run(
[os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"],
capture_output=True,
check=False,
)
# Exit 0 means hash changed (full scan needed)
if result.returncode == 0:
return True
except Exception:
# If hash check fails, run clang-tidy to be safe
return True
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
def should_run_clang_format(branch: str | None = None) -> bool:
"""Determine if clang-format should run based on changed files.
This function is used by the CI workflow to skip clang-format checks when no C++ files
have changed, saving CI time and resources.
Clang-format will run when any C++ source files have changed.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if clang-format should run, False otherwise.
"""
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
def should_run_python_linters(branch: str | None = None) -> bool:
"""Determine if Python linters (ruff, flake8, pylint, pyupgrade) should run based on changed files.
This function is used by the CI workflow to skip Python linting checks when no Python files
have changed, saving CI time and resources.
Python linters will run when any Python source files have changed.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if Python linters should run, False otherwise.
"""
return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS)
def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool:
"""Check if a changed file ends with any of the specified extensions."""
return any(file.endswith(extensions) for file in changed_files(branch))
def main() -> None:
"""Main function that determines which CI jobs to run."""
parser = argparse.ArgumentParser(
description="Determine which CI jobs should run based on changed files"
)
parser.add_argument(
"-b", "--branch", help="Branch to compare changed files against"
)
args = parser.parse_args()
# Determine what should run
run_integration = should_run_integration_tests(args.branch)
run_clang_tidy = should_run_clang_tidy(args.branch)
run_clang_format = should_run_clang_format(args.branch)
run_python_linters = should_run_python_linters(args.branch)
# Get changed components using list-components.py for exact compatibility
script_path = Path(__file__).parent / "list-components.py"
cmd = [sys.executable, str(script_path), "--changed"]
if args.branch:
cmd.extend(["-b", args.branch])
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
changed_components = parse_list_components_output(result.stdout)
# Build output
output: dict[str, Any] = {
"integration_tests": run_integration,
"clang_tidy": run_clang_tidy,
"clang_format": run_clang_format,
"python_linters": run_python_linters,
"changed_components": changed_components,
"component_test_count": len(changed_components),
}
# Output as JSON
print(json.dumps(output))
if __name__ == "__main__":
main()

View File

@ -1,8 +1,14 @@
from __future__ import annotations
from functools import cache
import json import json
import os
import os.path import os.path
from pathlib import Path from pathlib import Path
import re import re
import subprocess import subprocess
import time
from typing import Any
import colorama import colorama
@ -11,14 +17,42 @@ basepath = os.path.join(root_path, "esphome")
temp_folder = os.path.join(root_path, ".temp") temp_folder = os.path.join(root_path, ".temp")
temp_header_file = os.path.join(temp_folder, "all-include.cpp") temp_header_file = os.path.join(temp_folder, "all-include.cpp")
# C++ file extensions used for clang-tidy and clang-format checks
CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc")
def styled(color, msg, reset=True): # Python file extensions
PYTHON_FILE_EXTENSIONS = (".py", ".pyi")
# YAML file extensions
YAML_FILE_EXTENSIONS = (".yaml", ".yml")
# Component path prefix
ESPHOME_COMPONENTS_PATH = "esphome/components/"
def parse_list_components_output(output: str) -> list[str]:
"""Parse the output from list-components.py script.
The script outputs one component name per line.
Args:
output: The stdout from list-components.py
Returns:
List of component names, or empty list if no output
"""
if not output or not output.strip():
return []
return [c.strip() for c in output.strip().split("\n") if c.strip()]
def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str:
prefix = "".join(color) if isinstance(color, tuple) else color prefix = "".join(color) if isinstance(color, tuple) else color
suffix = colorama.Style.RESET_ALL if reset else "" suffix = colorama.Style.RESET_ALL if reset else ""
return prefix + msg + suffix return prefix + msg + suffix
def print_error_for_file(file, body): def print_error_for_file(file: str, body: str | None) -> None:
print( print(
styled(colorama.Fore.GREEN, "### File ") styled(colorama.Fore.GREEN, "### File ")
+ styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file) + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file)
@ -29,17 +63,22 @@ def print_error_for_file(file, body):
print() print()
def build_all_include(): def build_all_include() -> None:
# Build a cpp file that includes all header files in this repo. # Build a cpp file that includes all header files in this repo.
# Otherwise header-only integrations would not be tested by clang-tidy # Otherwise header-only integrations would not be tested by clang-tidy
headers = []
for path in walk_files(basepath): # Use git ls-files to find all .h files in the esphome directory
filetypes = (".h",) # This is much faster than walking the filesystem
ext = os.path.splitext(path)[1] cmd = ["git", "ls-files", "esphome/**/*.h"]
if ext in filetypes: proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, "/") # Process git output - git already returns paths relative to repo root
headers.append(f'#include "{include_p}"') headers = [
f'#include "{include_p}"'
for line in proc.stdout.strip().split("\n")
if (include_p := line.replace(os.path.sep, "/"))
]
headers.sort() headers.sort()
headers.append("") headers.append("")
content = "\n".join(headers) content = "\n".join(headers)
@ -48,29 +87,87 @@ def build_all_include():
p.write_text(content, encoding="utf-8") p.write_text(content, encoding="utf-8")
def walk_files(path): def get_output(*args: str) -> str:
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
def get_output(*args):
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
output, _ = proc.communicate() output, _ = proc.communicate()
return output.decode("utf-8") return output.decode("utf-8")
def get_err(*args): def get_err(*args: str) -> str:
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
_, err = proc.communicate() _, err = proc.communicate()
return err.decode("utf-8") return err.decode("utf-8")
def splitlines_no_ends(string): def splitlines_no_ends(string: str) -> list[str]:
return [s.strip() for s in string.splitlines()] return [s.strip() for s in string.splitlines()]
def changed_files(branch="dev"): def _get_pr_number_from_github_env() -> str | None:
"""Extract PR number from GitHub environment variables.
Returns:
PR number as string, or None if not found
"""
# First try parsing GITHUB_REF (fastest)
github_ref = os.environ.get("GITHUB_REF", "")
if "/pull/" in github_ref:
return github_ref.split("/pull/")[1].split("/")[0]
# Fallback to GitHub event file
github_event_path = os.environ.get("GITHUB_EVENT_PATH")
if github_event_path and os.path.exists(github_event_path):
with open(github_event_path) as f:
event_data = json.load(f)
pr_data = event_data.get("pull_request", {})
if pr_number := pr_data.get("number"):
return str(pr_number)
return None
@cache
def _get_changed_files_github_actions() -> list[str] | None:
"""Get changed files in GitHub Actions environment.
Returns:
List of changed files, or None if should fall back to git method
"""
event_name = os.environ.get("GITHUB_EVENT_NAME")
# For pull requests
if event_name == "pull_request":
pr_number = _get_pr_number_from_github_env()
if pr_number:
# Use GitHub CLI to get changed files directly
cmd = ["gh", "pr", "diff", pr_number, "--name-only"]
return _get_changed_files_from_command(cmd)
# For pushes (including squash-and-merge)
elif event_name == "push":
# For push events, we want to check what changed in this commit
try:
# Get the changed files in the last commit
return _get_changed_files_from_command(
["git", "diff", "HEAD~1..HEAD", "--name-only"]
)
except: # noqa: E722
# Fall back to the original method if this fails
pass
return None
def changed_files(branch: str | None = None) -> list[str]:
# In GitHub Actions, we can use the API to get changed files more efficiently
if os.environ.get("GITHUB_ACTIONS") == "true":
github_files = _get_changed_files_github_actions()
if github_files is not None:
return github_files
# Original implementation for local development
if not branch: # Treat None and empty string the same
branch = "dev"
check_remotes = ["upstream", "origin"] check_remotes = ["upstream", "origin"]
check_remotes.extend(splitlines_no_ends(get_output("git", "remote"))) check_remotes.extend(splitlines_no_ends(get_output("git", "remote")))
for remote in check_remotes: for remote in check_remotes:
@ -83,25 +180,165 @@ def changed_files(branch="dev"):
pass pass
else: else:
raise ValueError("Git not configured") raise ValueError("Git not configured")
command = ["git", "diff", merge_base, "--name-only"] return _get_changed_files_from_command(["git", "diff", merge_base, "--name-only"])
changed = splitlines_no_ends(get_output(*command))
changed = [os.path.relpath(f, os.getcwd()) for f in changed]
changed.sort()
return changed
def filter_changed(files): def _get_changed_files_from_command(command: list[str]) -> list[str]:
"""Run a git command to get changed files and return them as a list."""
proc = subprocess.run(command, capture_output=True, text=True, check=False)
if proc.returncode != 0:
raise Exception(f"Command failed: {' '.join(command)}\nstderr: {proc.stderr}")
changed_files = splitlines_no_ends(proc.stdout)
changed_files = [os.path.relpath(f, os.getcwd()) for f in changed_files if f]
changed_files.sort()
return changed_files
def get_changed_components() -> list[str] | None:
"""Get list of changed components using list-components.py script.
This function:
1. First checks if any core C++/header files (esphome/core/*.{cpp,h,hpp,cc,cxx,c}) changed - if so, returns None
2. Otherwise delegates to ./script/list-components.py --changed which:
- Analyzes all changed files
- Determines which components are affected (including dependencies)
- Returns a list of component names that need to be checked
Returns:
- None: Core C++/header files changed, need full scan
- Empty list: No components changed (only non-component files changed)
- List of strings: Names of components that need checking (e.g., ["wifi", "mqtt"])
"""
# Check if any core C++ or header files changed first
changed = changed_files() changed = changed_files()
files = [f for f in files if f in changed] core_cpp_changed = any(
print("Changed files:") f.startswith("esphome/core/")
if not files: and f.endswith(CPP_FILE_EXTENSIONS[:-1]) # Exclude .tcc for core files
print(" No changed files!") for f in changed
for c in files: )
print(f" {c}") if core_cpp_changed:
print("Core C++/header files changed - will run full clang-tidy scan")
return None
# Use list-components.py to get changed components
script_path = os.path.join(root_path, "script", "list-components.py")
cmd = [script_path, "--changed"]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, check=True, close_fds=False
)
return parse_list_components_output(result.stdout)
except subprocess.CalledProcessError:
# If the script fails, fall back to full scan
print("Could not determine changed components - will run full clang-tidy scan")
return None
def _filter_changed_ci(files: list[str]) -> list[str]:
"""Filter files based on changed components in CI environment.
This function implements intelligent filtering to reduce CI runtime by only
checking files that could be affected by the changes. It handles three scenarios:
1. Core C++/header files changed (returns None from get_changed_components):
- Triggered when any C++/header file in esphome/core/ is modified
- Action: Check ALL files (full scan)
- Reason: Core C++/header files are used throughout the codebase
2. No components changed (returns empty list from get_changed_components):
- Triggered when only non-component files changed (e.g., scripts, configs)
- Action: Check only the specific non-component files that changed
- Example: If only script/clang-tidy changed, only check that file
3. Specific components changed (returns list of component names):
- Component detection done by: ./script/list-components.py --changed
- That script analyzes which components are affected by the changed files
INCLUDING their dependencies
- Action: Check ALL files in each component that list-components.py identifies
- Example: If wifi.cpp changed, list-components.py might return ["wifi", "network"]
if network depends on wifi. We then check ALL files in both
esphome/components/wifi/ and esphome/components/network/
- Reason: Component files often have interdependencies (headers, base classes)
Args:
files: List of all files that clang-tidy would normally check
Returns:
Filtered list of files to check
"""
components = get_changed_components()
if components is None:
# Scenario 1: Core files changed or couldn't determine components
# Action: Return all files for full scan
return files
if not components:
# Scenario 2: No components changed - only non-component files changed
# Action: Check only the specific non-component files that changed
changed = changed_files()
files = [
f
for f in files
if f in changed and not f.startswith(ESPHOME_COMPONENTS_PATH)
]
if not files:
print("No files changed")
return files
# Scenario 3: Specific components changed
# Action: Check ALL files in each changed component
# Convert component list to set for O(1) lookups
component_set = set(components)
print(f"Changed components: {', '.join(sorted(components))}")
# The 'files' parameter contains ALL files in the codebase that clang-tidy would check.
# We filter this down to only files in the changed components.
# We check ALL files in each changed component (not just the changed files)
# because changes in one file can affect other files in the same component.
filtered_files = []
for f in files:
if f.startswith(ESPHOME_COMPONENTS_PATH):
# Check if file belongs to any of the changed components
parts = f.split("/")
if len(parts) >= 3 and parts[2] in component_set:
filtered_files.append(f)
return filtered_files
def _filter_changed_local(files: list[str]) -> list[str]:
"""Filter files based on git changes for local development.
Args:
files: List of all files to filter
Returns:
Filtered list of files to check
"""
# For local development, just check changed files directly
changed = changed_files()
return [f for f in files if f in changed]
def filter_changed(files: list[str]) -> list[str]:
"""Filter files to only those that changed or are in changed components.
Args:
files: List of files to filter
"""
# When running from CI, use component-based filtering
if os.environ.get("GITHUB_ACTIONS") == "true":
files = _filter_changed_ci(files)
else:
files = _filter_changed_local(files)
print_file_list(files, "Files to check after filtering:")
return files return files
def filter_grep(files, value): def filter_grep(files: list[str], value: str) -> list[str]:
matched = [] matched = []
for file in files: for file in files:
with open(file, encoding="utf-8") as handle: with open(file, encoding="utf-8") as handle:
@ -111,7 +348,7 @@ def filter_grep(files, value):
return matched return matched
def git_ls_files(patterns=None): def git_ls_files(patterns: list[str] | None = None) -> dict[str, int]:
command = ["git", "ls-files", "-s"] command = ["git", "ls-files", "-s"]
if patterns is not None: if patterns is not None:
command.extend(patterns) command.extend(patterns)
@ -121,7 +358,10 @@ def git_ls_files(patterns=None):
return {s[3].strip(): int(s[0]) for s in lines} return {s[3].strip(): int(s[0]) for s in lines}
def load_idedata(environment): def load_idedata(environment: str) -> dict[str, Any]:
start_time = time.time()
print(f"Loading IDE data for environment '{environment}'...")
platformio_ini = Path(root_path) / "platformio.ini" platformio_ini = Path(root_path) / "platformio.ini"
temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" temp_idedata = Path(temp_folder) / f"idedata-{environment}.json"
changed = False changed = False
@ -142,7 +382,10 @@ def load_idedata(environment):
changed = True changed = True
if not changed: if not changed:
return json.loads(temp_idedata.read_text()) data = json.loads(temp_idedata.read_text())
elapsed = time.time() - start_time
print(f"IDE data loaded from cache in {elapsed:.2f} seconds")
return data
# ensure temp directory exists before running pio, as it writes sdkconfig to it # ensure temp directory exists before running pio, as it writes sdkconfig to it
Path(temp_folder).mkdir(exist_ok=True) Path(temp_folder).mkdir(exist_ok=True)
@ -158,6 +401,9 @@ def load_idedata(environment):
match = re.search(r'{\s*".*}', stdout.decode("utf-8")) match = re.search(r'{\s*".*}', stdout.decode("utf-8"))
data = json.loads(match.group()) data = json.loads(match.group())
temp_idedata.write_text(json.dumps(data, indent=2) + "\n") temp_idedata.write_text(json.dumps(data, indent=2) + "\n")
elapsed = time.time() - start_time
print(f"IDE data generated and cached in {elapsed:.2f} seconds")
return data return data
@ -196,6 +442,29 @@ def get_binary(name: str, version: str) -> str:
raise raise
def print_file_list(
files: list[str], title: str = "Files:", max_files: int = 20
) -> None:
"""Print a list of files with optional truncation for large lists.
Args:
files: List of file paths to print
title: Title to print before the list
max_files: Maximum number of files to show before truncating (default: 20)
"""
print(title)
if not files:
print(" No files to check!")
elif len(files) <= max_files:
for f in sorted(files):
print(f" {f}")
else:
sorted_files = sorted(files)
for f in sorted_files[:10]:
print(f" {f}")
print(f" ... and {len(files) - 10} more files")
def get_usable_cpu_count() -> int: def get_usable_cpu_count() -> int:
"""Return the number of CPUs that can be used for processes. """Return the number of CPUs that can be used for processes.
@ -205,3 +474,83 @@ def get_usable_cpu_count() -> int:
return ( return (
os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count() os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count()
) )
def get_all_dependencies(component_names: set[str]) -> set[str]:
"""Get all dependencies for a set of components.
Args:
component_names: Set of component names to get dependencies for
Returns:
Set of all components including dependencies and auto-loaded components
"""
from esphome.const import KEY_CORE
from esphome.core import CORE
from esphome.loader import get_component
all_components: set[str] = set(component_names)
# Reset CORE to ensure clean state
CORE.reset()
# Set up fake config path for component loading
root = Path(__file__).parent.parent
CORE.config_path = str(root)
CORE.data[KEY_CORE] = {}
# Keep finding dependencies until no new ones are found
while True:
new_components: set[str] = set()
for comp_name in all_components:
comp = get_component(comp_name)
if not comp:
continue
# Add dependencies (extract component name before '.')
new_components.update(dep.split(".")[0] for dep in comp.dependencies)
# Add auto_load components
new_components.update(comp.auto_load)
# Check if we found any new components
new_components -= all_components
if not new_components:
break
all_components.update(new_components)
return all_components
def get_components_from_integration_fixtures() -> set[str]:
"""Extract all components used in integration test fixtures.
Returns:
Set of component names used in integration test fixtures
"""
import yaml
components: set[str] = set()
fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures"
for yaml_file in fixtures_dir.glob("*.yaml"):
with open(yaml_file) as f:
config: dict[str, any] | None = yaml.safe_load(f)
if not config:
continue
# Add all top-level component keys
components.update(config.keys())
# Add platform components (e.g., output.template)
for value in config.values():
if not isinstance(value, list):
continue
for item in value:
if isinstance(item, dict) and "platform" in item:
components.add(item["platform"])
return components

View File

@ -20,6 +20,12 @@ def filter_component_files(str):
return str.startswith("esphome/components/") | str.startswith("tests/components/") return str.startswith("esphome/components/") | str.startswith("tests/components/")
def get_all_component_files() -> list[str]:
"""Get all component files from git."""
files = git_ls_files()
return list(filter(filter_component_files, files))
def extract_component_names_array_from_files_array(files): def extract_component_names_array_from_files_array(files):
components = [] components = []
for file in files: for file in files:
@ -165,17 +171,20 @@ def main():
if args.branch and not args.changed: if args.branch and not args.changed:
parser.error("--branch requires --changed") parser.error("--branch requires --changed")
files = git_ls_files()
files = filter(filter_component_files, files)
if args.changed: if args.changed:
if args.branch: # When --changed is passed, only get the changed files
changed = changed_files(args.branch) changed = changed_files(args.branch)
else:
changed = changed_files()
# If any base test file(s) changed, there's no need to filter out components # If any base test file(s) changed, there's no need to filter out components
if not any("tests/test_build_components" in file for file in changed): if any("tests/test_build_components" in file for file in changed):
files = [f for f in files if f in changed] # Need to get all component files
files = get_all_component_files()
else:
# Only look at changed component files
files = [f for f in changed if filter_component_files(f)]
else:
# Get all component files
files = get_all_component_files()
for c in get_components(files, args.changed): for c in get_components(files, args.changed):
print(c) print(c)

View File

@ -0,0 +1,12 @@
i2c:
- id: i2c_gl_r01_i2c
scl: ${scl_pin}
sda: ${sda_pin}
sensor:
- platform: gl_r01_i2c
id: tof
name: "ToF sensor"
i2c_id: i2c_gl_r01_i2c
address: 0x74
update_interval: 15s

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,8 @@
sensor:
- platform: lps22
address: 0x5d
update_interval: 10s
temperature:
name: "LPS22 Temperature"
pressure:
name: "LPS22 Pressure"

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 16
sda: 17
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 5
sda: 4
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 5
sda: 4
<<: !include common.yaml

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